关于关联子查询优化的几点理解是否准确

【TiDB 使用环境】Poc
【TiDB 版本】 v8.5.1
关于关联子查询的优化建议点,请帮看下是否都正确。

  1. 关联子查询通常会通过in,exists语句方式实现,在通用场景下更多的建议使用exists,因为exists相比in有更多的解关联处理场景(由apply优化成semi join)。

  2. 对于最常见的子查询场景:select * from t1 where exists (select 1 from t2 where t1.id=t2.id)

    1. 如果t1是小表,t2是大表,那么最佳优化方式是:t2.id必须存在索引,让Semi Join走indexJoin(只会选择t1表做驱动表),获得更好的效率(如果t2.id没有索引,则只能走HashJoin,选择t2这个大表构建哈希表,效率很低)。
    2. 如果t1是大表,t2是小表,关联匹配结果是大结果集,那么无需任何优化,Semi Join会选择Hash Join方式且只会选择t2做哈希表,获得最高半连接效率。
    3. 如果t1是大表,t2是小表,关联匹配结果是小结果集,那么最佳优化方式是:t1.id必须存在索引,且需要使用SEMI_JOIN_REWRITE这个hint关键字,让子查询优化成inner Join方式,对于inner Join优化器会自动选择indexjoin且选择t2表做驱动表,获得最高半连接效率。
  3. 对于exists语句,不能随意使用SEMI_JOIN_REWRITE这个hint关键字,因为它总是会对子查询(t2表)做分组去重然后优化成inner join,如果t2表具有非常大的数据量,那么会非常耗时,效率可能更低。

  4. 对于多表关联且存在子查询的场景,如:a inner join b outer join c where exists (select 1 from d where a.id=d.id),优化器无法对semi join做join reorder,只会最后执行semi join,因此如果想提前过滤子查询数据,那么适当评估添加SEMI_JOIN_REWRITE这个hint关键字,让子查询优化成inner join,这样可让优化器对优化后的子查询(优化成inner join)使用join reorder算法进行优化。

  5. semi join效率理论上会高于inner join,只是由于在tidb中存在一些实现上的局限性,导致多数场景不如被优化成inner join更好一些(semi join局限性:比如上面说的多表关联join reorder,hash join只能选择内表做哈希表,indexjoin只能选择外表做驱动表)。

另外,对于select * from t1 where exists (select /*+SEMI_JOIN_REWRITE() */ 1 from t2 where t1.id=t2.id),如果t2.id是主键,那么将其重写为inner join时没别要对t2做去重处理了吧?但是优化器还是会做去重(in子查询语句则不会对主键再做去重处理)。产品是否需要对其进行优化?

例子:
mysql> desc nation; --n_nationkey是主键
+-------------+--------------+------+------+---------+-------+
| Field       | Type         | Null | Key  | Default | Extra |
+-------------+--------------+------+------+---------+-------+
| N_NATIONKEY | bigint(20)   | NO   | PRI  | NULL    |       |
| N_NAME      | char(25)     | NO   |      | NULL    |       |
| N_REGIONKEY | bigint(20)   | NO   |      | NULL    |       |
| N_COMMENT   | varchar(152) | YES  |      | NULL    |       |
+-------------+--------------+------+------+---------+-------+
4 rows in set (0.00 sec)
mysql> explain select * from customer a where exists(select /*+ semi_join_rewrite() */ 1 from nation b where a.c_custkey=b.n_nationkey);
+--------------------------------+---------+-----------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| id                             | estRows | task      | access object | operator info                                                                                                                                                                   |
+--------------------------------+---------+-----------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| IndexHashJoin_19               | 25.00   | root      |               | inner join, inner:TableReader_14, outer key:tpch10.nation.n_nationkey, inner key:tpch10.customer.c_custkey, equal cond:eq(tpch10.nation.n_nationkey, tpch10.customer.c_custkey) |
| ├─StreamAgg_43(Build)          | 25.00   | root      |               | group by:tpch10.nation.n_nationkey, funcs:firstrow(tpch10.nation.n_nationkey)->tpch10.nation.n_nationkey                                                                        |
| │ └─TableReader_44             | 25.00   | root      |               | data:StreamAgg_36                                                                                                                                                               |
| │   └─StreamAgg_36             | 25.00   | cop[tikv] |               | group by:tpch10.nation.n_nationkey,                                                                                                                                             |
| │     └─TableFullScan_28       | 25.00   | cop[tikv] | table:b       | keep order:true                                                                                                                                                                 |
| └─TableReader_14(Probe)        | 25.00   | root      |               | data:TableRangeScan_13                                                                                                                                                          |
|   └─TableRangeScan_13          | 25.00   | cop[tikv] | table:a       | range: decided by [tpch10.nation.n_nationkey], keep order:false                                                                                                                 |
+--------------------------------+---------+-----------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
7 rows in set (0.00 sec)

