LEFT JOIN 行数暴增是正常行为,因左表一行匹配右表N行即生成N行;DISTINCT无法真正解决,因其按整行去重而非单列;应JOIN前用子查询聚合或窗口函数控制右表粒度。

LEFT JOIN 后行数暴增是正常行为,不是 bug
数据库严格按标准执行 JOIN:左表一行匹配右表 N 行,就生成 N 行结果。比如 users 表 100 行,t_log 表中某个 user_id 出现了 7 次,那这条用户数据在结果里就占 7 行。这不是 SQL 写错了,而是 JOIN 的本质行为。
常见误判现象包括:
- COUNT(*) 统计结果远大于左表原始行数,误以为数据污染
- 后续再 JOIN 第三张表,拿这个已膨胀的结果当主表,错误级联放大
- 看到重复就加
DISTINCT,但右表字段(如log_time、ip_addr)只要有一列不同,整行就不被合并
为什么 DISTINCT 不能真正解决问题
DISTINCT 是对整行所有字段组合去重,不是按 user_id 单列“智能去重”。只要任意一列值不同——哪怕只是毫秒级时间戳差异——就被视为不同行。
典型失效场景:
-
SELECT DISTINCT u.id, u.name FROM users u LEFT JOIN t_log l ON u.id = l.user_id看似去重,但一旦加上l.log_time,重复立刻回来 -
DISTINCT在某些引擎中会触发隐式排序,大表查询明显变慢 - 用它掩盖了一对多未收敛的问题,后续聚合或导出仍可能出错
真正有效的解法:JOIN 前控制右表粒度
核心思路是让右表在 JOIN 前只输出你需要的那一行,而不是在结果上“打补丁”。选哪种方式,取决于你要什么:
- 要汇总值(如登录次数、最新 IP):用子查询 +
GROUP BY预聚合SELECT u.id, u.name, p.cnt, p.max_ip FROM users u LEFT JOIN (SELECT user_id, COUNT(*) AS cnt, MAX(ip_addr) AS max_ip FROM t_log GROUP BY user_id) p ON p.user_id = u.id - 要完整单条明细(如最新一条日志):用
ROW_NUMBER()窗口函数,且rn = 1必须写在ON条件里SELECT u.id, u.name, p.ip_addr, p.log_time FROM users u LEFT JOIN (SELECT user_id, ip_addr, log_time, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY log_time DESC, id DESC) AS rn FROM t_log) p ON p.user_id = u.id AND p.rn = 1 - 只判断“有没有关联记录”:改用
EXISTS,逻辑清晰、无重复、性能更好SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM t_log l WHERE l.user_id = u.id)
COUNT(*) 或 SUM() 统计失真时怎么修
JOIN 后直接用 COUNT(*) 或 SUM(amount),统计的是物理行数,不是业务逻辑行。一对多关系下,1 个用户 × 3 笔订单 → 3 行参与计算,COUNT(*) 返回 3,但你想知道的可能是“有多少用户”。
实操建议:
- 统计主表数量:用
COUNT(DISTINCT main_table.id),别用COUNT(*) - 汇总从表数值:先对从表按关联字段
GROUP BY聚合(如SUM(amount) AS total_amount),再 JOIN 到主表 - 用
COUNT(o.id)替代COUNT(*):因为主键天然非空,COUNT(o.id)准确反映“主表每条记录匹配了多少从表有效行”
最容易被忽略的一点:JOIN 后的结果集,已经不是原始左表的行集合了。哪怕你只 SELECT 左表字段,中间的膨胀过程依然发生,内存和 CPU 开销照旧。

















