Skip Scan
了解到这个事情的原因,是因为最新的这个项目用的是阿里云最新版本的rds数据库,后来发现统计汇总时查询出入金的效率时快时慢。后来定位到是一个特殊的sql问题,explain看下来有时候会走skipScan,有时候又判断可能全表扫描比较快,直接走的全表扫描。后续把有个字段的分布范围缩到了4个,差不多每次都能命中skip scan。
这里简单介绍一下,在 MySQL 数据库的使用过程中,索引的高效利用一直是提升查询性能的关键。MySQL 8.0.13 版本引入了由 Facebook 贡献的 Loose Skip Scan 特性,为复合索引的查询优化带来了新的可能。这个特性解决了以往版本中,当查询条件不包含复合索引前缀列时无法有效利用索引的问题,极大地提升了查询效率。
什么是 Skip Scan Range
Skip Scan Range 是 MySQL 8.0.13 版本推出的一种新的范围扫描方式,也被称为 Loose Skip Scan。它主要针对复合索引的查询场景进行优化。
在之前的 MySQL 版本中,对于像 idx (col1,col2) 这样的复合索引,如果查询的 WHERE 条件只包含 col2,那么无法有效地利用这个索引,数据库需要扫描索引上的所有行,然后再根据 col2 上的条件进行过滤,效率很低。
而 Skip Scan Range 优化则避免了全量索引扫描,它会根据复合索引中前缀列(如 col1)上的每个值,结合后面列(如 col2)上的条件,启动多次范围扫描。每次范围扫描都会根据构建的 key 值直接在索引上定位,从而忽略那些不满足条件的记录,大幅提升查询性能。
Skip Scan Range 的实际应用示例
为了更好地理解 Skip Scan Range 的作用,我们来看一个具体的示例。
首先创建一个表 t1,并建立复合主键 PRIMARY KEY (f1, f2):
CREATE TABLE t1 (f1 INT NOT NULL, f2 INT NOT NULL, PRIMARY KEY(f1, f2));
然后向表中插入一系列数据:
INSERT INTO t1 VALUES
(1,1), (1,2), (1,3), (1,4), (1,5),
(2,1), (2,2), (2,3), (2,4), (2,5);
INSERT INTO t1 SELECT f1, f2 + 5 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 10 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 20 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 40 FROM t1;
ANALYZE TABLE t1;
现在,我们执行一个查询:SELECT f1, f2 FROM t1 WHERE f2 > 40;,并通过 EXPLAIN 来查看执行计划:
EXPLAIN SELECT f1, f2 FROM t1 WHERE f2 > 40;
执行结果如下:
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | t1 | NULL | range | PRIMARY | PRIMARY | 8 | NULL | 53 | 100.00 | Using where; Using index for skip scan |
从 Extra 列可以看到 "Using index for skip scan",这表明查询使用了 Skip Scan Range 特性。
我们还可以从 optimizer trace 中看到选择 skip scan 的过程:
"skip_scan_range": {
"potential_skip_scan_indexes": [
{
"index": "PRIMARY",
"tree_travel_cost": 0.4,
"num_groups": 3,
"rows": 53,
"cost": 10.625
}
]
},
"best_skip_scan_summary": {
"type": "skip_scan",
"index": "PRIMARY",
"key_parts_used_for_access": [
"f1",
"f2"
],
"range": [
"40 < f2"
],
"chosen": true
}
Skip Scan Range 的执行过程
从 InnoDB 的角度来看,上述查询的执行步骤如下:
-
第一次从索引左侧开始扫描
-
第二次使用 key (1,40) 扫描索引,直到第一个范围结束
-
使用 key (1),find_flag = HA_READ_AFTER_KEY,找到下一个 Key 值 2
-
使用 key (2,40) 扫描索引,直到范围结束
-
使用 Key (2) 去找大于 2 的 key 值,由于没有,因此结束扫描
通过代码注入日志打印的 search_tuple 也能验证这个过程:
STEP 1: no search_tuple
STEP 2:
DATA TUPLE: 2 fields;
0: len 4; hex 80000001; asc ;;
1: len 4; hex 80000028; asc (;;
STEP 3:
DATA TUPLE: 1 fields;
0: len 4; hex 80000001; asc ;;
STEP 4:
DATA TUPLE: 2 fields;
0: len 4; hex 80000002; asc ;;
1: len 4; hex 80000028; asc (;;
STEP 5:
DATA TUPLE: 1 fields;
0: len 4; hex 80000002; asc ;;
可以看出,使用 skip - scan 的方式避免了全索引扫描,从而提升了性能,尤其是在索引前缀列区分度比较低的时候,效果更为明显。
使用 Skip Scan Range 的条件
Skip Scan 可以通过 Hint 或者 optimizer_switch 来控制(skip_scan),默认是打开的。对于如下形式的查询:
SELECT A_1,...,A_k, B_1,...,B_m, C
FROM T
WHERE
EQ(A_1,...,A_k)
AND RNG(C);
需要满足以下条件才能使用 Skip Scan:
| 条件 | 说明 |
|---|---|
| A | 表 T 至少有一个复合索引 I,形式为:I = <A_1,...,A_k, B_1,..., B_m, C ,[D_1,...,D_n]>,其中 A 和 D 部分可以为空,但 B 和 C 部分必须非空 |
| B | 只引用了一个表 |
| C | 没有 group by 和 select distinct |
| D | 查询只引用索引中的字段 |
| E | 对 A_1...A_k 的谓词必须是等式谓词,且为常量,这包括 IN 操作符 |
| F | 查询必须是一个联合查询,即它是 OR 的 AND 组合:(COND1 (kp1) OR COND2 (kp1)) AND (COND1 (kp2) OR ...) AND ... |
| G | 在 C 上必须有范围条件 |
| H | 允许在 D 列上有条件,但 D 上的条件必须与 C 上的范围条件联合使用 |
当 Skip Scan 拥有更低的成本时,优化器会选择使用它。成本计算由 cost_skip_scan () 函数完成,它会基于索引统计信息中不同前缀列值的不同值数量(rec_per_key)来预估可能需要读取的行数。
总结
MySQL 8.0 引入的 Skip Scan Range 特性为复合索引的查询优化提供了新的思路,它使得在查询条件不包含复合索引前缀列时,也能高效地利用索引,避免了全索引扫描,显著提升了查询性能。
了解 Skip Scan Range 的工作原理和使用条件,有助于我们在实际开发中更好地设计索引和编写查询语句,充分发挥 MySQL 8.0 的性能优势。特别是在索引前缀列区分度较低的场景下,这个特性能带来明显的性能提升。