DISTINCT 去重的是整行而非单列,JOIN 一对多时因关联字段差异导致“看似重复实则不同”;应优先用 EXISTS/IN、GROUP BY 或 ROW_NUMBER() 精准控制去重逻辑。

为什么 DISTINCT 加了还是有重复?
不是 DISTINCT 不管用,而是它去重的是整行结果——只要任意一列值不同,就算“不重复”。多表 JOIN 后,一对多关系(比如一个订单对应多个商品)天然会把主表记录“撑开”,此时哪怕你只选主表字段,数据库仍按连接后的中间结果去重,ORDER_ID 看似一样,但因关联的 ITEM_ID、QUANTITY 等字段参与了 JOIN,整行实际不同。
实操建议:
- 先确认重复根源:用
SELECT *查看原始 JOIN 结果,观察哪几列在变 - 如果只需要主表数据,优先考虑
EXISTS或IN替代JOIN,避免物理拼接 -
DISTINCT必须紧贴SELECT,不能写成SELECT DISTINCT a.* FROM ...(MySQL 8.0+ 支持,但多数旧版本报错)
用 GROUP BY 替代 DISTINCT 更可控
GROUP BY 明确指定按哪些列聚合,比 DISTINCT 更易预测行为,也方便后续加聚合函数(如统计子项数量)。但它要求所有 SELECT 中的非分组字段必须用聚合函数包裹,否则报错(SQL 标准严格模式下)。
常见错误现象:SELECT order_id, customer_name, COUNT(*) FROM orders o JOIN items i ON o.id = i.order_id GROUP BY order_id —— customer_name 未出现在 GROUP BY 且没聚合,MySQL 5.7+ 默认拒绝执行。
实操建议:
- 只选主表关键字段时,
GROUP BY列和SELECT列保持一致,例如SELECT o.id, o.status FROM orders o JOIN ... GROUP BY o.id, o.status - 需要附带子表信息(如最新一条日志),改用窗口函数
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY created_at DESC)过滤 - 注意
GROUP BY性能:字段越多、数据量越大,临时表和排序开销越高
LEFT JOIN + IS NULL 检查缺失关联时别误伤主表
想查“没关联到子表的主表记录”,常写 LEFT JOIN ... WHERE sub.id IS NULL。但如果子表有多条匹配记录,LEFT JOIN 仍会产生多行,再用 WHERE 过滤时逻辑没问题,但容易忽略:若子表本就允许空值或存在重复键,IS NULL 可能匹配出意料之外的行。
使用场景:排查订单无支付记录、用户无头像等“缺失”类问题。
实操建议:
- 确保子表关联字段有唯一约束(如
payment.order_id应为外键+唯一索引),否则LEFT JOIN天然导致主表膨胀 - 避免在
ON条件里写AND sub.status = 'success'后又用WHERE sub.id IS NULL——这会把 status 不是 success 的也当“缺失”,应改用子查询或CASE WHEN - 测试时加
LIMIT 10并SELECT *看原始连接结果,比直接信COUNT(*)更可靠
MySQL 8.0+ 用 ROW_NUMBER() 精确取每组第一条
当必须保留主表一行、又得带某条子表记录(如最新创建时间那条),DISTINCT 和 GROUP BY 都难满足需求。ROW_NUMBER() 是更干净的解法,但需注意它不改变物理连接数,只是标记序号后过滤。
性能影响:窗口函数需缓存分区数据,大数据量时内存占用明显高于普通 JOIN。
实操示例:
SELECT order_id, item_name, price
FROM (
SELECT
o.id AS order_id,
i.name AS item_name,
i.price,
ROW_NUMBER() OVER (PARTITION BY o.id ORDER BY i.created_at DESC) AS rn
FROM orders o
LEFT JOIN items i ON o.id = i.order_id
) ranked
WHERE rn = 1;要点:
-
PARTITION BY字段必须是主表唯一标识,否则分组逻辑错乱 -
ORDER BY尽量用有索引的字段,否则排序成本高 - 低版本 MySQL(
实际写多表查重,最麻烦的往往不是语法,而是搞不清“到底要哪一条”——是任意一条?最新的?数量最多的?这个业务语义一旦模糊,后面所有优化都可能南辕北辙。

















