查询优化大揭秘 TiDB的查询优化规则有哪些?
根据源码中的optRuleList
列表来看,TiDB 的逻辑计划优化规则总共有22种,下面是源码:
1 | var optRuleList = []logicalOptRule{ |
我们现在来一个一个过一遍这些 rule。
- gcSubstituter。下面是
optimize
方法的注释:
1 | // optimize try to replace the expression to indexed virtual generate column in where, group by, order by, and field clause |
简单来说,这个优化规则是将where、group by、order by或者字段子句中的表达式替换成被索引的虚拟生成列,从而能够使用索引来访问。具体例子可以看注释中的解释,或者mysql 中对生成列索引优化的解释。
columnPruner。这个方法直接没有注释了,但是也很好理解,其实就是列裁剪,对于一些用不到的列,优化过程中可以直接去除,避免多余的 IO 占用。
resultReorder。 结果重排序,对没有排序的结果插入一个排序操作符,用于对结果进行排序。
1 | /* |
buildKeySolver。提取索引信息为 schema 设置对应的 key。这个东西应该是之后会用来决定如何使用索引的,以后看到这个内容可以再回来研究一下。
decorrelateSolver。用来将
apply plan
转换成join plan
。这个应该就是关联子查询去关联,说白了就是讲一些能够转换成 join 操作的子查询重写成 join 的形式,好处是可以减少子查询重复执行次数。详细解释可以见https://docs.pingcap.com/zh/tidb/dev/correlated-subquery-optimization
1 | "Apply plan" 是一个数据库查询优化和执行的概念,用于在查询执行过程中使用嵌套循环连接或半连接操作。 |
- semiJoinRewriter。半连接重写。
1 | "semiJoin"(半连接)是数据库查询中的一种操作,用于判断一个表中的数据是否存在于另一个表中。 |
上述查询中,子查询会根据连接条件 A.id = B.id 来检查表 A 的行是否存在于表 B 中。如果存在匹配的行,则返回结果。
通过使用半连接操作,可以有效地筛选出左表中存在于右表中的行,从而减少数据量和查询的开销,提高查询性能。
1 |
|
- skewDistinctAggRewriter。将group distinct 聚集函数重写成两级聚合。可以参考下面注释中的例子来理解。这种优化是为了优化在group key数据偏移的情况下缓解数据偏移。这个规则会被应用于满足以下条件的 query:
- 至少有一个 group by的语句
- 有且仅有一个 distinct aggregate 函数(仅限于 count、avg 和 sum)
1 | // skewDistinctAggRewriter will rewrite group distinct aggregate into 2 level aggregates, e.g.: |
projectionEliminator。消除逻辑计划中多余的投影。
maxMinEliminator。消除最大值最小值函数。下面是注释,可以看一下其中的例子:
1 | // maxMinEliminator tries to eliminate max/min aggregate function. |
- ppdSolver。这个名字有意思,其实是
PredicatePushDown
的缩写。 这个是用来执行谓词下推的规则。
1 | // PredicatePushDown pushes down the predicates in the where/on/having clauses as deeply as possible. |
- outerJoinEliminator。消除外连接。主要有以下两个规则:
- 外连接消除:例如左外连接,如果父查询只使用左表的列和右表(内部表)的连接键(唯一键),则可以消除左外连接。
- 使用不考虑重复的聚合函数进行外连接消除:例如左外连接。如果父查询只使用带有 ‘distinct’ 标签的左表列,则可以消除左外连接。
下面是代码中的注释:
1 | // tryToEliminateOuterJoin will eliminate outer join plan base on the following rules |
- partitionProcessor。对分区语句进行重写。主要是做固定分区修剪。下面是注释内容:
1 | // partitionProcessor rewrites the ast for table partition. |
这里提到在谓词下推之后做分区修剪会更加简单。
collectPredicateColumnsPoint。这个方法没有一句注释。不过这个方法有几个比较重要的子方法,下面是这几个子方法的注释:(TODO)
1
2
3
4
5
6
7
8
9// CollectColumnStatsUsage collects column stats usage from logical plan.
// predicate indicates whether to collect predicate columns and histNeeded indicates whether to collect histogram-needed columns.
// The first return value is predicate columns(nil if predicate is false) and the second return value is histogram-needed columns(nil if histNeeded is false).
// collectSyncIndices will collect the indices which includes following conditions:
// 1. the indices contained the any one of histNeededColumns, eg: histNeededColumns contained A,B columns, and idx_a is
// composed up by A column, then we thought the idx_a should be collected
// 2. The stats condition of idx_a can't meet IsFullLoad, which means its stats was evicted previouslyaggregationPushDownSolver。聚集函数下推,下面是重要方法的注释:
1 | // tryToPushDownAgg tries to push down an aggregate function into a join path. If all aggFuncs are first row, we won't |
deriveTopNFromWindow。 从窗口函数中推导 TopN或Limit。按照官方文档中的例子,其实就是通过改写带有窗口函数的语句,减少无意义的排序操作。从下面的例子其实很容易理解这个过程:
1
2
3
4
5// 改写前
SELECT * FROM (SELECT ROW_NUMBER() OVER (ORDER BY a) AS rownumber FROM t) dt WHERE rownumber <= 3
// 改写后
WITH t_topN AS (SELECT a FROM t1 ORDER BY a LIMIT 3) SELECT * FROM (SELECT ROW_NUMBER() OVER (ORDER BY a) AS rownumber FROM t_topN) dt WHERE rownumber <= 3可以看出,进行改写之后,原语句中对全表的 sort 操作被简化成一个 sort + limit 操作,极大地节省了资源。
predicateSimplification。谓词简化,其实就是对一些谓词语句进行简化。
pushDownTopNOptimizer。这个方法将下推 topN或者 limit操作。
上述的几个下推方法其实都是类似的思想,讲一些计算操作或者条件判断尽可能下推到距离数据源近的地方,尽早完成数据的过滤操作,从而减少数据传输和计算的开销。
syncWaitStatsLoadPoint。同步等待数据加载。
joinReOrderSolver。递归采集 join 组,然后对每个组执行 join 重排序算法。
columnPruner。最后再进行一次列裁剪,因为前面的列裁剪可能会被
buildKeySolver
弄乱。pushDownSequenceSolver。递归执行下推序列。
Reference
[1] https://docs.pingcap.com/zh/tidb/dev/sql-logical-optimization