索引的选择

从存储层读取数据是 SQL 计算过程中最为耗时的部分之一,TiDB 目前支持从不同的存储和不同的索引中读取数据,索引选择得是否合理将很大程度上决定一个查询的运行速度。

本章节将介绍 TiDB 如何选择索引去读入数据,以及相关的一些控制索引选择的方式。

读表

在介绍索引的选择之前,首先要了解 TiDB 有哪些读表的方式,这些方式的触发条件是什么,不同方式有什么区别,各有什么优劣。

读表算子

读表算子触发条件适用场景说明
PointGet/BatchPointGet读表的范围是一个或多个单点范围任何场景如果能被触发,通常被认为是最快的算子,因为其直接调用 kvget 的接口进行计算,不走 coprocessor
TableReader任何场景从 TiKV 端直接扫描表数据,一般被认为是效率最低的算子,除非在 _tidb_rowid 这一列上存在范围查询,或者无其他可以选择的读表算子时,才会选择这个算子
TableReader表在 TiFlash 节点上存在副本需要读取的列比较少,但是需要计算的行很多TiFlash 是列式存储,如果需要对少量的列和大量的行进行计算,一般会选择这个算子
IndexReader表有一个或多个索引,且计算所需的列被包含在索引里存在较小的索引上的范围查询,或者对索引列有顺序需求的时候当存在多个索引的时候,会根据估算代价选择合理的索引
IndexLookupReader表有一个或多个索引,且计算所需的列不完全被包含在索引里同 IndexReader因为计算列不完全被包含在索引里,所以读完索引后需要回表,这里会比 IndexReader 多一些开销

索引的选择

TiDB 在选择索引时,会基于每个读表算子的代价估算,在此基础上提供了启发式规则 "Skyline-Pruning",以降低错误估算导致选错索引的概率。

Skyline-Pruning

Skyline-Pruning 是一个针对索引的启发式过滤规则,评判一个索引的好坏需要从以下三个维度进行衡量:

  • 选择该索引读表时,是否需要回表(即该索引生成的计划是 IndexReader 还是 IndexLookupReader)。不用回表的索引在这个维度上优于需要回表的索引。

  • 选择该索引是否能满足一定的顺序。因为索引的读取可以保证某些列集合的顺序,所以满足查询要求顺序的索引在这个维度上优于不满足的索引。

  • 索引的列涵盖了多少访问条件。“访问条件”指的是可以转化为某列范围的 where 条件,如果某个索引的列集合涵盖的访问条件越多,那么它在这个维度上更优。

对于这三种维度,如果某个索引 idx_a三个维度上都不比 idx_b,且有一个维度比 idx_b,那么就会优先选择 idx_a

基于代价选择

在使用 Skyline-Pruning 规则排除了不合适的索引之后,索引的选择完全基于代价估算,读表的代价估算需要考虑以下几个方面:

  • 索引的每行数据在存储层的平均长度。
  • 索引生成的查询范围的行数量。
  • 索引的回表代价。
  • 索引查询时的范围数量。

根据这些因子和代价模型,优化器会选择一个代价最低的索引进行读表。

代价选择调优的常见问题

  1. 估算的行数量不准确?

    一般是统计信息过期或者准确度不够造成的,可以重新执行 analyze table 或者修改 analyze table 的参数。

  2. 统计信息准确,为什么读 TiFlash 更快,而优化器选择了 TiKV?

    目前区别 TiFlash 和 TiKV 的代价模型还比较粗糙,可以调小 tidb_opt_seek_factor 的值,让优化器倾向于选择 TiFlash。

  3. 统计信息准确,某个索引要回表,但是它比另一个不用回表的索引实际执行更快,为什么选择了不用回表的索引?

    碰到这种情况,可能是代价估算时对于回表的代价计算得过大,可以调小 tidb_opt_network_factor,降低回表的代价。

控制索引的选择

通过 Optimizer Hints 可以实现单条查询对索引选择的控制。

  • USE_INDEX/IGNORE_INDEX 可以强制优化器使用/不使用某些索引。

  • READ_FROM_STORAGE 可以强制优化器对于某些表选择 TiKV/TiFlash 的存储引擎进行查询。