引言
在订单业务中,我们要如何做到使用非自增主键ID,但是又保证一定的查询性能呢?一般来说,我们可以通过引入索引来优化SQL查询性能,但是具体要如何优化呢?从什么方向上优化呢?
本文通过如何设计订单表ID为起点,引出关于如何设计主键保证数据库查询性能所引起的血案,来逐步发掘 "MySQL 数据库索引设计中" 的真相,一步步解析 MySQL 的索引相关知识点
思路
主键设计 => 数据库查询性能 => 索引性能 => 如何充分利用索引 => MySQL索引架构 => 主键索引 => 二级索引 => 联合索引 => 覆盖索引
DBMS 二级索引结构
数据库中,二级索引(Secondary Index)是除了主键索引之外的索引
MySQL中主键索引是属于聚簇索引,由于聚簇索引中索引和数据物理上粘连存储,所以索引是逻辑并物理有序的,也就是说主键索引不仅在B+树上是有序的,而且在磁盘中的存储位置也是有序排列的。
由于磁盘读取是每次按块读取的,物理有序的主键索引 也都会连续地 存储在同一磁盘块上 ,磁盘读取命中率就会大大提高,减少磁盘IO次数提高读写性能
而二级索引就只是逻辑上有序 ,通过磁盘中存储的一个数据结构组织数据的具体地址,而不是物理数据,是一种非聚簇索引。二级索引在这里作为补充方案,对部分常用的字段查找性能进行有取舍的优化
MySQL实现
在MySQL中的二级索引实现,是通过B+树存储二级索引结,在叶节点存储对应数据的主键ID。当使用二级索引进行查找对应数据时,会进入到对应的索引B+树中进行查找,然后从对应叶节点获取主键ID进行回表查询
回表查询
MySQL的二级索引的叶节点,存储的并不是数据的磁盘物理地址,而是主键的值,通过主键值再从主键索引中查询数据。
因此对于这个特性可以针对性地优化SQL语句:减少查询字段,
sql
SELECT * FROM users WHERE name = 'Alice';
sql
SELECT id, name FROM users WHERE name = 'Alice';
第一条SQL中select所有列,就需要回表查询所有数据。而第二条SQL中,首先是通过 name = 'Alice' 获取到了name,然后在name字段的二级索引中,获取到了id。由于到这里已经获取到所有需要的数据了,因此直接返回而没有进行回表查询。
联合索引
相较于前面提到的索引,都是单个字段 组成的索引,而我们可以还可以将多个字段组合在一起变成一个二级索引 ,也就是我们常说的联合索引。
这里我们通过一个图表来解释联合索引的构成: 假设我们有一个表 orders
,有三个字段:user_id
, status
, created_at
。
我们创建了一个联合索引:INDEX idx_order (user_id, status)
。从而我们就可以得到如下的一个索引B+树:
通过这张图,我们可以发现联合索引中,按照联合索引的排列顺序 ,整个B+树按照这个排列顺序 有序。换句话说,在这个B+索引树中,
user_id
全局上有序;但status
只在user_id
其中内部有序,而整个B+索引树中全局上无序
而这一个特性,也就引出联合索引中的一个关键使用原则:最左匹配原则
最左匹配原则 & 失效
最左匹配原则是指在使用联合索引时,MySQL 会从联合索引的最左边的列开始,依次向右匹配查询条件,直到遇到范围查询(如
>
、<
、BETWEEN
等)或不满足条件的列为止。
从刚才的B+联合索引树中,我们可以很容易就理解最左匹配原则的原理: 在这个B+索引树中,user_id
全局上有序;但status
只在user_id
其中内部有序,而整个B+索引树中全局上无序
通过遵守最左匹配原则,我们能大大提高SQL的查询性能,但是为什么有时候我们的最左匹配原则会失效呢?首先是比较常见的失效常见:
- 查询条件跳过最左列
- 查询条件包含范围查询 & 使用非等值操作符
- 查询条件
- 索引列参与表达式或函数
- 隐式类型转换
- 数据分布极端
而这里由于包含范围查询 & 使用非等值操作符导致的失效比较难理解外,其他的都一看就懂,因此不做分析
范围查询 & 非等值操作符导致的联合索引失效
B+树与链表
为什么在B+树中包含范围查询 & 使用非等值操作符会导致联合索引失效?对于这样一个问题,我们要层层刨析、分而治之。
首先,B+树的结构不仅适合精确查询,也十分适合范围查询。因为在B+树中,所有的数据都存储在叶节点中,而且所有的叶节点都必须在同一层上,且B+树的叶节点之间还会有指针相连接前后节点。 因此,根据B+树的特性,我们不妨可以将这个树作为一个链表来看 :B+树的叶节点是一条长长的链表,而在链表之上,有一个索引树指向链表分为一条条的段。似乎可以将B+树作为一种另类的跳表来看
通过上面B+树特性的分析,我们就可以简而明了地理解B+树对范围查询的支持:B+树的叶节点作为链表来看,是全局有序的。只需要通过索引找出对应的开头和结尾,就可以确定范围。
"链表"中的有序性
似乎我们看见了一个故人------全局有序性 ,而与之对应的就是局部有序性。回看我们前面的内容: 在这个B+索引树中,user_id
全局上有序;但status
只在user_id
其中内部有序,而整个B+索引树中全局上无序
我们在对次级索引进行更详细的归纳:次级索引的有序性,是基于==单个==主要索引的有序性之上的。
是的,单个主要索引之上。到了这里问题的答案也就自然引出: 在user_id > 100
中存在多个主要索引,而在这些不同的主要索引之中的次级索引,只在单个主要索引中具有有序性,而在多个索引中、全局上是不具有有序性的。 因此我们就无法用联合索引进行SQL查询WHERE user_id > 100 AND status = 0
!
举一反三,通过上面的原理WHERE user_id = 100 AND status > 1
就可以使用联合索引,首先找到user_id = 100的节点区域,在这个主要索引内部中,status就具有局部的有效性。
最后留个思考题,like的模糊查询呢?
覆盖索引
基于MySQL中回表查询的特性,我们就可以针对特定的SQL语句,创建具有所有需要查找字段的索引 ------覆盖索引
结合前面二级索引,当我们使用覆盖索引进行SQL查询,就可以之间通过B+索引树之间获取到我们全部所需要字段内容,而无需进行回表查询,实现和主键索引一样的性能
总结
至此,通过我们对 MySQL 索引结构、回表查询、联合索引、覆盖索引的学习,我们可以针对特定业务所需要执行的SQL语句,建立对应字段的覆盖索引,就可以同时实现安全性和查询性能的保障了。