2019-001-MySQL 索引的原理与应用
本文节选自 《MySQL-Notes
》 ,参考文档声明在 Awesome MySQL List https://parg.co/htL。
MySQL 索引的原理与应用:索引类型,存储结构与锁
在《Algorithm-Notes》一节中,我们讨论了
索引(Index)是帮助数据库系统高效获取数据的数据结构,而数据库索引本质上是以增加额外的写操作,与用于维护索引数据结构的存储空间为代价的,用于提升数据库中数据检索效率的数据结构。索引可以帮助我们快速地定位到数据而不需要每次搜索的时候都遍历数据库中的每一行。当然,索引不是建立的越多、越长越好,因为索引除了占用空间之外,对后续数据库的增加、删除、修改都有额外的操作来更新索引。一般来说,小表使用全表扫描更快,中大表才使用索引,而超级大表索引基本无效,我们可能需要借助独立的全文索引系统;
索引类型
从索引的实现上,我们可以将其分为聚集索引与非聚集索引,或称辅助索引或二级索引,这两大类;从索引的实际应用中,又可以细分为普通索引、唯一索引、主键索引、联合索引、外键索引、全文索引这几种。

而
在

普通索引中还有唯一索引和联合索引两个特例,唯一索引在插入和修改的时候会校验该索引对应的列的值是否已经存在,联合索引将两个列的值按照申明时候的顺序进行拼接后在构建索引。
数据行并不是存储引擎管理的最小存储单位,索引只能够帮助我们定位到某个数据页,每一次磁盘读写的最小单位为也是数据页,而一个数据页内存储了多个数据行,我们需要了解数据页的内部结构才能知道存储引擎怎么定位到某一个数据行
索引选择性
对索引列和字符串前缀长度,都参考选择性(Selectivity)这个指标来确定:选择性定义为不重复的索引值和数据总记录条数的比值,其选择性越高,那么索引的查询效率也越高,譬如对于性别这种参数,建立索引根本没有意义。
Index Selectivity = Cardinality / #T
显然选择性的取值范围为 (0, 1]
,选择性越高的索引价值越大,这是由
SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM titles;
主键
在
如果在创建表时没有显式地定义主键(Primary Key
- 首先表中是否有非空的唯一索引(Unique NOT NULL
) ,如果有,则该列即为主键。 - 不符合上述条件,
InnoDB 存储引擎自动创建一个6 个字节大小的指针,用户不能查看或访问。
主键的选择
在《MySQL-Notes》一文中我们讨论过分布式场景下的分布式

自增
- 唯一性:自增
ID 很容易会被暴力破解,数据迁移的时候,特别是发生表格合并这种操作的时候,会不可避免地存在冲突。UUID 则能够保证唯一性,彻底避免冲突。 - 键长度:自增字段的长度较
UUID 小很多,这会对检索的性能有较大影响。Innodb 引擎进行数据检索时,也是先根据索引找到主键,然后根据主键找到记录;这样在主键长度短的情况下,会有较好的读性能。 - 并发性:自增
ID 并且高并发的情况下,竞争自增锁会降低数据库的吞吐能力。UUID 则能够在应用层生成UUID ,提高数据库的吞吐能力。 - 数据库索引:
InnoDB 中表数据是按照主键顺序存放的,在写入数据时候如果发生了随机IO ,那么就会频繁地移动磁盘块。当数据量大的时候,写的短板将非常明显。自增ID 中新增的数据可以默认按序排列,对于性能有很大的提升;UUID 则主键之间没有顺序规律。
主键与唯一索引
主键就是唯一索引,但是唯一索引不一定是主键,唯一索引可以为空,但是空值只能有一个,主键不能为空。对于单列索引,要求该列所有数据都不相同,但允许有
对于字符串类型,可以指定索引前缀长度
联合索引
单列索引指的是在表上为某一个字段建立的索引,一般索引的创建选择整型或者较小的定长字符串将更有利于效率的提升。联合索引指的是多个字段按照一定顺序组织的索引。以索引 (name, city, gender)
为例,其首先是按照
常见的条件联合包括了
前缀索引
联合索引前缀与最左匹配(Leftmost Prefix)
联合索引前缀指的是在建立多列索引的时候,必须按照从左到右的顺序使用全部或部分的索引列,才能充分的使用联合索引,比如:(col1, col2, col3)
使用 (col1)、(col1, col2)、(col1, col2, col3)
有效。在查询语句中会一直向右匹配直到遇到范围查询 (>,<,BETWEEN,LIKE)
就停止匹配,其后的索引列将不会使用索引来优化查找了。
以 (name, city, interest)
三个字段联合的索引为例,如果查询条件为 where name='Bush';
那么就只需要根据where name='Bush' and city='Chicago';
的查询,
由此我们可以得出联合索引前缀的注意点:
- 无法跨越字段使用联合索引,如
where name='Bush' and interest='baseball';
,对于该查询,name 字段是可以使用联合索引的第一个字段过滤大部分数据的,但是对于interest 字段,其无法通过B+ 树的特性直接定位第三个字段的索引片数据,比如这里的baseball 可能分散在了第二条和第七条数据之中。最终,interest 字段其实进行的是覆盖索引扫描。 - 对于非等值条件,如
> 、<、!= 等,联合索引前缀对于索引片的过滤只能到第一个使用非等值条件的字段为止,后续字段虽然在联合索引上也无法参与索引片的过滤。这里比如where name='Bush' and city>'Chicago' and interest='baseball';
,对于该查询条件,首先可以根据name 字段过滤索引片中第一个字段的非Bush 的数据,然后根据联合索引的第二个字段定位到索引片的Chicago 位置,由于其是非等值条件,这里MySQL 就会从定位的Chicago 往下顺序扫描,由于interest 字段是可能分散在索引第三个字段的任何位置的,因而第三个字段无法参与索引片的过滤。
因此Index(A,B,C)
# 使用索引
A>5 AND A<10 - 最左前缀匹配
A=5 AND B>6 - 最左前缀匹配
A=5 AND B=6 AND C=7 - 全列匹配
A=5 AND B IN (2,3) AND C>5 - 最左前缀匹配,填坑
# 不能使用索引
B>5 - 没有包含最左前缀
B=6 AND C=7 - 没有包含最左前缀
# 使用部分索引
A>5 AND B=2 - 使用索引 A 列
A=5 AND B>6 AND C=2 - 使用索引的 A 和 B 列
使用索引对结果进行排序,需要索引的顺序和
# 使用索引排序
ORDER BY A - 最左前缀匹配
WHERE A=5 ORDER BY B,C - 最左前缀匹配
WHERE A=5 ORDER BY B DESC - 最左前缀匹配
WHERE A>5 ORDER BY A,B - 最左前缀匹配
# 不能使用索引排序
WHERE A=5 ORDER BY B DESC,C ASC - 升降序不一致
WHERE A=5 ORDER BY B,D - D 不在索引中
WHERE A=5 ORDER BY C - 没有包含最左前缀
WHERE A>5 ORDER BY B,C - 第一列是范围条件,无法使用 BC 排序
WHERE A=5 AND B IN(1, 2) ORDER BY C - B 也是范围条件,无法用 C 排序
like 前缀
对于first_name like 'rMq%';
那么其是可以用到first_name like '%Chu%';
,其就无法使用first_name like 'rMq%';
,

字符串前缀
字符串前缀索引指的是只取字符串前几个字符建立的索引。在进行查询时,如果一个字段值较长,那么为其建立索引的成本将非常高,并且查询效率也比较低,字符串前缀索引就是为了解决这一问题而存在的。字符串前缀索引主要应用在两个方面:
- 字段前缀部分的选择性比较高;
- 字段整体的选择性不太大(如果字段整体选择性比较大则可以使用哈希索引
) 。
譬如为where first_name='qWhNIZqxcbD';
,那么
字符串前缀索引最需要注意的一个问题是如何选择前缀的长度,长度选择合适时,前缀索引的过滤性将和对整个字段建立索引的选择性几乎相等。这里我们就需要用到前面讲解的关于字段选择性的概念,即字段选择性为对该字段分组之后,数据量最大的组的数据量占总数据量的比例。这里选择前缀长度时,可以理解为,前缀的选择性为按照前缀分组之后,数据量最大的组占总数据量的比例。如下表所示为计算前缀长度的
select count(*) as cnt, first_name as perf from actor group by perf ORDER BY cnt desc limit 10; -- 0
select count(*) as cnt, left(first_name, 2) as perf from actor group by perf ORDER BY cnt desc limit 10; -- 2
select count(*) as cnt, left(first_name, 3) as perf from actor group by perf ORDER BY cnt desc limit 10; -- 3
select count(*) as cnt, left(first_name, 4) as perf from actor group by perf ORDER BY cnt desc limit 10; -- 4
其他索引
覆盖索引
覆盖索引指的是对于查询中使用的除去参与索引过滤扫描的所有字段将其加入到该查询所使用的索引尾部的索引。覆盖索引扫描的优点在于由于查询中所使用的所有字段都在同一索引的字段,因而在进行查询时只需要在索引中获取相关数据即可,而不需要回磁盘扫描相应的数据,从而避免了查询中最耗时的磁盘
select a, b, c from t where a='a' and b='b';
该查询中如果建立联合索引
三星索引
三星索引指的是对于一个查询,设立了三个通用的索引条件满足的条件,建立的索引对于特定的查询每满足一个条件就表示该索引得到一颗星,当该索引得到三颗星时就表示该索引对于该查询是一个三星索引。三星索引是对于特定查询的最优索引,建立三星索引的条件如下:
- 取出所有的等值谓词的列
(WHERE COL=…)
作为索引开头的列; - 将
ORDER BY 中的列加入到索引中; - 将查询语句中剩余的列加入到索引中,将易变得列放到最后以降低更新成本。
譬如对于如下的查询,索引 (first_name, last_name, email)
就是一个三星索引:
SELECT first_name, last_name, email FROM user WHERE first_name = 'aa' ORDER BY last_name;
三星索引的创建过程可以发现如下规律:
- 覆盖等值谓词条件,如
first_name ,可以过滤大部分的索引片数据; - 覆盖
order by 字段可以避免对结果集的排序,如last_name; - 覆盖其余字段可以避免回磁盘读取数据,即使用了覆盖索引扫描,如
email 。
索引存储结构
Memory Architecture | 内存架构

其中缓冲池占最大块内存,用来缓存各自数据,数据文件按页(每页
Storage Architecture | 存储结构

表空间作为存储结构的最高层,所有数据都存放在表空间中,默认情况下用一个共享表空间
表空间由各个段构成,
从物理意义上来看,
Process Architecture | 进程架构
默认情况下,

其中主循环的伪代码如下:
void master_thread() (
loop:
for (int i =0; i <10; i++){
do thing once per second
sleep 1 second if necessary
}
do things once per ten seconds
goto loop;
}
- 其中每秒一次的操作包括:刷新日志缓冲区(总是
) ,合并插入缓冲(可能) ,至多刷新100 个脏数据页(可能) ,如果没有当前用户活动,切换至background loop (可能) 。 - 其中每
10 秒一次的操作包括:合并至多5 个插入缓冲(总是) ,刷新日志缓冲(总是) ,刷新100 个或10 个脏页到磁盘(总是) ,产生一个检查点(总是) ,删除无用Undo 页(总是) 。 - 后台循环,若当前没有用户活动或数据库关闭时,会切换至该循环执行以下操作:删除无用的
undo 页(总是) ,合并20 个插入缓冲(总是) ,跳回到主循环(总是) ,不断刷新100 个页,直到符合条件跳转到flush loop (可能) 。 - 如果
flush loop 中也没有什么事情可做,边切换到suspend loop ,将master 线程挂起。
索引与锁
行锁 | 表锁 | 页锁 | |
---|---|---|---|
MyISAM | √ | ||
BDB | √ | √ | |
InnoDB | √ | √ |
for update
的记录不存在会导致锁住全表。当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,
譬如对于 select * from o_order where order_sn = '201912102322' for update;
这条

-
order_sn 是主键索引,这种情况将在主键索引上的order_sn = 201912102322
这条记录上加排他锁。 -
order_sn 是普通索引,并且是唯一索引,将会对普通索引上对应的一条记录加排他锁,对主键索引上对应的记录加排他锁。 -
order_sn 是普通索引,并且不是唯一索引,将会对普通索引上order_sn = 201912102322
一条或者多条记录加锁,并且对这些记录对应的主键索引上的记录加锁。这里除了加上行锁外,还会加上间隙锁,防止其他事务插入order_sn = 201912102322
的记录,然而如果是唯一索引就不需要间隙锁,行锁就可以。 -
order_sn 上没有索引,innoDB 将会在主键索引上全表扫描,这里并没有加表锁,而是将所有的记录都会加上行级排他锁,而实际上innoDB 内部做了优化,当扫描到一行记录后发现不匹配就会把锁给释放,当然这个违背了2PL 原则在事务提交的时候释放。这里除了对记录进行加锁,还会对每两个记录之间的间隙加锁,所以最终将会保存所有的间隙锁和order_sn = 201912102322
的行锁。 -
order_sn = 201912102322
这条记录不存在的情况下,如果order_sn 是主键索引,则会加一个间隙锁,而这个间隙是主键索引中order_sn 小于201912102322 的第一条记录到大于201912102322 的第一条记录。试想一下如果不加间隙锁,如果其他事物插入了一条order_sn = 201912102322
的记录,由于select for update 是当前读,即使上面那个事物没有提交,如果在该事物中重新查询一次就会发生幻读。 -
如果没有索引,则对扫描到的所有记录和间隙都加锁,如果不匹配行锁将会释放只剩下间隙锁。回忆一下上面讲的数据页的结果中又一个最大记录和最小记录,
Infimum 和Supremum Record ,这两个记录在加间隙锁的时候就会用到。
延伸阅读
此文尚未涉及