【MySQL实战】SQL优化的利器:组合索引的正确使用姿势
通过前面几篇文章的介绍,大家应该都对MySQL中索引的创建和基本技巧有了大概的了解。在实际在实际业务中,我们会遇到很多复杂的场景,比如对多个列进行查询。这时,可能会要求用户创建多个列组成的索引,如列 a 和 b 创建的组合索引,但究竟是创建(a,b)的索引,还是(b,a)的索引,结果却是完全不同的。那本篇,我们就来看一下更贴近业务实战的组合索引的创建和使用。
一、什么是组合索引
组合索引(Compound Index)是指由多个列所组合而成的 B+树索引,这和我们之前的B+ 树索引的原理完全一样,只是之前是对一个列排序,现在是对多个列排序。组合索引既可以是主键索引,也可以是二级索引,下图显示的是一个二级组合索引:
二、设计实战
1、避免额外排序
在业务中,我们经常会遇到根据某个列进行查询,并根据时间排序倒序展示的需求。例如某宝的订单页面,都是通过个人信息倒序展示个人的订单数据;又比如微博展示用户订阅的微博用户,并通过订阅时间倒序展示博文。比如如下表结构:
CREATE TABLE `orders` (
`O_ORDERKEY` int NOT NULL,
`O_CUSTKEY` int NOT NULL,
`O_ORDERSTATUS` char(1) NOT NULL,
`O_TOTALPRICE` decimal(15,2) NOT NULL,
`O_ORDERDATE` date NOT NULL,
`O_ORDERPRIORITY` char(15) NOT NULL,
`O_CLERK` char(15) NOT NULL,
`O_SHIPPRIORITY` int NOT NULL,
`O_COMMENT` varchar(79) NOT NULL,
PRIMARY KEY (`O_ORDERKEY`),
KEY `ORDERS_FK1` (`O_CUSTKEY`),
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`O_CUSTKEY`) REFERENCES `customer` (`C_CUSTKEY`)
) ENGINE=InnoDB DEFAULT
- 字段 o_orderkey 是 INT 类型的主键;
- 字段 o_custkey 是一个关联字段,关联表 customer;
- 字段 o_orderdate、o_orderstatus、o_totalprice、o_orderpriority 用于描述订单的基本详情,分别表示下单的时间、当前订单的状态、订单的总价、订单的优先级。
在有了上述订单表后,当用户查看自己的订单信息,并且需要根据订单时间排序查询时,可通过下面的 SQL:
SELECT * FROM orders
WHERE o_custkey = 147601 ORDER BY o_orderdate DESC
但由于上述表结构的索引设计时,索引 ORDERS_FK1 仅对列 O_CUSTKEY 排序,因此在取出用户 147601 的数据后,还需要一次额外的排序才能得到结果,可通过命令EXPLAIN验证:
EXPLAIN SELECT * FROM orders
WHERE o_custkey = 147601 ORDER BY o_orderdate DESC
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: orders
partitions: NULL
type: ref
possible_keys: ORDERS_FK1
key: ORDERS_FK1
key_len: 4
ref: const
rows: 19
filtered: 100.00
Extra: Using filesort
1 row in set, 1 warning (0.00 sec)
在上面的命令 EXPLAIN 输出结果中可以看到,SQL 语句的确可以使用索引 ORDERS_FK1,但在 Extra 列中显示的 Using filesort,表示还需要一次额外的排序才能得到最终的结果。
在 MySQL 8.0 版本中,通过命令 EXPLAIN 的额外选项,FORMAT=tree,观察得更为明确:
EXPLAIN FORMAT=tree
SELECT * FROM orders
WHERE o_custkey = 147601 ORDER BY o_orderdate DESC
*************************** 1. row ***************************
EXPLAIN: -> Sort: orders.O_ORDERDATE DESC (cost=18.98 rows=19)
-> Index lookup on orders using ORDERS_FK1 (O_CUSTKEY=147601)
可以看到,上述 SQL 的执行计划显示进行 Index lookup 索引查询,然后进行 Sort 排序,最终得到结果。
由于已对列 o_custky 创建索引,因此上述 SQL 语句并不会执行得特别慢,但是在海量的并发业务访问下,每次 SQL 执行都需要排序就会对业务的性能产生非常明显的影响,比如 CPU 负载变高,QPS 降低。
要解决这个问题,最好的方法是:在取出结果时已经根据字段 o_orderdate 排序,这样就不用额外的排序了。
为此,我们在表 orders 上创建新的组合索引 idx_custkey_orderdate,对字段(o_custkey,o_orderdate)进行索引:
ALTER TABLE orders ADD INDEX
idx_custkey_orderdate(o_custkey,o_orderdate);
这时再进行之前的 SQL,根据时间展示用户的订单信息,其执行计划为:
EXPLAIN FORMAT=tree
SELECT * FROM orders
WHERE o_custkey = 147601 ORDER BY o_orderdate
*************************** 1. row ***************************
EXPLAIN: -> Index lookup on orders using idx_custkey_orderdate (O_CUSTKEY=147601) (cost=6.65 rows=19)
可以看到,这时优化器使用了我们新建的索引 idx_custkey_orderdate,而且没有了 Sort 排序第二个过程。
2、避免回表
回表的概念:即 SQL 需要通过二级索引查询得到主键值,然后再根据主键值搜索主键索引,最后定位到完整的数据。
但是由于二级组合索引的叶子节点,包含索引键值和主键值,若查询的字段在二级索引的叶子节点中,则可直接返回结果,无需回表。这种通过组合索引避免回表的优化技术也称为索引覆盖(Covering Index) 。
如下SQL:
EXPLAIN
SELECT o_custkey,o_orderdate,o_totalprice
FROM orders WHERE o_custkey = 147601\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: orders
partitions: NULL
type: ref
possible_keys:
idx_custkey_orderdate,ORDERS_FK1
key: idx_custkey_orderdate
key_len: 4
ref: const
rows: 19
filtered: 100.00
Extra: NULL
执行计划显示上述SQL会使用到之前新创建的组合索引 idx_custkey_orderdate,但是,由于组合索引的叶子节点只包含(o_custkey,o_orderdate,_orderid),没有字段 o_totalprice 的值,所以需要通过 o_orderkey 回表找到对应的 o_totalprice。
再通过 EXPLAIN 的额外选项 FORMAT=tree,查看上述 SQL 的执行成本:
EXPLAIN FORMAT=tree
SELECT o_custkey,o_orderdate,o_totalprice
FROM orders WHERE o_custkey = 147601\G
*************************** 1. row ***************************
EXPLAIN: -> Index lookup on orders using idx_custkey_orderdate (O_CUSTKEY=147601) (cost=6.65 rows=19)
cost=6.65 表示的就是这条 SQL 当前的执行成本。不用关心 cost 的具体单位,cost 越小,开销越小,执行速度越快。
如果想要避免回表,可以通过索引覆盖技术,创建(o_custkey,o_orderdate,o_totalprice)的组合索引:
ALTER TABLE `orders` ADD INDEX
idx_custkey_orderdate_totalprice(o_custkey,o_orderdate,o_totalprice);
然后再次通过命令 EXPLAIN 观察执行计划:
EXPLAIN
SELECT o_custkey,o_orderdate,o_totalprice
FROM orders WHERE o_custkey = 147601\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: orders
partitions: NULL
type: ref
possible_keys:
idx_custkey_orderdate,ORDERS_FK1,idx_custkey_orderdate_totalprice
key: idx_custkey_orderdate_totalprice
key_len: 4
ref: const
rows: 19
filtered: 100.00
Extra: Using index
可以看到,这时优化器选择了新创建的组合索引 idx_custkey_orderdate_totalprice,同时这时Extra 列不为 NULL,而是显示 Using index,这就表示优化器使用了索引覆盖技术。
再次观察 SQL 的执行成本,可以看到 cost 有明显的下降,从 6.65 下降为了 2.94:
EXPLAIN FORMAT=tree
SELECT o_custkey,o_orderdate,o_totalprice
FROM orders WHERE o_custkey = 147601\G
*************************** 1. row ***************************
EXPLAIN: -> Index lookup on orders using idx_custkey_orderdate_totalprice (O_CUSTKEY=147601) (cost=2.94 rows=19)
三、组合索引的三大优势
组合索引也是一颗 B+ 树,只是索引的列由多个组成,组合索引既可以是主键索引,也可以是二级索引。通过上述的案例,我们可以得出组合索引的三大优势:
- 1、覆盖多个查询条件,如(a,b)索引可以覆盖查询 a = ? 或者 a = ? and b = ?;
- 2、避免 SQL 的额外排序,提升 SQL 性能,如 WHERE a = ? ORDER BY b 这样的查询条件;
- 3、利用组合索引包含多个列的特性,可以实现索引覆盖技术,提升 SQL 的查询性能。
文章将持续更新,欢迎关注公众号:服务端技术精选。欢迎点赞、关注、转发。
转载自:https://juejin.cn/post/7372818506533552137