索引的选择
本章节将介绍 TiDB 如何选择索引去读入数据,以及相关的一些控制索引选择的方式。
在介绍索引的选择之前,首先要了解 TiDB 有哪些读表的方式,这些方式的触发条件是什么,不同方式有什么区别,各有什么优劣。
注意
TableReader 是基于 _tidb_rowid
的索引,TiFlash 是列存索引,所以索引的选择即是读表算子的选择。
TiDB 基于规则或基于代价来选择索引。基于的规则包括前置规则和 Skyline-Pruning。在选择索引时,TiDB 会先尝试前置规则。如果存在索引满足某一条前置规则,则直接选择该索引。否则,TiDB 会采用 Skyline-Pruning 来排除不合适的索引,然后基于每个读表算子的代价估算,选择代价最小的索引。
前置规则
TiDB 采用如下的启发式前置规则来选择索引:
规则 1:如果存在索引满足“唯一性索引全匹配 + 不需要回表(即该索引生成的计划是 IndexReader)”时,直接选择该索引。
规则 2:如果存在索引满足“唯一性索引全匹配 + 需要回表(即该索引生成的计划是 IndexLookupReader)”时,选择满足该条件且回表行数最小的索引作为候选索引。
规则 4:如果规则 2 和 3 之中仅选出一条候选索引,则选择该候选索引。如果规则 2 和 3 均选出候选索引,则选择读取行数(读索引行数 + 回表行数)较小的索引。
上述规则中的“索引全匹配”指每个索引列上均存在等值条件。在执行 EXPLAIN FORMAT = 'verbose' ...
语句时,如果前置规则匹配了某一索引,TiDB 会输出一条 NOTE 级别的 warning 提示该索引匹配了前置规则。
在以下示例中,因为索引 idx_b
满足规则 2 中“唯一性索引全匹配 + 需要回表”的条件,TiDB 选择索引 idx_b
作为访问路径,SHOW WARNING
返回了索引 idx_b
命中前置规则的提示。
Skyline-Pruning
Skyline-Pruning 是一个针对索引的启发式过滤规则,能降低错误估算导致选错索引的概率。Skyline-Pruning 从以下三个维度衡量一个索引的优劣:
索引的列涵盖了多少访问条件。“访问条件”指的是可以转化为某列范围的
where
条件,如果某个索引的列集合涵盖的访问条件越多,那么它在这个维度上更优。选择该索引是否能满足一定的顺序。因为索引的读取可以保证某些列集合的顺序,所以满足查询要求顺序的索引在这个维度上优于不满足的索引。
对于以上三种维度,如果索引 idx_a
在这三个维度上都不比 idx_b
差,且有一个维度比 idx_b
好,那么 TiDB 会优先选择 idx_a
。在执行 EXPLAIN FORMAT = 'verbose' ...
语句时,如果 Skyline-Pruning 排除了某些索引,TiDB 会输出一条 NOTE 级别的 warning 提示哪些索引在 Skyline-Pruning 排除之后保留下来。
在以下示例中,索引 idx_b
和 均劣于 idx_b_c
,因而被 Skyline-Pruning 排除,SHOW WARNING
的返回结果显示了经过 Skyline-Pruning 后剩余的索引。
mysql> CREATE TABLE t(a INT PRIMARY KEY, b INT, c INT, d INT, e INT, INDEX idx_b(b), INDEX idx_b_c(b, c), INDEX idx_e(e));
Query OK, 0 rows affected (0.01 sec)
mysql> EXPLAIN FORMAT = 'verbose' SELECT * FROM t WHERE b = 2 AND c > 4;
+-------------------------------+---------+---------+-----------+------------------------------+----------------------------------------------------+
| id | estRows | estCost | task | access object | operator info |
+-------------------------------+---------+---------+-----------+------------------------------+----------------------------------------------------+
| IndexLookUp_10 | 33.33 | 738.29 | root | | |
| ├─IndexRangeScan_8(Build) | 33.33 | 2370.00 | cop[tikv] | table:t, index:idx_b_c(b, c) | range:(2 4,2 +inf], keep order:false, stats:pseudo |
| └─TableRowIDScan_9(Probe) | 33.33 | 2370.00 | cop[tikv] | table:t | keep order:false, stats:pseudo |
+-------------------------------+---------+---------+-----------+------------------------------+----------------------------------------------------+
3 rows in set, 1 warning (0.00 sec)
+-------+------+------------------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------------------------------------------+
| Note | 1105 | [t,idx_b_c] remain after pruning paths for t given Prop{SortItems: [], TaskTp: rootTask} |
+-------+------+------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
- 索引的每行数据在存储层的平均长度。
- 索引生成的查询范围的行数量。
- 索引的回表代价。
- 索引查询时的范围数量。
根据这些因子和代价模型,优化器会选择一个代价最低的索引进行读表。
代价选择调优的常见问题
估算的行数量不准确?
一般是统计信息过期或者准确度不够造成的,可以重新执行
analyze table
或者修改analyze table
的参数。统计信息准确,为什么读 TiFlash 更快,而优化器选择了 TiKV?
目前区别 TiFlash 和 TiKV 的代价模型还比较粗糙,可以调小
tidb_opt_seek_factor
的值,让优化器倾向于选择 TiFlash。统计信息准确,某个索引要回表,但是它比另一个不用回表的索引实际执行更快,为什么选择了不用回表的索引?
碰到这种情况,可能是代价估算时对于回表的代价计算得过大,可以调小
tidb_opt_network_factor
,降低回表的代价。
通过 可以实现单条查询对索引选择的控制。
USE_INDEX
/IGNORE_INDEX
可以强制优化器使用/不使用某些索引。FORCE_INDEX
和 的作用相同。