多对多去重不能仅靠子查询中DISTINCT,因其仅对整行去重且不感知业务语义;应先用子查询裁剪唯一主键(如DISTINCT order_id)再JOIN,或用CTE+窗口函数实现“每订单一条最新商品”,也可用IN/EXISTS做存在性判断提升效率。

子查询里直接用 DISTINCT 不解决多对多去重
多对多关联(比如 orders ↔ products 通过 order_items)一 JOIN 就膨胀,此时在子查询里写 SELECT DISTINCT order_id FROM order_items 看似去重,但外层再 JOIN products 时,只要一个订单含多个商品,结果仍会重复出现该订单多次。DISTINCT 只对当前 SELECT 的整行生效,不感知业务语义上的“一个订单只该出现一次”。
先聚合再 JOIN:用子查询做主键裁剪
真正可控的做法是让子查询只输出你要的唯一标识(如去重后的 order_id),再拿它去驱动后续 JOIN,避免中间结果膨胀:
SELECT o.*, p.name FROM orders o JOIN (SELECT DISTINCT order_id FROM order_items WHERE qty > 0) oi ON o.id = oi.order_id JOIN products p ON p.id = o.product_id;- 关键点:子查询不带任何业务字段,只保留用于连接的主键或外键
- 如果需保留每个订单的最新商品,子查询就得换成窗口函数,不能靠 DISTINCT
- 注意索引:确保
order_items(order_id)或(order_id, qty)有索引,否则 DISTINCT 扫全表代价高
需要“每订单一条最新商品”?别用子查询套 DISTINCT
想取每个订单下单时间最晚的商品,DISTINCT 完全无能为力——它不认时间大小。必须把排序和分组逻辑放在顶层或 CTE 中:
- 错误写法:
SELECT DISTINCT ON (o.id) o.id, p.name, p.created_at FROM orders o JOIN order_items oi ON o.id = oi.order_id JOIN products p ON p.id = oi.product_id ORDER BY o.id, p.created_at DESC;—— 这看似可行,但一旦外层再加 WHERE 或 LIMIT,就可能漏掉某订单的首行 - 正确结构:用 CTE 预算好每个订单要哪条商品:
WITH latest_item AS (SELECT order_id, product_id, ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY updated_at DESC) rn FROM order_items) SELECT o.*, p.name FROM orders o JOIN latest_item li ON o.id = li.order_id AND li.rn = 1 JOIN products p ON p.id = li.product_id; - CTE 比嵌套子查询更易读、PostgreSQL 优化器也更愿意下推条件
IN / EXISTS 替代 JOIN 是更轻量的去重入口
如果你只是想“查出所有买过指定类目商品的订单”,根本不需要 JOIN 全量数据,用子查询做存在性判断更安全高效:
SELECT * FROM orders WHERE id IN (SELECT DISTINCT order_id FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE p.category = 'electronics');- 比 JOIN + DISTINCT 更快,因为数据库可提前终止子查询(semi-join 优化)
- 若子查询结果超 10k 行,改用
EXISTS避免 IN 的隐式转换开销:WHERE EXISTS (SELECT 1 FROM order_items oi WHERE oi.order_id = orders.id AND ...) - 注意 NULL:
IN遇到子查询返回 NULL 会整个条件失效,EXISTS没这个问题
多对多去重最易被忽略的点:不是语法能不能写出来,而是你到底要“去什么重”——是去 ID 重复?还是去业务实体重复?前者靠子查询裁剪主键即可,后者必须引入排序、窗口或聚合,且操作位置必须在最终结果生成前一层。










