索引合并
索引合并访问方法可以在查询中对一个表使用多个索引,对它们同时扫描,并且合并结果(intersect/union)。 此访问方法合并来自单个表的索引扫描; 它不会将扫描合并到多个表中。
使用索引合并的示例查询:
1
2
3
4
5
6
7
8
9
10
11
12
|
SELECT * FROM tbl_name WHERE key1 = 10 OR key2 = 20;
SELECT * FROM tbl_name
WHERE (key1 = 10 OR key2 = 20) AND non_key = 30;
SELECT * FROM t1, t2
WHERE (t1.key1 IN (1,2) OR t1.key2 LIKE 'value%')
AND t2.key1 = t1.some_col;
SELECT * FROM t1, t2
WHERE t1.key1 = 1
AND (t2.key1 = t1.some_col OR t2.key2 = t1.some_col2);
|
解释:
- 对于第一条语句:使用索引合并并集访问算法,得到key1=10的主键有序集合,得到key2=20的主键有序集合,再进行求并集;最后回表
- 对于第二条语句:先丢弃non_key=30,因为它使用不到索引,where语句就变成了where key10 or key2=20,使用索引先根据索引合并并集访问算法
- 对于第三条语句:索引合并并集访问算法求出(t1.key1 IN (1,2) OR t1.key2 LIKE ‘value%’;然后是join操作
在EXPLAIN输出中,索引合并方法在类型列中显示为index_merge。 在这种情况下,键列包含使用的索引列表,而key_len包含这些索引的最长键部分列表。
注意事项:
索引合并优化算法有以下已知的缺陷:
-
如果您的查询具有深度AND或OR嵌套的复杂WHERE子句,并且MySQL不选择最佳计划,请尝试使用以下标识转换来分配条件:
1
2
|
(x AND y) OR z => (x OR z) AND (y OR z)
(x OR y) AND z => (x AND z) OR (y AND z)
|
-
索引合并(Index Merge)不适应与全文索引(full-text indexes)
-
如果某个键上的范围扫描是可能的,则优化器将不考虑使用Index Merge Union或Index Merge Sort-Union算法。 例如,考虑这个查询:
1
|
SELECT * FROM t1 WHERE (goodkey1 < 10 OR goodkey2 < 20) AND badkey < 30;
|
对于这个查询,有两种处理方案:
- 索引合并(Index Merge)使用条件(goodkey1 < 10 OR goodkey2 < 20)
- 范围扫描(range scan)使用条件 badkey < 30
但是,优化器只考虑第二种方案。
索引合并(Index Merge)访问方法有几种算法,这些算法显示在EXPLAIN输出的Extra字段中:
- Using intersect(…)
- Using union(…)
- Using sort_union(…)
以下部分将更详细地介绍这些算法。 优化器根据各种可用选项的成本估计,在不同的可能的索引合并算法和其他访问方法之间进行选择。
索引合并(Index Merge)的使用取决于optimizer_switch系统变量的index_merge,index_merge_intersection,index_merge_union和index_merge_sort_union标志的值。默认情况下,所有这些标志都打开。 要仅启用特定算法,请将index_merge设置为关闭,并仅启用其他应允许的其他算法。
索引合并交集访问算法(The Index Merge Intersection Access Algorithm)
它的工作流程是:对于每一个使用到的索引进行查询,查询主键值集合,然后进行合并,求交集,也就是and运算。下面是使用到该算法的两种必要条件:
例如:
1
2
3
4
5
6
7
|
-- 主键可以是范围查询,二级索引只能是等值查询
SELECT * FROM innodb_table
WHERE primary_key < 10 AND key_col1 = 20;
-- 没有主键的情况
SELECT * FROM tbl_name
WHERE (key1_part1 = 1 AND key1_part2 = 2) AND key2 = 2;
|
会有这两条条件的原因:
- 它是会根据不同的索引条件查询出主键集合,然后进行归并(并集)
- 当二级索引的值相同时,其叶子节点上的数据也是根据主键排序的
- 如果存在主键和二级索引同时存在的情况,主键不是用来查询数据的,而是用来过滤数据。
- 针对一个必要条件:如果二级索引上的条件是范围条件,那么得到的数据就没有按照主键顺序排序,合并之前需要排序。(根据求并集的算法)。
- 针对组合索引:同理,组合索引也必须满足等值(即所有部分都等值查询)
- 针对主键:主键可以是范围查询而不是等值查询,是因为主键就是根据主键值排好序的
求交集的算法:
针对两个升序排序的数组,进行归并:逐个取出两个数组中的最小的值,如果相等,就放入结果集,否则将较小的数指针向后移动。时间复杂度O(N)
索引合并并集访问算法(The Index Merge Union Access Algorithm)
该算法的标准与索引合并交集访问算法相似。 当将表的WHERE子句转换由OR组合的不同键上的几个范围条件时,该算法是适用的,必要条件是:
例如:
1
2
3
4
5
6
7
8
|
-- 无主键,or 连接
SELECT * FROM t1
WHERE key1 = 1 OR key2 = 2 OR key3 = 3;
-- 既有and 也有or
SELECT * FROM innodb_table
WHERE (key1 = 1 AND key2 = 2)
OR (key3 = 'foo' AND key4 = 'bar') AND key5 = 5;
|
第一个例子中:就是简单的求并集
第二个例子中:现根据key1 = 1 AND key2 = 2和key3 = 'foo' AND key4 = 'bar'使用了两次index merge interset 得到两个主键集合进行合并;然后再根据key5=5得到主键集合,然后进行两个主键集合的合并;然后根据有序的主键序列进行回表。
求并集的算法:
两个有序的升序数组,从两个数组中取出最小的数,然后比较,如果相等并且等于目前最小值,就放入结果集中,向后移动两个指针;否则,将小的数添加到结果集中。
索引合并排序并集访问算法(The Index Merge Sort-Union Access Algorithm)
从上可以看到使用 index merge union 算法的条件太苛刻,更多的时候,会对二级索引适用范围查询而不是等值查询。因此出现了新的算法。
必要条件:
- 二级索引不必等值查询,联合索引也不必匹配所有的索引项
执行流程:
根据索引查询得到主键集合,对于每个主键集合进行排序,然后求并集。
这样做的好处是扩展了使用条件,增加了使用的范围;缺点就是消耗更大了。
例如:
1
2
3
4
5
|
SELECT * FROM tbl_name
WHERE key_col1 < 10 OR key_col2 < 20;
SELECT * FROM tbl_name
WHERE (key_col1 > 10 OR key_col2 = 20) AND nonkey_col = 30;
|
验证
通过实践验证上述分析是否正确。
先创建一个一个表,包含主键,联合索引,单列索引,然后插入10000行数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
create table t2(id int primary key, a int not null, b int not null,c int not null, d int not null,f int not null ,index idx_abc(a,b,c), index idx_d(d),index idx_f(f));
DELIMITER $$
CREATE PROCEDURE t2_copy()
BEGIN
SET @i=1;
WHILE @i<=10000 DO
INSERT INTO t2 VALUES(@i,@i,@i,@i,@i,@i);
SET @i=@i+1;
END WHILE;
END $$
DELIMITER ;
call tw_copy;
|
复现 intersect
执行sql语句:
1
2
3
4
5
6
|
mysql> explain select * from t2 where id>1000 and f=1000;
+----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+---------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+---------------------------------------------+
| 1 | SIMPLE | t2 | NULL | index_merge | PRIMARY,idx_f | idx_f,PRIMARY | 8,4 | NULL | 1 | 100.00 | Using intersect(idx_f,PRIMARY); Using where |
+----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+---------------------------------------------+
|
type列是index_merge,extra列是using intersect。
分析:使用了主键范围查找,二级索引等值查询
复现因为组合索引没有完全覆盖而导致没有使用intersect
sql语句:
1
2
3
4
5
6
7
|
mysql> explain select * from t2 where id>1000 and a=1000;
+----+-------------+-------+------------+------+-----------------+---------+---------+-------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-----------------+---------+---------+-------+------+----------+-----------------------+
| 1 | SIMPLE | t2 | NULL | ref | PRIMARY,idx_abc | idx_abc | 4 | const | 1 | 49.99 | Using index condition |
+----+-------------+-------+------------+------+-----------------+---------+---------+-------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)
|
复现因为二级索引不是等值查询而导致没有使用intersect
sql语句:
1
2
3
4
5
6
|
mysql> explain select * from t2 where id>1000 and d<1000;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | t2 | NULL | range | PRIMARY,idx_d | PRIMARY | 4 | NULL | 4973 | 10.04 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|
复现 union
执行sql语句
1
2
3
4
5
6
7
|
mysql> explain select * from t2 where f=1000 or d=1000;
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
| 1 | SIMPLE | t2 | NULL | index_merge | idx_d,idx_f | idx_f,idx_d | 4,4 | NULL | 2 | 100.00 | Using union(idx_f,idx_d); Using where |
+----+-------------+-------+------------+-------------+---------------+-------------+---------+------+------+----------+---------------------------------------+
|
复现 sort-union
可以看见tpye 是index_merge ,并且extra里实现using union(idx_f,idx_d),也就是使用了 index merge union access algorithm。
分析sql语句:使用了二级索引等值查询。
再执行sql 语句:
1
2
3
4
5
6
|
mysql> explain select * from t2 where id >1000 or a=2;
+----+-------------+-------+------------+-------------+-----------------+-----------------+---------+------+------+----------+------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------------+-----------------+-----------------+---------+------+------+----------+------------------------------------------------+
| 1 | SIMPLE | t2 | NULL | index_merge | PRIMARY,idx_abc | idx_abc,PRIMARY | 4,4 | NULL | 4974 | 100.00 | Using sort_union(idx_abc,PRIMARY); Using where |
+----+-------------+-------+------------+-------------+-----------------+-----------------+---------+------+------+----------+------------------------------------------------+
|
type是index_merge, extra 是using sort_union(idx_abc,primary),使用了 index merge sort-union access algorithm算法。
分析sql 语句:使用了组合索引,但是没有使用联合索引中的所有项,使用了主键范围查询。
死锁
mysql innodb引擎使用索引来实现行级别的锁,以下描述一下死锁发生的场景。
业务上需要更新排序值,采用事务来更新,而且更新前为i_sort_id添加了redis锁,但仍然有死锁的情况发生
事务A更新:
1
2
|
update * where i_sort_id=1 and brand_id=1
update * where i_sort_id=1 and brand_id=2
|
事务B更新:
1
2
|
update * where i_sort_id=2 and brand_id=2
update * where i_sort_id=2 and brand_id=1
|
由于更新使用了2个索引,idx_brand_id是造成死锁的关键。事务A更新了brand_id=1,但发现brand_id=2被事务B锁定,事务B发现brand_id=1被事务A锁定。2个事务均无法执行。
解决方案:
其实也很简单,为i_sort_id和brand_id建立复合索引即可。这个问题是由于索引建立不合理导致的,同时也没有考虑到index merge的影响。
性能问题及替代方案
该新特性可以在一些场景中大幅度提升查询性能,但受限于MySQL糟糕的统计信息,也导致很多场景查询性能极差甚至导致数据库崩溃。
以SELECT * FROM TB1 WHERE c1="xxx" AND c2=""xxx" 为例:
- 当c1列和c2列选择性较高时,按照c1和c2条件进行查询性能较高且返回数据集较小,再对两个数据量较小的数据集求交集的操作成本也较低,最终整个语句查询高效;
- 当c1列或c2列选择性较差且统计信息不准时,比如整表数据量2000万,按照c2列条件返回1500万数据,按照c1列返回1000条数据,此时按照c2列条件进行索引扫描+聚集索引查找的操作成本极高(可能是整表扫描的百倍消耗),对1000条数据和1500万数据求交集的成本也极高,最终导致整条SQL需要消耗大量CPU和IO资源且相应时间超长,而如果值使用c1列的索引,查询消耗资源较少且性能较高。
替代方案
最近系统中发现SQL执行异常,SQL类似为:
1
2
3
4
5
|
SELECT *
FROM tb_xxxx_xxxx
WHERE yn=0
AND C1=‘123456789’
OR C2=‘123456789’;
|
表上C1和C2列分别建有索引,但OR条件导致仅扫描任何一个索引都无法得到满足条件的全部数据,需要同时扫描两个索引并对两个临时结果求并集,但由于我们关闭了Index merge特性,导致执行优化器只能对表进行全表扫描并导致执行性能不佳。
该问题的临时解决办法为开启Index merge特性,但存在未知风险,因此我们建议修改SQL,将OR操作修改为UNION操作,使得不开启Index merge特性的情况下语句依然能使用多个索引,优化SQL为:
1
2
3
4
5
6
7
8
9
10
|
SELECT *
FROM tb_xxxx_xxxx
WHERE yn=0
AND C1=‘123456789’
UNION ALL
SELECT *
FROM tb_xxxx_xxxx
WHERE yn=0
AND C2=‘123456789’
AND C1<>‘123456789’
|
PS:
1、在第二个SELECT语句中增加第一个SELECT语句条件的反操作,从而保证两个SELECT 语句中没有重复数据,可以使用UNION ALL来求交集,避免UNION所带来的排序消耗。
2、在编写SQL语句时,需要注意OR条件的书写,
原SQL为:
1
2
3
|
WHERE yn=0
AND C1=‘123456789’
OR C2=‘123456789’
|
等价于:
1
2
|
WHERE (yn=0 AND C1=‘123456789’)
OR C2=‘123456789’
|
而实际需求要求所有返回数据满足yn=0的条件,应正确写为:
1
2
3
|
WHERE yn=0
AND (C1=‘123456789’
OR C2=‘123456789’)
|
参考:
https://gentlezuo.github.io/2019/09/11/MySQL%EF%BC%9A%E7%B4%A2%E5%BC%95%E5%90%88%E5%B9%B6/#%E7%B4%A2%E5%BC%95%E5%90%88%E5%B9%B6
https://www.jianshu.com/p/67b39af2f851