LEFT JOIN 多对多必须通过中间表,如 users → user_roles → roles;直接连接 users 和 roles 会错配;ON 条件中右表过滤需用子查询或预处理,避免 LEFT JOIN 退化为 INNER JOIN。

LEFT JOIN 多对多必须经过中间表
直接 LEFT JOIN 两张业务表(比如 users 和 roles)永远得不到正确结果——它们之间没有外键直连,强行写 ON u.id = r.id 是错的。真实路径一定是:左表 → 中间表 → 右表。例如查用户及其角色,必须走 users → user_roles → roles 这条链。
常见错误现象:SELECT u.name, r.role_name FROM users u LEFT JOIN roles r ON u.id = r.id,结果要么全 NULL,要么乱匹配,因为没中间表兜底。
- 中间表字段必须明确参与两次
ON:第一次连左表主键,第二次连右表主键 - 三张表都建议加别名(
u、ur、r),避免列名冲突 - 中间表本身通常无业务意义,只存两个外键,不要在
SELECT里漏掉它
ON 条件里不能塞右表的业务过滤条件
想查“所有用户 + 他们启用的角色”,如果把状态判断写在 WHERE 或第二次 ON 的末尾(如 ON ur.role_id = r.id AND r.status = 'active'),会导致没启用角色的用户也被过滤掉——LEFT JOIN 退化成 INNER JOIN 效果。
正确做法是把业务过滤单独拎到 WHERE 后面,但只作用于左表;右表的过滤必须用子查询或先在中间表/右表上预处理。
- ❌ 错误:
LEFT JOIN roles r ON ur.role_id = r.id AND r.deleted = 0 - ✅ 正确:
LEFT JOIN (SELECT * FROM roles WHERE deleted = 0) r ON ur.role_id = r.id - 或者更稳妥:先
LEFT JOIN roles r,再在SELECT中用CASE WHEN r.deleted = 0 THEN r.role_name END控制显示逻辑
多对多下 LEFT JOIN 容易引发行数膨胀
一个用户有 3 个角色,用 LEFT JOIN 拉出来就是 3 行。这不是 bug,是多对多的天然行为。如果你要聚合展示(比如 “用户A:角色1, 角色2, 角色3”),不能靠多次 LEFT JOIN 解决,得用 GROUP_CONCAT(MySQL)或 STRING_AGG(PostgreSQL)配合 GROUP BY。
- 行数膨胀会拖慢查询,尤其左表数据量大时;确认是否真需要展开成多行,还是只需汇总值
- 若必须展开,记得在中间表上对
(user_id, role_id)建联合索引,否则JOIN性能骤降 -
SELECT DISTINCT不能解决根本问题,只是掩盖重复,且可能误删合法的多对多组合
查“有用户但没角色”的记录要小心 NULL 判断
用 LEFT JOIN 找未分配角色的用户,写法是:SELECT u.* FROM users u LEFT JOIN user_roles ur ON u.id = ur.user_id WHERE ur.user_id IS NULL。关键点在于:必须用中间表的字段判 NULL,而不是右表(roles)的字段。
因为中间表 user_roles 是连接枢纽,它不存在才代表“没角色”;而 roles 表字段为 NULL 可能只是中间表存在但右表被过滤掉了(比如 deleted = 1)。
- 务必确认中间表关联字段(如
ur.user_id)定义为NOT NULL,否则IS NULL判断不可靠 - 不要写成
WHERE r.id IS NULL,这会漏掉中间表有记录但角色被软删除的情况 - 这种查询适合加覆盖索引:
INDEX(user_id)在user_roles上
实际复杂度往往不在语法,而在你是否清楚“哪张表才是事实源头”——中间表的数据完整性、是否允许空值、有没有逻辑删除标记,这些细节一错,LEFT JOIN 就会 quietly 返回错误结果。

















