正确做法是用row_number()在连接前对右表分区排序,再通过on r.rn=1筛选;错误写法包括on中加limit/top或group by后join,会破坏left join语义。

LEFT JOIN 后只取右表第一条记录的典型错误写法
直接在 LEFT JOIN 的 ON 条件里加 LIMIT 1 或 TOP 1 是无效的——SQL 标准不支持在 JOIN 子句中限制右表行数。更常见的误操作是把右表先 GROUP BY 再连,结果却丢失了本该保留的左表空匹配(即右表无匹配时应为 NULL),违背了 LEFT JOIN 语义。
正确思路是:先对右表按关联字段分区编号,再在连接后筛选编号为 1 的记录。
用 ROW_NUMBER() 配合子查询或 CTE 实现“每组取一”
关键在于 ROW_NUMBER() 必须在连接前就对右表做分区排序,否则无法保证“每个左表主键对应右表第一条”。
推荐写法(以 PostgreSQL / SQL Server / Oracle / MySQL 8.0+ 为例):
SELECT l.*, r.name, r.amount
FROM orders l
LEFT JOIN (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY created_at DESC) AS rn
FROM order_items
) r ON l.id = r.order_id AND r.rn = 1;
注意三点:
-
PARTITION BY order_id必须和JOIN条件中的外键字段一致,否则分组错位 -
ORDER BY created_at DESC决定哪条算“第一条”,业务含义要明确(如取最新、最早、ID 最小等) -
AND r.rn = 1必须写在ON子句里,不能写在WHERE,否则会把右表无匹配的左表行过滤掉
MySQL 5.7 或 SQLite 等不支持窗口函数怎么办
这些引擎不支持 ROW_NUMBER(),得换策略。常见替代是用相关子查询或自连接,但性能较差。
例如 MySQL 5.7 可用:
SELECT l.*,
(SELECT name FROM order_items r2
WHERE r2.order_id = l.id
ORDER BY created_at DESC LIMIT 1) AS item_name,
(SELECT amount FROM order_items r2
WHERE r2.order_id = l.id
ORDER BY created_at DESC LIMIT 1) AS item_amount
FROM orders l;
缺点明显:
- 每个左表行触发两次子查询,数据量大时明显变慢
- 无法在一个子查询里同时取多个字段(除非用
(SELECT ...)包裹多列,但 MySQL 5.7 不支持) - 如果右表没有匹配,子查询返回
NULL,语义上仍符合LEFT JOIN,这点没问题
性能与索引的关键提醒
ROW_NUMBER() 方案看似简洁,但若右表极大且未建索引,PARTITION BY + ORDER BY 会产生巨大临时排序开销。
务必确保右表上有复合索引:
- PostgreSQL / SQL Server:
CREATE INDEX idx_order_items_order_id_created ON order_items (order_id, created_at DESC); - MySQL:
CREATE INDEX idx_order_items_order_id_created ON order_items (order_id, created_at);(注意 MySQL 8.0+ 才支持降序索引)
没索引时,即使只取“第一条”,数据库也可能全表扫描右表再排序——这时候比子查询还慢。
实际业务中,“第一条”的定义是否真需要严格按时间/ID 排序,还是可接受任意一条(比如用MIN(id) 聚合)?后者有时能绕过排序,但要注意语义偏移。











