mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-22 03:06:40 +08:00
445 lines
27 KiB
HTML
445 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
<head>
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
<link rel="icon" href="/static/favicon.png">
|
||
<title>09 索引组织表:万物皆索引.md.html</title>
|
||
<!-- Spectre.css framework -->
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
<!-- theme css & js -->
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
</head>
|
||
<body>
|
||
<div class="book-container">
|
||
<div class="book-sidebar">
|
||
<div class="book-brand">
|
||
<a href="/">
|
||
<img src="/static/favicon.png">
|
||
<span>技术文章摘抄</span>
|
||
</a>
|
||
</div>
|
||
<div class="book-menu uncollapsible">
|
||
<ul class="uncollapsible">
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li><a href="../">上一级</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/00 开篇词 从业务出发,开启海量 MySQL 架构设计.md.html">00 开篇词 从业务出发,开启海量 MySQL 架构设计</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/01 数字类型:避免自增踩坑.md.html">01 数字类型:避免自增踩坑</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/02 字符串类型:不能忽略的 COLLATION.md.html">02 字符串类型:不能忽略的 COLLATION</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/03 日期类型:TIMESTAMP 可能是巨坑.md.html">03 日期类型:TIMESTAMP 可能是巨坑</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/04 非结构存储:用好 JSON 这张牌.md.html">04 非结构存储:用好 JSON 这张牌</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/05 表结构设计:忘记范式准则.md.html">05 表结构设计:忘记范式准则</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/06 表压缩:不仅仅是空间压缩.md.html">06 表压缩:不仅仅是空间压缩</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/07 表的访问设计:你该选择 SQL 还是 NoSQL?.md.html">07 表的访问设计:你该选择 SQL 还是 NoSQL?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/08 索引:排序的艺术.md.html">08 索引:排序的艺术</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/MySQL实战宝典/09 索引组织表:万物皆索引.md.html">09 索引组织表:万物皆索引</a>
|
||
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/10 组合索引:用好,性能提升 10 倍!.md.html">10 组合索引:用好,性能提升 10 倍!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/11 索引出错:请理解 CBO 的工作原理.md.html">11 索引出错:请理解 CBO 的工作原理</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/12 JOIN 连接:到底能不能写 JOIN?.md.html">12 JOIN 连接:到底能不能写 JOIN?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/13 子查询:放心地使用子查询功能吧!.md.html">13 子查询:放心地使用子查询功能吧!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/14 分区表:哪些场景我不建议用分区表?.md.html">14 分区表:哪些场景我不建议用分区表?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/15 MySQL 复制:最简单也最容易配置出错.md.html">15 MySQL 复制:最简单也最容易配置出错</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/16 读写分离设计:复制延迟?其实是你用错了.md.html">16 读写分离设计:复制延迟?其实是你用错了</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/17 高可用设计:你怎么活用三大架构方案?.md.html">17 高可用设计:你怎么活用三大架构方案?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/18 金融级高可用架构:必不可少的数据核对.md.html">18 金融级高可用架构:必不可少的数据核对</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/19 高可用套件:选择这么多,你该如何选?.md.html">19 高可用套件:选择这么多,你该如何选?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/20 InnoDB Cluster:改变历史的新产品.md.html">20 InnoDB Cluster:改变历史的新产品</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/21 数据库备份:备份文件也要检查!.md.html">21 数据库备份:备份文件也要检查!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/22 分布式数据库架构:彻底理解什么叫分布式数据库.md.html">22 分布式数据库架构:彻底理解什么叫分布式数据库</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/23 分布式数据库表结构设计:如何正确地将数据分片?.md.html">23 分布式数据库表结构设计:如何正确地将数据分片?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/24 分布式数据库索引设计:二级索引、全局索引的最佳设计实践.md.html">24 分布式数据库索引设计:二级索引、全局索引的最佳设计实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/25 分布式数据库架构选型:分库分表 or 中间件 ?.md.html">25 分布式数据库架构选型:分库分表 or 中间件 ?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/26 分布式设计之禅:全链路的条带化设计.md.html">26 分布式设计之禅:全链路的条带化设计</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/27 分布式事务:我们到底要不要使用 2PC?.md.html">27 分布式事务:我们到底要不要使用 2PC?</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
<div class="sidebar-toggle-inner"></div>
|
||
</div>
|
||
<script>
|
||
function add_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.add('show')
|
||
}
|
||
function remove_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.remove('show')
|
||
}
|
||
function sidebar_toggle() {
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let content = document.querySelector('.off-canvas-content')
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
sidebar_toggle.classList.remove('extend')
|
||
sidebar.classList.remove('hide')
|
||
content.classList.remove('extend')
|
||
} else { // hide
|
||
sidebar_toggle.classList.add('extend')
|
||
sidebar.classList.add('hide')
|
||
content.classList.add('extend')
|
||
}
|
||
}
|
||
function open_sidebar() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.add('show')
|
||
overlay.classList.add('show')
|
||
}
|
||
function hide_canvas() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.remove('show')
|
||
overlay.classList.remove('show')
|
||
}
|
||
</script>
|
||
<div class="off-canvas-content">
|
||
<div class="columns">
|
||
<div class="column col-12 col-lg-12">
|
||
<div class="book-navbar">
|
||
<!-- For Responsive Layout -->
|
||
<header class="navbar">
|
||
<section class="navbar-section">
|
||
<a onclick="open_sidebar()">
|
||
<i class="icon icon-menu"></i>
|
||
</a>
|
||
</section>
|
||
</header>
|
||
</div>
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;">
|
||
<div class="book-post">
|
||
<p id="tip" align="center"></p>
|
||
<div><h1>09 索引组织表:万物皆索引</h1>
|
||
<p>上一讲,我已经带你了解了 B+ 树索引的基本概念,以及 MySQL 中怎么对 B+ 树索引进行基本的管理。为了让你进一步深入了解 MySQL 的 B+ 树索引的具体使用,这一讲我想和你聊一聊 MySQL InnoDB 存储引擎的索引结构。</p>
|
||
<p>InnoDB 存储引擎是 MySQL 数据库中使用最为广泛的引擎,在海量大并发的 OLTP 业务中,InnoDB 必选。它在数据存储方面有一个非常大的特点:索引组织表(Index Organized Table)。</p>
|
||
<p>接下来我就带你了解最为核心的概念:索引组织表。希望你学完今天的内容之后能理解 MySQL 是怎么存储数据和索引对象的。</p>
|
||
<h3>索引组织表</h3>
|
||
<p>数据存储有<strong>堆表和索引组织表</strong>两种方式。</p>
|
||
<p><strong>堆表中的数据无序存放,</strong> 数据的排序完全依赖于索引(Oracle、Microsoft SQL Server、PostgreSQL 早期默认支持的数据存储都是堆表结构)。</p>
|
||
<p><img src="assets/Cgp9HWCscj-AMF2xAAGCKX1ziAQ251.png" alt="png" /></p>
|
||
<p>从图中你能看到,堆表的组织结构中,数据和索引分开存储。索引是排序后的数据,而堆表中的数据是无序的,索引的叶子节点存放了数据在堆表中的地址,当堆表的数据发生改变,且位置发生了变更,所有索引中的地址都要更新,这非常影响性能,特别是对于 OLTP 业务。</p>
|
||
<p><strong>而索引组织表,数据根据主键排序存放在索引中</strong>,主键索引也叫聚集索引(Clustered Index)。在索引组织表中,数据即索引,索引即数据。</p>
|
||
<p>MySQL InnoDB 存储引擎就是这样的数据组织方式;Oracle、Microsoft SQL Server 后期也推出了支持索引组织表的存储方式。</p>
|
||
<p>但是,PostgreSQL 数据库因为只支持堆表存储,不适合 OLTP 的访问特性,虽然它后期对堆表有一定的优化,但本质是通过空间换时间,对海量并发的 OLTP 业务支持依然存在局限性。</p>
|
||
<p>回看 08 讲中的 User 表,其就是索引组织表的方式:</p>
|
||
<p><img src="assets/CioPOWCsbt-Ac7SqAARB58fvCFA564.png" alt="png" /></p>
|
||
<p>表 User 的主键是 id,所以表中的数据根据 id 排序存储,叶子节点存放了表中完整的记录,可以看到表中的数据存放在索引中,即表就是索引,索引就是表。</p>
|
||
<p>在了解完 MySQL InnoDB 的主键索引存储方式之后,接下来我们继续了解二级索引。</p>
|
||
<h3>二级索引</h3>
|
||
<p>InnoDB 存储引擎的数据是根据主键索引排序存储的,除了主键索引外,<strong>其他的索引都称之为二级索引(Secondeary Index),</strong> 或非聚集索引(None Clustered Index)。</p>
|
||
<p>二级索引也是一颗 B+ 树索引,但它和主键索引不同的是叶子节点存放的是索引键值、主键值。对于 08 讲创建的表 User,假设在列 name 上还创建了索引 idx_name,该索引就是二级索引:</p>
|
||
<pre><code>CREATE TABLE User (
|
||
id BIGINT AUTO_INCREMENT,
|
||
name VARCHAR(128) NOT NULL,
|
||
sex CHAR(6) NOT NULL,
|
||
registerDate DATETIME NOT NULL,
|
||
...
|
||
PRIMARY KEY(id), -- 主键索引
|
||
KEY idx_name(name) -- 二级索引
|
||
)
|
||
</code></pre>
|
||
<p>如果用户通过列 name 进行查询,比如下面的 SQL:</p>
|
||
<pre><code>SELECT * FROM User WHERE name = 'David',
|
||
</code></pre>
|
||
<p>通过二级索引 idx_name 只能定位主键值,需要额外再通过主键索引进行查询,才能得到最终的结果。<strong>这种“二级索引通过主键索引进行再一次查询”的操作叫作“回表”</strong>,你可以通过下图理解二级索引的查询:</p>
|
||
<p><img src="assets/CioPOWCsciqAXCpWAAEzcba-N8Q504.png" alt="png" /></p>
|
||
<p>索引组织表这样的二级索引设计有一个非常大的好处:若记录发生了修改,则其他索引无须进行维护,除非记录的主键发生了修改。</p>
|
||
<p>与堆表的索引实现对比着看,你会发现索引组织表在存在大量变更的场景下,性能优势会非常明显,因为大部分情况下都不需要维护其他二级索引。</p>
|
||
<p>前面我强调“索引组织表,数据即索引,索引即数据”。那么为了便于理解二级索引,你可以将二级索引按照一张表来进行理解,比如索引 idx_name 可以理解成一张表,如下所示:</p>
|
||
<pre><code>CREATE TABLE idx_name (
|
||
name VARCHAR(128) NOT NULL,
|
||
id BIGINT NOT NULL,
|
||
PRIAMRY KEY(name,id)
|
||
)
|
||
</code></pre>
|
||
<p>根据 name 进行查询的 SQL 可以理解为拆分成了两个步骤:</p>
|
||
<pre><code>SELECT id FROM idx_name WHERE name = ?
|
||
SELECT * FROM User WHERE id = _id; -- 回表
|
||
</code></pre>
|
||
<p>当插入数据时,你可以理解为对主键索引表、二级索引表进行了一个事务操作,要么都成功,要么都不成功:</p>
|
||
<pre><code>START TRANSATION;
|
||
INSERT INTO User VALUES (...) -- 主键索引
|
||
INSERT INTO idx_name VALUES (...) -- 二级索引
|
||
COMMIT;
|
||
</code></pre>
|
||
<p>当然,对于索引,还可以加入唯一的约束,具有唯一约束的索引称之为唯一索引,也是二级索引。</p>
|
||
<p>对于表 User,列 name 应该具有唯一约束,因为通常用户注册通常要求昵称唯一,所以表User 定义更新为:</p>
|
||
<pre><code>CREATE TABLE User (
|
||
id BIGINT AUTO_INCREMENT,
|
||
name VARCHAR(128) NOT NULL,
|
||
sex CHAR(6) NOT NULL,
|
||
registerDate DATETIME NOT NULL,
|
||
...
|
||
PRIMARY KEY(id), -- 主键索引
|
||
UNIQUE KEY idx_name(name) -- 二级索引
|
||
)
|
||
</code></pre>
|
||
<p><strong>那么对于唯一索引又该如何理解为表呢?</strong> 其实我们可以将约束理解成一张表或一个索引,故唯一索引 idx_name 应该理解为:</p>
|
||
<pre><code>CREATE TABLE idx_name (
|
||
name VARCHAR(128) NOT NULL,
|
||
id BIGINT NOT NULL,
|
||
PRIAMRY KEY(name,id)
|
||
) -- 二级索引
|
||
CREATE TABLE check_idx_name (
|
||
name VARCHAR(128),
|
||
PRIMARY KEY(name),
|
||
) -- 唯一约束
|
||
</code></pre>
|
||
<p>讲到这儿,你应该理解了吧?<strong>在索引组织表中,万物皆索引,索引就是数据,数据就是索引</strong>。</p>
|
||
<p><strong>最后,为了加深你对于索引组织表的理解</strong>,我们再来回顾一下堆表的实现。</p>
|
||
<p>堆表中的索引都是二级索引,哪怕是主键索引也是二级索引,也就是说它没有聚集索引,每次索引查询都要回表。同时,堆表中的记录全部存放在数据文件中,并且无序存放,这对互联网海量并发的 OLTP 业务来说,堆表的实现的确“过时”了。</p>
|
||
<p>以上就是二级索引的内容。</p>
|
||
<p>有的同学会提问:感觉二级索引与主键索引并没有太大的差别,所以为了进一步理解二级索引的开销,接下来我们一起学习二级索引的性能评估。</p>
|
||
<p>希望学完这部分内容之后,你能明白,<strong>为什么通常二级索引会比主键索引慢一些</strong>。</p>
|
||
<h3>二级索引的性能评估</h3>
|
||
<p>主键在设计时可以选择比较顺序的方式,比如自增整型,自增的 UUID 等,所以主键索引的排序效率和插入性能相对较高。二级索引就不一样了,它可能是比较顺序插入,也可能是完全随机的插入,<strong>具体如何呢?来看一下比较接近业务的表 User:</strong></p>
|
||
<pre><code>CREATE TABLE User (
|
||
id BINARY(16) NOT NULL,
|
||
name VARCHAR(255) NOT NULL,
|
||
sex CHAR(1) NOT NULL,
|
||
password VARCHAR(1024) NOT NULL,
|
||
money BIG INT NOT NULL DEFAULT 0,
|
||
register_date DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||
last_modify_date DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||
uuid CHAR(36) AS (BIN_TO_UUID(id)),
|
||
CHECK (sex = 'M' OR sex = 'F'),
|
||
CHECK (IS_UUID(UUID)),
|
||
PRIMARY KEY(id),
|
||
UNIQUE KEY idx_name(name),
|
||
KEY idx_register_date(register_date),
|
||
KEY idx_last_modify_date(last_modify_date)
|
||
);
|
||
</code></pre>
|
||
<p>可以看到,表 User 有三个二级索引 idx_name、idx_register_date、idx_last_modify_date。</p>
|
||
<p><strong>通常业务是无法要求用户注册的昵称是顺序的,所以索引 idx_name 的插入是随机的,</strong> 性能开销相对较大;另外用户昵称通常可更新,但业务为了性能考虑,可以限制单个用户每天、甚至是每年昵称更新的次数,比如每天更新一次,每年更新三次。</p>
|
||
<p><strong>而用户注册时间是比较顺序的,所以索引 idx_register_date 的性能开销相对较小,</strong> 另外用户注册时间一旦插入后也不会更新,只是用于标识一个注册时间。</p>
|
||
<p><strong>而关于 idx_last_modify_date ,</strong> 我在 03 讲就强调过,在真实业务的表结构设计中,你必须对每个核心业务表创建一个列 last_modify_date,标识每条记录的修改时间。</p>
|
||
<p>这时索引 idx_last_modify_date 的插入和 idx_register_date 类似,是比较顺序的,但不同的是,索引 idx_last_modify_date 会存在比较频繁的更新操作,比如用户消费导致余额修改、money 字段更新,这会导致二级索引的更新。</p>
|
||
<p>由于每个二级索引都包含了主键值,查询通过主键值进行回表,所以在设计表结构时让主键值尽可能的紧凑,为的就是能提升二级索引的性能,<strong>我在 05 讲推荐过 16 字节顺序 UUID 的列设计,这是性能和存储的最佳实践</strong>。</p>
|
||
<p>除此之外,在实际核心业务中,开发同学还有很大可能会设计带有业务属性的主键,但请牢记以下两点设计原则:</p>
|
||
<ul>
|
||
<li>要比较顺序,对聚集索引性能友好;</li>
|
||
<li>尽可能紧凑,对二级索引的性能和存储友好;</li>
|
||
</ul>
|
||
<h3>函数索引</h3>
|
||
<p>到目前为止,我们的索引都是创建在列上,从 MySQL 5.7 版本开始,MySQL 就开始支持创建函数索引 <strong>(即索引键是一个函数表达式)。</strong> 函数索引有两大用处:</p>
|
||
<ul>
|
||
<li>优化业务 SQL 性能;</li>
|
||
<li>配合虚拟列(Generated Column)。</li>
|
||
</ul>
|
||
<p><strong>先来看第一个好处,优化业务 SQL 性能。</strong></p>
|
||
<p>我们知道,不是每个开发人员都能比较深入地了解索引的原理,有时他们的表结构设计和编写 SQL 语句会存在“错误”,比如对于上面的表 User,要查询 2021 年1 月注册的用户,有些开发同学会错误地写成如下所示的 SQL:</p>
|
||
<pre><code>SELECT * FROM User
|
||
WHERE DATE_FORMAT(register_date,'%Y-%m') = '2021-01'
|
||
</code></pre>
|
||
<p>或许开发同学认为在 register_date 创建了索引,所以所有的 SQL 都可以使用该索引。<strong>但索引的本质是排序,</strong> 索引 idx_register_date 只对 register_date 的数据排序,又没有对DATE_FORMAT(register_date) 排序,因此上述 SQL 无法使用二级索引idx_register_date。</p>
|
||
<p><strong>数据库规范要求查询条件中函数写在等式右边,而不能写在左边</strong>,就是这个原因。</p>
|
||
<p>我们通过命令 EXPLAIN 查看上述 SQL 的执行计划,会更为直观地发现索引 idx_register_date没有被使用到:</p>
|
||
<pre><code>EXPLAIN SELECT * FROM User
|
||
WHERE DATE_FORMAT(register_date,'%Y-%m') = '2021-01'
|
||
*************************** 1. row ***************************
|
||
id: 1
|
||
select_type: SIMPLE
|
||
table: User
|
||
partitions: NULL
|
||
type: ALL
|
||
possible_keys: NULL
|
||
key: NULL
|
||
key_len: NULL
|
||
ref: NULL
|
||
rows: 1
|
||
filtered: 100.00
|
||
Extra: Using where
|
||
</code></pre>
|
||
<p>上述需求正确的 SQL 写法应该是,其中变化在第 2 行,主要将函数 DATE_FORMAT 插接为了一个范围查询:</p>
|
||
<pre><code>EXPLAIN SELECT * FROM User
|
||
WHERE register_date > '2021-01-01'
|
||
AND register_date < '2021-02-01'
|
||
*************************** 1. row ***************************
|
||
id: 1
|
||
select_type: SIMPLE
|
||
table: User
|
||
partitions: NULL
|
||
type: range
|
||
possible_keys: idx_register_date
|
||
key: idx_register_date
|
||
key_len: 8
|
||
ref: NULL
|
||
rows: 1
|
||
filtered: 100.00
|
||
Extra: Using index condition
|
||
</code></pre>
|
||
<p>如果线上业务真的没有按正确的 SQL 编写,那么可能造成数据库存在很多慢查询 SQL,导致业务缓慢甚至发生雪崩的场景。<strong>要尽快解决这个问题,可以使用函数索引,</strong> 创建一个DATE_FORMAT(register_date) 的索引,这样就能利用排序数据快速定位了:</p>
|
||
<pre><code>ALTER TABLE User
|
||
ADD INDEX
|
||
idx_func_register_date((DATE_FORMAT(register_date,'%Y-%m')));
|
||
</code></pre>
|
||
<p>接着用命令 EXPLAIN 查看执行计划,就会发现 SQL 可以使用到新建的索引idx_func_register_date:</p>
|
||
<pre><code>EXPLAIN SELECT * FROM User
|
||
WHERE DATE_FORMAT(register_date,'%Y-%m') = '2021-01'
|
||
*************************** 1. row ***************************
|
||
id: 1
|
||
select_type: SIMPLE
|
||
table: User
|
||
partitions: NULL
|
||
type: ref
|
||
possible_keys: idx_func_register_date
|
||
key: idx_func_register_date
|
||
key_len: 31
|
||
ref: const
|
||
rows: 1
|
||
filtered: 100.00
|
||
Extra: NULL
|
||
</code></pre>
|
||
<p>上述创建的函数索引可以解决业务线上的燃眉之急,但强烈建议业务开发同学在下一个版本中优化 SQL,否则这会导致对同一份数据做了两份索引,索引需要排序,排序多了就会影响性能。</p>
|
||
<p><strong>函数索引第二大用处是结合虚拟列使用。</strong></p>
|
||
<p>在前面的 JSON 小节中,我们已经创建了表 UserLogin:</p>
|
||
<pre><code>CREATE TABLE UserLogin (
|
||
userId BIGINT,
|
||
loginInfo JSON,
|
||
cellphone VARCHAR(255) AS (loginInfo->>"$.cellphone"),
|
||
PRIMARY KEY(userId),
|
||
UNIQUE KEY idx_cellphone(cellphone)
|
||
);
|
||
</code></pre>
|
||
<p>其中的列 cellphone 就是一个虚拟列,它是由后面的函数表达式计算而成,本身这个列不占用任何的存储空间,而索引 idx_cellphone 实质是一个函数索引。这样做得好处是在写 SQL 时可以直接使用这个虚拟列,而不用写冗长的函数:</p>
|
||
<pre><code>-- 不用虚拟列
|
||
SELECT * FROM UserLogin
|
||
WHERE loginInfo->>"$.cellphone" = '13918888888'
|
||
-- 使用虚拟列
|
||
SELECT * FROM UserLogin
|
||
WHERE cellphone = '13918888888'
|
||
</code></pre>
|
||
<p>对于爬虫类的业务,我们会从网上先爬取很多数据,其中有些是我们关心的数据,有些是不关心的数据。通过虚拟列技术,可以展示我们想要的那部分数据,再通过虚拟列上创建索引,就是对爬取的数据进行快速的访问和搜索。</p>
|
||
<h3>总结</h3>
|
||
<p>这一讲,我们对上一节索引的部分做了更为深入的介绍,你应该了解到<strong>MySQL InnoDB 存储引擎是索引组织表,以及索引组织表和堆表之间的区别。</strong> 总结来看:</p>
|
||
<ul>
|
||
<li>索引组织表主键是聚集索引,索引的叶子节点存放表中一整行完整记录;</li>
|
||
<li>除主键索引外的索引都是二级索引,索引的叶子节点存放的是(索引键值,主键值);</li>
|
||
<li>由于二级索引不存放完整记录,因此需要通过主键值再进行一次回表才能定位到完整数据;</li>
|
||
<li>索引组织表对比堆表,在海量并发的OLTP业务中能有更好的性能表现;</li>
|
||
<li>每种不同数据,对二级索引的性能开销影响是不一样的;</li>
|
||
<li>有时通过函数索引可以快速解决线上SQL的性能问题;</li>
|
||
<li>虚拟列不占用实际存储空间,在虚拟列上创建索引本质就是函数索引。</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/MySQL实战宝典/08 索引:排序的艺术.md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/MySQL实战宝典/10 组合索引:用好,性能提升 10 倍!.md.html">下一页</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
</div>
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099730d9f1d3d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
</body>
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
<script>
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag() {
|
||
dataLayer.push(arguments);
|
||
}
|
||
gtag('js', new Date());
|
||
gtag('config', 'G-NPSEEVD756');
|
||
var path = window.location.pathname
|
||
var cookie = getCookie("lastPath");
|
||
console.log(path)
|
||
if (path.replace("/", "") === "") {
|
||
if (cookie.replace("/", "") !== "") {
|
||
console.log(cookie)
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
}
|
||
} else {
|
||
setCookie("lastPath", path)
|
||
}
|
||
function setCookie(cname, cvalue) {
|
||
var d = new Date();
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
var expires = "expires=" + d.toGMTString();
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
}
|
||
function getCookie(cname) {
|
||
var name = cname + "=";
|
||
var ca = document.cookie.split(';');
|
||
for (var i = 0; i < ca.length; i++) {
|
||
var c = ca[i].trim();
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
}
|
||
return "";
|
||
}
|
||
</script>
|
||
</html>
|