在关系型数据库的执行引擎中,Nested Loop Join是最基础也最常用的连接算法之一。当我们写一条左连接SQL时,很多人直觉上认为“大表在左边会拖慢查询”,但实际业务里却经常观察到千万级大表左连几百行小表反而比反过来更快的现象。要解释这一点,就必须弄清楚驱动表在Nested Loop里到底扮演什么角色,以及左连接的语义如何限制了优化器的选择空间。

一、Nested Loop Join的基本工作原理
Nested Loop Join的本质是两层循环:外层循环遍历驱动表(也叫外表)的每一条记录,内层循环根据外层传来的连接键去被驱动表(内表)中查找匹配行。如果内表在连接列上有索引,那么每一次内层查找都可以走索引定位,复杂度接近O(N·logM),其中N是驱动表行数,M是被驱动表行数。
下面是一段伪代码,用来说明最朴素的Nested Loop过程:
-- 假设驱动表为 outer_tbl,被驱动表为 inner_tbl
-- 连接条件:outer_tbl.id = inner_tbl.outer_id
SELECT o.*, i.*
FROM outer_tbl o
LEFT JOIN inner_tbl i ON o.id = i.outer_id;
/* 执行引擎逻辑近似如下:
for each row r in outer_tbl:
find rows in inner_tbl where outer_id = r.id
if found:
output (r, matched_row)
else:
output (r, null)
*/
从这段逻辑能看出,外层循环的次数严格等于驱动表的行数。因此驱动表越小,外层迭代次数越少,即使内表很大,只要内表连接列有索引,总体代价依然可控。反过来若驱动表是千万行大表,外层就要循环千万次,即便每次内层查找很快,累积起来的CPU和IO开销也极为可观。
还需要注意,左连接要求保留左表全部记录,当右表无匹配时填充NULL。这一语义决定了优化器通常只能把左表作为驱动表,而不能像内连接那样自由交换内外表顺序来寻找最优计划。
二、左连接语义如何固定驱动表
在内连接(INNER JOIN)中,A join B 和 B join A 结果集相同,优化器可以基于代价估算决定让小表驱动大表。但左连接不同:大表 LEFT JOIN 小表 与 小表 LEFT JOIN 大表 的结果语义完全不一样,前者要保留大表所有行,后者要保留小表所有行。因此优化器在生成执行计划时,左表就是驱动表,右表是被驱动表,这个顺序无法颠倒。
我们用一个具体例子说明。假设 orders 表有1000万行,channel 表只有50行,记录订单来源渠道:
-- 大表左连小表:驱动表为 orders(1000万行) SELECT o.order_id, c.channel_name FROM orders o LEFT JOIN channel c ON o.channel_id = c.id; -- 小表左连大表:驱动表为 channel(50行),但语义变了 SELECT c.channel_name, o.order_id FROM channel c LEFT JOIN orders o ON c.id = o.channel_id;
第一条语句中,orders 是驱动表,外层循环1000万次,每次拿 channel_id 去 channel 表(有主键索引)查找,内层每次几乎是常数时间。第二条语句虽然驱动表只有50行,但它表达的是“列出每个渠道及对应的订单”,结果里渠道行会膨胀成订单行,与第一条业务含义不同。若业务本就需要以订单为主体,就不能为了性能改写成小表左连大表。
这也是为什么很多慢SQL出现在“大表 LEFT JOIN 小表”却依然很快,而反过来“小表 LEFT JOIN 大表”在业务不对等时根本不是同一种需求。理解这一点,就能明白左连接里驱动表由左表固定,我们优化重点是保证右表连接列有索引,而不是试图调换左右顺序。
三、为什么大表左连小表常常比预期快
不少开发者担心左表是大表会慢,其实只要右表小且连接列有索引,Nested Loop的外层虽然行数多,但内层极轻量。数据库对这类计划做了大量优化,比如批量抓取驱动表行、使用索引唯一扫描等。相比之下,若误写成内连接让小表驱动大表,但大表无索引或过滤性差,反而可能更慢。
我们对比两种写法在真实执行计划中的差异:
| 写法 | 驱动表 | 外层次数 | 内层访问 | 典型耗时 |
|---|---|---|---|---|
| 大表 LEFT JOIN 小表(小表有索引) | 大表 | 1000万 | 索引点查 | 较低 |
| 小表 INNER JOIN 大表(大表无索引) | 小表 | 50 | 大表全扫 | 极高 |
从表中可见,驱动表大小并非唯一决定因素,被驱动表的访问方式更为关键。左连接场景下,由于小表天然适合做被驱动表且容易建索引,大表左连小表反而能稳定利用索引嵌套循环,整体表现良好。
另外,统计信息准确时,优化器也会对大表驱动小表估计出较低代价,因为小表常驻内存,索引命中率高。若发现大表左连小表很慢,优先检查右表连接列是否缺索引,或左表是否有未过滤的大范围扫描。
四、实践中的优化建议
针对左连接与Nested Loop,有几个实用原则。第一,永远保证右表(被驱动表)的连接列存在索引,最好是主键或唯一索引,这样内层查找代价最低。第二,在写SQL时明确业务主体,不要用调换左右表来“骗”优化器,否则语义错误比性能问题更严重。
第三,可以通过EXPLAIN观察驱动表是否如预期。例如在MySQL中:
EXPLAIN SELECT o.order_id, c.channel_name FROM orders o LEFT JOIN channel c ON o.channel_id = c.id; -- 查看输出中第一行的 table 列,即为驱动表 -- 若发现右表无 key 列,说明索引缺失,需补充
最后,若大表左连小表仍慢,可考虑对大表加WHERE条件缩减驱动表行数,或调整数据库参数使Nested Loop批量更激进。总之,理解驱动表由左连接固定、Nested Loop代价模型偏向小内表索引查找,就能合理设计表结构和SQL,避免无谓的调优弯路。
驱动表的选择在左连接里由语法决定,性能优化的核心在于被驱动表的索引与驱动表的有效过滤,而非盲目颠倒表顺序。
Nested_Loop驱动表左连接修改时间:2026-08-02 09:57:31