LEFT JOIN 后加 GROUP BY 留不住左表全部记录,是因为将“找每组最新”逻辑放在 JOIN 条件中会导致空匹配时右表字段全为 NULL;正确做法是先用子查询(如 ROW_NUMBER())在右表预筛选最新记录,再 LEFT JOIN,确保左表行不丢且右表字段准确填充或为 NULL。

LEFT JOIN 后加 GROUP BY 为什么留不住左表全部记录?
因为 GROUP BY 会强制按分组字段聚合,如果右表没有匹配行,GROUP BY 本身不丢数据,但一旦你用 MAX()、ROW_NUMBER() 等窗口函数或聚合函数配合 GROUP BY,就容易误把左表的空匹配行也过滤掉——本质是逻辑写反了:不是先 JOIN 再筛右表最新,而是该先筛右表最新,再 LEFT JOIN。
用子查询预筛选右表最新记录再 LEFT JOIN
这是最直白、兼容性最好的做法。核心是把“找每组最新”这个动作从 JOIN 条件里剥离出来,单独在右表上完成,避免 JOIN 过程中因 ON 条件不满足而丢左表行。
常见错误写法:SELECT * FROM orders o LEFT JOIN order_items oi ON o.order_id = oi.order_id AND oi.created_at = (SELECT MAX(created_at) FROM order_items WHERE order_id = o.order_id)
问题:子查询在 ON 里对每行左表都执行一次,性能差;且若某 order_id 在 order_items 中无记录,子查询返回 NULL,oi.created_at = NULL 永远为 false,导致 oi 列全为 NULL —— 表面上没丢行,但右表字段全空,达不到“保留最新”的目的。
正确做法(推荐):
SELECT o.*, oi.*
FROM orders o
LEFT JOIN (
SELECT order_id, item_id, quantity, created_at,
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY created_at DESC) AS rn
FROM order_items
) oi ON o.order_id = oi.order_id AND oi.rn = 1
-
ROW_NUMBER()确保每个order_id只取一条最新(时间相同则按插入顺序定) - 子查询里不做
WHERE过滤,保证所有order_id都参与编号,哪怕对应零条记录(此时子查询结果为空,LEFT JOIN 自然补 NULL) - MySQL 5.7 不支持窗口函数?改用
GROUP BY + MAX()子查询 + 再关联取详情,但需额外去重处理
ON 条件里用相关子查询 + COALESCE 处理 NULL 边界
当无法改写为子查询 JOIN(比如视图封装、ORM 限制),必须在 ON 中判断时,要小心 NULL 语义陷阱。
错误示例:ON o.order_id = oi.order_id AND oi.created_at >= ALL (SELECT created_at FROM order_items WHERE order_id = o.order_id)
问题:若子查询无结果,ALL 返回 UNKNOWN,整个条件为 FALSE,右表字段全 NULL —— 左表行还在,但“最新记录”信息丢了。
可缓解的写法(仅限少量数据):
SELECT o.*, oi.*
FROM orders o
LEFT JOIN order_items oi ON o.order_id = oi.order_id
AND oi.created_at = (
SELECT COALESCE(MAX(created_at), '1970-01-01')
FROM order_items oi2
WHERE oi2.order_id = o.order_id
)
-
COALESCE把空结果转成一个确定值,避免 NULL 导致比较失败 - 但前提是右表
created_at不会出现'1970-01-01',否则会误匹配 - 仍存在性能隐患:每行左表触发一次子查询,且无法利用索引加速
MAX()
用 LATERAL(PostgreSQL / MySQL 8.0.14+)实现更清晰的关联
如果你用的是支持 LATERAL 的数据库,它天然适配“对左表每行执行一次右表计算”的语义,比手写子查询更安全、更易读。
SELECT o.*, oi.* FROM orders o LEFT JOIN LATERAL ( SELECT item_id, quantity, created_at FROM order_items WHERE order_id = o.order_id ORDER BY created_at DESC LIMIT 1 ) oi ON TRUE
-
LATERAL显式声明右子查询依赖左表,语义明确 -
ORDER BY ... LIMIT 1直接取最新,无需担心重复时间或编号逻辑 - 即使子查询无结果,
LEFT JOIN ... ON TRUE仍保留左表行,右表字段为 NULL - 注意:MySQL 8.0.14+ 才支持
LATERAL,旧版不可用;SQL Server 用OUTER APPLY等价
真正容易被忽略的点是:所谓“保留左表所有记录”,不只是 SELECT 结果行数不变,更要确保右表字段在有匹配时填最新值、无匹配时填 NULL——任何把“筛选最新”逻辑塞进 ON 或 WHERE 的写法,都可能在空匹配时让右表字段意外丢失,而不是安静地 NULL。窗口函数子查询 + LEFT JOIN 是目前最稳的通用解法。











