左连接消除
操作场景
在业务系统中,ORM框架、视图展开、报表查询和通用SQL模板经常会生成包含多个LEFT JOIN的SQL。部分LEFT JOIN右侧表仅出现在连接条件中,查询结果并不需要读取该表的列;或者左侧表每行可能出现多次,而后续GROUP BY、DISTINCT会消除行数放大带来的影响。社区MySQL在执行这类查询时,通常仍会为这些LEFT JOIN生成访问路径并执行连接操作。对于复杂查询,这会带来额外的表扫描、索引查找、Join计算和中间结果处理开销,影响查询性能。
TaurusDB支持左连接消除功能。对于满足条件的查询,优化器可以在执行计划生成前移除结果无关的LEFT JOIN右侧表,使查询只访问真正需要的表,从而降低CPU、I/O和连接执行开销。
主要覆盖以下三类场景:
- 基于GROUP BY或DISTINCT的消除:LEFT JOIN右侧表只会放大左侧表行数,后续 GROUP BY或DISTINCT会消除重复,且不存在聚合函数等依赖行数的表达式时,可以移除右侧表。
- 基于唯一性的消除:ON 条件中的等值谓词可以确定右侧表唯一键的全部列时,每个左侧表行最多匹配一个右侧表行,可以移除右侧表。对于由GROUP BY 或 DISTINCT 形成唯一结果的派生表,也可以认为这些列具有唯一性。
- 基于EXISTS/IN等子查询语义的消除:当 LEFT JOIN 位于 EXISTS、IN、NOT EXISTS 或 NOT IN 等子查询中,且右侧表不影响子查询的布尔判断结果时,可以移除该 LEFT JOIN 右侧表。
原理介绍
左连接消除是一种基于关系代数等价规则的查询改写。优化器在保证结果集不变的前提下,识别并删除冗余的LEFT JOIN。
SELECT t1.a FROM t1 LEFT JOIN t2 ON t2.a = t1.b;
SELECT t1.a FROM t1;
左连接消除在查询解析(resolver)阶段执行,位于 Join 结构简化和 Join 信息收集之后。优化器会先简化 Join 结构,再分析 LEFT JOIN 右侧表是否被外部引用、是否满足唯一性或去重条件。满足条件时,右侧表会从后续执行计划中移除。因此,开启功能后,被消除的表不会出现在 EXPLAIN 执行计划中;通过 optimizer_trace 的 join_preparation 步骤可以看到 join_elimination 节点及被消除表数量。
转换过程
SELECT t1.a FROM t1 LEFT JOIN t2 ON t2.a > t1.b GROUP BY t1.a;
SELECT t1.a FROM t1 GROUP BY t1.a;
当 t2 仅在 LEFT JOIN 的 ON 条件中出现,且 ON t2.a > t1.b 可能使 t1 每行出现多次匹配、GROUP BY t1.a 会消除 t2 带来的重复行时,优化器可以移除 t2 表的访问。
前提条件
- TaurusDB内核版本为2.0.75.260300及以上的版本。内核版本的查询方法请参见如何查看云数据库 TaurusDB实例的版本号。
- 系统参数optimizer_switch中left_join_elimination为ON。详细内容请参见修改TaurusDB实例参数。
- SQL语句中存在LEFT JOIN/RIGHT JOIN,且满足 LEFT JOIN 消除的条件,消除后不影响查询结果。
支持的查询语句
- SELECT
- INSERT ... SELECT
- REPLACE ... SELECT
- CREATE TABLE ... SELECT
- 查询语句中的子查询
- 视图、派生表和 Prepared Statement 中的符合条件查询
约束限制
| 类别 | 约束项 | 说明 |
|---|---|---|
| 支持的连接类型 | LEFT JOIN | 支持消除。 |
| RIGHT JOIN | 在解析阶段会转换为等价LEFT JOIN,符合条件时可触发优化。 | |
| 不支持的场景 | INNER JOIN | 不支持内连接消除。 |
| 多表 UPDATE | 不支持多表UPDATE 中的 LEFT JOIN 消除。 | |
| 多表 DELETE | 不支持多表DELETE 中的 LEFT JOIN 消除。 | |
| 字段引用限制 | SELECT | 一般情况:查询列引用右侧表字段,不会进行左连接消除。 EXISTS/IN子查询内:不受此限制,因为EXISTS仅关注是否有行返回,IN不关注右侧表的列值,引用右侧表字段不影响消除。 |
| WHERE | 一般情况:过滤条件引用右侧表字段,不会进行左连接消除。 EXISTS/IN子查询内:不受此限制,因为EXISTS仅关注是否有行返回,IN不关注右侧表的列值,引用右侧表字段不影响消除。 | |
| GROUP BY | 分组列中引用右侧表字段,不会进行左连接消除。 | |
| HAVING | 分组过滤条件中引用右侧表字段,不会进行左连接消除。 | |
| ORDER BY | 排序列中引用右侧表字段,不会进行左连接消除。 | |
| 窗口函数 | 窗口函数定义或使用中引用右侧表字段,不会进行左连接消除。 | |
| 其他 JOIN 的 ON 条件 | 其他连接的连接条件中引用右侧表字段,不会进行左连接消除。 | |
| ON DUPLICATE KEY UPDATE | 写入冲突更新中引用右侧表字段,不会进行左连接消除。 | |
| DEFAULT(col) 函数 | DEFAULT(col) 函数调用中引用右侧表字段,不会进行左连接消除。 | |
| 聚合函数相关 | 包含聚合函数 | 不会基于 GROUP BY 或 DISTINCT 规则进行左连接消除。 |
| 安全条件满足 | 唯一性等其他安全条件满足时,仍可能进行左连接消除。 | |
| DISTINCT + 窗口函数 | 仅依赖 DISTINCT 去重且包含窗口函数 | 不会基于DISTINCT规则消除右侧表。 |
| 同时存在 GROUP BY | 优化器会按 GROUP BY 语义继续判断。 | |
| 唯一性判断限制 | OR 条件 | SQL语句中包含OR的连接条件时,不符合左连接消除条件。 |
| <=> 比较 | SQL语句中包含<=> 比较时,不符合左连接消除条件。 | |
| 非确定性表达式 | SQL语句中包含RAND() 等,不符合左连接消除条件。 | |
| 字符串与非字符串比较 | SQL语句中对右侧字符串列与非字符串表达式比较,不符合左连接消除条件。 | |
| 排序规则不一致 | SQL语句中两侧均为字符串但比较排序规则与右侧列排序规则不一致,不符合左连接消除条件。 | |
| 其他情况 | 即使唯一性消除被放弃,仍可能通过 GROUP BY/DISTINCT 或 EXISTS/IN 等子查询语义消除。 |
使用方法
您可以通过 optimizer_switch 参数设置 LEFT JOIN 消除功能。
| 参数名称 | 级别 | 描述 |
|---|---|---|
| optimizer_switch | Global、Session | 通过 left_join_elimination=on 开启 LEFT JOIN 消除功能,通过left_join_elimination=off 关闭该功能。默认关闭。 |
SET optimizer_switch='left_join_elimination=ON';
SET optimizer_switch='left_join_elimination=OFF';
使用示例
CREATE TABLE t1(a INT PRIMARY KEY, b INT); CREATE TABLE t2(a INT PRIMARY KEY, b INT); INSERT INTO t1 VALUES(1,1),(2,3),(7,NULL); INSERT INTO t2 VALUES(2,2),(4,5),(5,4); ANALYZE TABLE t1,t2;
SET optimizer_switch='left_join_elimination=off'; EXPLAIN FORMAT=TREE SELECT t1.a FROM t1 LEFT JOIN t2 ON t2.a > t1.b GROUP BY t1.a\G
*************************** 1. row ***************************
EXPLAIN: -> Group (no aggregates)
-> Nested loop left join (cost=1.70 rows=9)
-> Index scan on t1 using PRIMARY (cost=0.55 rows=3)
-> Filter: (t2.a > t1.b) (cost=0.18 rows=3)
-> Index range scan on t2 (re-planned for each iteration) (cost=0.18 rows=3) SET optimizer_switch='left_join_elimination=on'; EXPLAIN FORMAT=TREE SELECT t1.a FROM t1 LEFT JOIN t2 ON t2.a > t1.b GROUP BY t1.a\G
*************************** 1. row *************************** EXPLAIN: -> Table scan on t1 (cost=0.55 rows=3)
SET optimizer_switch='left_join_elimination=off'; EXPLAIN FORMAT=TREE SELECT t1.a FROM t1 LEFT JOIN t2 ON t2.a = t1.b\G
*************************** 1. row ***************************
EXPLAIN: -> Nested loop left join (cost=1.60 rows=3)
-> Table scan on t1 (cost=0.55 rows=3)
-> Single-row index lookup on t2 using PRIMARY (a=t1.b) (cost=0.28 rows=1) SET optimizer_switch='left_join_elimination=on'; EXPLAIN FORMAT=TREE SELECT t1.a FROM t1 LEFT JOIN t2 ON t2.a = t1.b\G *************************** 1. row *************************** EXPLAIN: -> Table scan on t1 (cost=0.55 rows=3)
SET optimizer_switch='left_join_elimination=off'; EXPLAIN FORMAT=TREE SELECT t0.a FROM t1 AS t0 WHERE EXISTS ( SELECT t2.b FROM t1 LEFT JOIN t2 ON t2.a > t1.b WHERE t0.a = t1.b )\G
*************************** 1. row ***************************
EXPLAIN: -> Remove duplicate t0 rows using temporary table (weedout) (cost=2.75 rows=9)
-> Nested loop left join (cost=2.75 rows=9)
-> Nested loop inner join (cost=1.60 rows=3)
-> Filter: (t1.b is not null) (cost=0.55 rows=3)
-> Table scan on t1 (cost=0.55 rows=3)
-> Single-row index lookup on t0 using PRIMARY (a=t1.b) (cost=0.28 rows=1)
-> Filter: (t2.a > t1.b) (cost=0.55 rows=3)
-> Index range scan on t2 (re-planned for each iteration) (cost=0.55 rows=3) SET optimizer_switch='left_join_elimination=on';
EXPLAIN FORMAT=TREE SELECT t0.a
FROM t1 AS t0
WHERE EXISTS (
SELECT t2.b
FROM t1 LEFT JOIN t2 ON t2.a > t1.b
WHERE t0.a = t1.b
)\G
*************************** 1. row ***************************
EXPLAIN: -> Nested loop inner join (cost=1.15 rows=3)
-> Filter: (`<subquery2>`.b is not null) (cost=0.60 rows=3)
-> Table scan on <subquery2> (cost=0.60 rows=3)
-> Materialize with deduplication (cost=0.55 rows=3)
-> Filter: (t1.b is not null) (cost=0.55 rows=3)
-> Table scan on t1 (cost=0.55 rows=3)
-> Single-row index lookup on t0 using PRIMARY (a=`<subquery2>`.b) (cost=0.35 rows=1) 执行计划可以看出,子查询内的t2被消除,优化生效。
{
"steps": [
{
"join_preparation": {
"steps": [
{
"join_elimination": {
"number_of_eliminated_tables": 1,
"expanded_query": "/* select#1 */ select `t0`.`a` AS `a` from `t1` `t0` semi join (`t1`) where ((`t0`.`a` = `t1`.`b`))"
}
}
]
}
}
]
}