非等值连接(如>、BETWEEN)无法使用Hash Join或Merge Join,只能退化为Nested Loops,若被驱动表无合适索引(如等值列未置联合索引最左位),则每轮循环触发全表扫描,导致执行计划中出现type=ALL。

非等值连接为什么执行计划里全是全表扫描
因为优化器无法用哈希或合并连接处理 >、BETWEEN 这类条件,只能退回到嵌套循环(Nested Loops),而被驱动表又没索引支持范围查找,结果就是外层每来一行,内层就扫一遍全表。比如 JOIN logs l ON u.id = l.user_id AND l.created_at > u.last_login,哪怕 logs 有 (created_at) 单列索引,也基本没用——范围字段在前,等值字段 user_id 压根用不上。
- 必须建复合索引,且等值列(如
user_id)必须在最左位,范围列(如created_at)靠右:CREATE INDEX idx_user_ts ON logs (user_id, created_at) - 如果外层
u.last_login经常为NULL,优化器可能直接放弃该索引,改走全表扫描 -
EXPLAIN里看到type=ALL或Extra含Using join buffer,基本可断定没走索引查找
用 EXISTS 替代 JOIN 后还是慢?检查这三个点
EXISTS 不是银弹。它快的前提是内层能快速“找到一个就停”,而这依赖谓词能否下推并命中索引最左前缀。
- 写法错误示例:
EXISTS (SELECT 1 FROM logs l WHERE l.created_at > u.registered_at AND l.user_id = u.id)—— 索引若为(created_at, user_id),因created_at是范围查询,user_id的等值过滤失效 - 正确索引必须是
(user_id, created_at),且查询中user_id必须参与裸等值(不能是l.user_id IS NOT NULL,也不能包在COALESCE()里) - 如果内层子查询里用了函数(如
DATE(l.created_at)),索引直接失效,哪怕结构再对也没用
什么时候该放弃非等值,改用窗口函数或物化中间表
当业务语义明确指向“每个用户最近一条满足时间条件的记录”这类场景时,硬扛 NON-EQUI JOIN 反而更慢。优化器没法把“最近”这个逻辑下推,只能反复扫描。
- 先用窗口函数筛出候选集:
SELECT user_id, ip, created_at FROM (SELECT user_id, ip, created_at, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn FROM logs WHERE created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)) t WHERE rn = 1 - 再和主表做等值
JOIN:JOIN users u ON u.id = l.user_id,此时连接条件完全可走索引 - 如果数据量极大,把窗口结果物化成临时表或物化视图,避免每次重算
SQL Server 和 MySQL 在非等值连接上的关键差异
MySQL 8.0.14+ 支持 LATERAL,但要求驱动条件必须是等值(如 l2.user_id = u.id),否则索引依然不生效;SQL Server 则依赖 APPLY,同样受限于内表是否能走索引查找,且 CROSS APPLY 配合非等值条件时,若缺少覆盖索引,仍会触发多次全表扫描。
- SQL Server 中,
APPLY内表若只有CREATE INDEX ix_t2_x ON t2(x),而查询是CROSS APPLY (SELECT * FROM t2 WHERE t2.ref_id = t1.id AND t2.x > t1.threshold),则ref_id字段无法利用单列索引 - 必须建覆盖索引:
CREATE INDEX ix_t2_ref_x ON t2(ref_id, x) INCLUDE (y, z),才能让APPLY走索引查找而非扫描 - MySQL 的
LATERAL在WHERE子句含非等值时,不会自动下推外层字段到内层索引,仍需手动确保索引顺序与驱动逻辑匹配
真正卡性能的不是语法写法,而是执行计划里看不见的 Number of Executions 和 Actual Rows Read——非等值连接容易把“扫一次”变成“扫 N 次”,而这些数字只在实际执行(EXPLAIN ANALYZE 或 SQL Server 的实际执行计划)里才暴露得清清楚楚。