关于关联子查询,上面的理解是否正确,请大佬指正。

2 个赞

都没意义的说法,不要看这类文字说明了,具体看相关sql执行计划,根据检索数据量分析合理性。
比如in和exists哪个更有优势的问题,优化器没问题两个就是应该性能一样的
以下例子就是tidb优化器在 exists时候选则错误,执行非常慢

1 个赞

执行计划,然后根据实际情况,灵活应对,大部分都是这样吧 :rofl:

1 个赞

根据你这个例子,猜测外表是大表,子查询内表是小表,关联结果是小结果集,套用上面这个规则逻辑看看是不是有更好的执行计划?

这里两个表g是相对小,正确的是g表先插出6条draw_id,去t表检索数据,draw_id本身也是t表主键只,不应该走全表扫描,而是应该推到tikv执行。

数据库优化多了,你就知道理论都无法解释执行计划,执行计划怎么生成的对于你来说都是黑盒。
你要学习的是能看懂执行计划,并分析这个执行计划好不好

1 个赞

明白,受教了!

他的意思是,你这个执行计划里面的table g ,可以看到有个唯一键索引。

select t.* from game_draw_fund t
where exists (select /*+SEMI_JOIN_REWRITE() */ 1 from game_draw g where g.draw_no=‘22081’ and g.draw_id=t.draw_id)

类似这样一个sql。

如果带exists的情况下,加了SEMI_JOIN_REWRITE()的话,被重写为inner join了,那么关联字段上,已经有唯一键或者主键了,就不应该还需要StreamAgg这个算子。
这个算子是用来聚合/排重的,应该直接根据主键/唯一键扫描就可以了。

你可以试试,这样是否执行计划中真的会出现StreamAgg或者hashagg这种聚合算子来排重。

总体感觉你俩说的不是一件事。

从他的描述上,我认为他说的没问题,这个执行计划里面的StreamAgg这类聚合算子确实多余,哪怕下推了,仍然多余。这里确实有个小的优化点。

:joy:确实,有的时候真的是只能根据执行计划倒推优化方案。

不符合逻辑的执行计划见多了,我还在oracle上见过按主键点查走错执行计划的单表最简单查询sql呢,完全无法解释

加了/*+SEMI_JOIN_REWRITE() */ 执行计划这样的

1 个赞

希望官方能加强exist场景下的优化器判断逻辑。

仍然有聚合hashagg。

不清楚这个hashagg算子内部发现取到的数据来自主键/唯一键这类本身已经带有唯一性的数据,是否有什么快速的处理方式。
如果没有的话,这块确实感觉浪费了很微小的一点时间,甚至可能不到1毫秒。

楼上各位说了这么多也没有举出说这个子查询最佳实践规则是错误的点,请举出反例,让我更好的总结问题。
数据库优化是一个复杂的过程,不能说复杂就只想着只有上了生产看到执行计划再调优(而且你不知道最佳实践可能也不知道怎么优化),那就出问题了。还是要总结出最佳使用方式,尽量避免踩坑,按照最佳时间来让多数情况下获得更好的结果就可以了。

1 个赞