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;

执行结果如下:

idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEt1NULLrangePRIMARYPRIMARY8NULL53100.00Using 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 的角度来看,上述查询的执行步骤如下:

  1. 第一次从索引左侧开始扫描

  2. 第二次使用 key (1,40) 扫描索引,直到第一个范围结束

  3. 使用 key (1),find_flag = HA_READ_AFTER_KEY,找到下一个 Key 值 2

  4. 使用 key (2,40) 扫描索引,直到范围结束

  5. 使用 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 的性能优势。特别是在索引前缀列区分度较低的场景下,这个特性能带来明显的性能提升。