更新时间:2026-07-23 GMT+08:00
分享

左连接消除

操作场景

在业务系统中,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;
如果t2.a是唯一键,并且t2的列没有在 SELECT、WHERE、GROUP BY、HAVING、ORDER BY等位置被引用,则对 t1 的每一行,t2 最多只匹配一行。由于 LEFT JOIN不会过滤 t1 的行,且t2列不影响最终输出,该查询可等价改写为:
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 表的访问。

前提条件

支持的查询语句

  • SELECT
  • INSERT ... SELECT
  • REPLACE ... SELECT
  • CREATE TABLE ... SELECT
  • 查询语句中的子查询
  • 视图、派生表和 Prepared Statement 中的符合条件查询

约束限制

表1 约束限制

类别

约束项

说明

支持的连接类型

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 消除功能。

表2 参数说明

参数名称

级别

描述

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;
先关闭 LEFT JOIN 消除:
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
关闭 LEFT JOIN 消除后,执行计划中会保留 t2:
*************************** 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)
再开启 LEFT JOIN 消除:
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
开启左连接消除后,执行计划中只包含 t1,消除t2表:
*************************** 1. row ***************************
EXPLAIN: -> Table scan on t1  (cost=0.55 rows=3)
当 ON 条件绑定右侧表唯一键时,优化器可以证明每个左侧表行最多匹配一个右侧表行:
SET optimizer_switch='left_join_elimination=off';
EXPLAIN FORMAT=TREE SELECT t1.a
FROM t1
LEFT JOIN t2 ON t2.a = t1.b\G
关闭左连接消除后,执行计划中会保留 t2,并通过主键进行 eq_ref 访问:
*************************** 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)
开启左连接消除后,执行计划中只包含 t1:
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)
当LEFT JOIN位于EXISTS 子查询中,且右侧表不影响子查询的布尔判断结果时,可以移除该右侧表:
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
关闭左连接消除后,t2 仍保留在子查询转换后的执行计划中:
*************************** 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)
开启左连接消除后,t2 从子查询转换后的执行计划中移除:
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被消除,优化生效。

NOT EXISTS 和 NOT IN 等否定子查询在满足同类条件时也可以触发该优化,也可以通过 optimizer_trace 确认是否发生消除。开启 trace 后,如果命中左连接消除,join_elimination 节点会出现在join_preparation 步骤中:
{
  "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`))"
            }
          }
        ]
      }
    }
  ]
}

相关文档