三表join必须通过中间表按“主表→中间表→业务表”路径关联,否则逻辑错误;中间表需建联合索引、字段类型与主表外键严格一致,并正确使用on与where过滤条件。

必须用中间表,跳过它或写错路径,结果不是空就是爆炸式重复——这不是性能问题,是逻辑错误。
三表 JOIN 必须走「主表 → 中间表 → 业务表」路径
user 和 role 表之间没有外键,强行 JOIN user u ON u.id = r.user_id 会报错(r.user_id 字段根本不存在);换成 WHERE u.id = r.id 则触发笛卡尔积,100 个用户 × 50 个角色 = 5000 行无意义数据。
- 查「所有用户及其角色(含无角色者)」:以
user为主表,LEFT JOIN user_role ON u.id = ur.user_id,再LEFT JOIN role ON ur.role_id = r.id - 查「角色 3 对应的所有用户」:用
INNER JOIN更安全,避免 NULL 干扰后续聚合 - 中间表字段名必须统一,比如固定用
user_id和role_id,别混用uid/rid,否则ON条件一写就失效 - 别在 SQL 里硬凑 JSON 或逗号分隔字符串模拟多对多——
LIKE '%3%'全表扫描、无法加索引、更新不原子
LEFT JOIN 时 WHERE 和 ON 的位置决定是否丢数据
把过滤条件放错地方,LEFT JOIN 就等于白写。典型翻车:LEFT JOIN user_role ur ON u.id = ur.user_id WHERE ur.role_id = 3 —— 这个 WHERE 实际把左连接“降级”了,所有没角色的用户全被干掉。
- 想保留主表全部记录,只让右表匹配特定值:条件必须进
ON,例如LEFT JOIN user_role ur ON u.id = ur.user_id AND ur.role_id = 3 - 想先完整关联,再筛结果(比如排除某些状态):才用
WHERE,但得确认不误筛左表字段 - MySQL 允许在
ON里写ur.role_id = 3,但旧版 PostgreSQL 可能不支持非等值判断,得查文档
合并多个关联值要用 GROUP_CONCAT 或 STRING_AGG,不是 DISTINCT
一个用户有 3 个角色,JOIN 后自然出 3 行——这不是 bug,是关系代数的必然结果。盲目加 DISTINCT 只压平行,不解决语义歧义,还可能掩盖真实数据问题。
- MySQL 用
GROUP_CONCAT(r.name ORDER BY r.name),默认长度上限 1024,超长会被截断,需提前设SET SESSION group_concat_max_len = 1000000 - PostgreSQL 用
STRING_AGG(r.name, ', '),9.6+ 才支持ORDER BY子句,旧版本得套子查询 - 必须配
GROUP BY u.id, u.name,否则 MySQL 5.7+ 严格模式直接报错 - 中间表有额外字段(如
created_at)想按时间排序合并:MySQL 写GROUP_CONCAT(r.name ORDER BY ur.created_at),其中ur是中间表别名
中间表没联合索引,JOIN 性能直接掉坑里
多对多查询慢,90% 是因为 user_role 这类中间表缺联合索引。单列索引(比如只给 user_id 加)几乎无效——它既被 user_id 驱动,也被 role_id 驱动。
- 必须建联合索引:
CREATE INDEX idx_user_role_user_id ON user_role(user_id, role_id)(按常用 JOIN 方向排) - 如果常反向查「某个角色下所有用户」,再补一个:
CREATE INDEX idx_user_role_role_id ON user_role(role_id, user_id) - 外键约束不光保一致性,还能帮优化器更好估算执行计划,别省这一步
- 用
EXPLAIN看type字段,如果出现ALL或Using join buffer (Block Nested Loop),基本就是索引没打在关键路径上
最容易被忽略的是:中间表字段类型要和主表外键严格一致(比如都是 INT,不是一边 INT 一边 VARCHAR),否则即使有索引也可能无法使用——这种问题不会报错,但查询会突然变慢,且很难定位。











