三表JOIN必须通过中间表连接,避免漏数据或重复:主表→中间表→业务表;LEFT JOIN时过滤条件须放ON中;中间表需建联合索引;聚合合并用GROUP_CONCAT/STRING_AGG并注意长度与排序。

怎么写三表 JOIN 才不会漏数据或重复
多对多关系必须通过中间表连接,直接 JOIN 两张业务表会出错——要么笛卡尔爆炸,要么根本查不到关联记录。核心是把中间表当「桥梁」,而不是可选配件。
常见错误现象:SELECT * FROM user, role WHERE user.id = role.user_id —— 这根本不是多对多,是错把中间表逻辑硬塞进一对多写法里;或者漏掉中间表,JOIN 顺序错导致 LEFT JOIN 失效。
- 正确路径永远是:业务表 A → 中间表 → 业务表 B
- 如果要查「所有用户及其角色(含无角色者)」,必须以
user为主表,LEFT JOIN中间表,再LEFT JOINrole - 如果只想要「有角色的用户」,用
INNER JOIN更安全,避免 NULL 干扰后续聚合 - 中间表字段命名要统一,比如
user_id和role_id,别混用uid/rid,否则 JOIN 条件容易写错
GROUP_CONCAT 或 STRING_AGG 怎么合并多个关联值
查一个用户对应多个角色时,不合并就会出现重复行;但盲目用 GROUP BY user.id 又可能丢字段。关键看你要不要保留明细,还是只要汇总字符串。
MySQL 用 GROUP_CONCAT,PostgreSQL 用 STRING_AGG,语法和默认行为差异大,不注意会返回空或报错。
-
GROUP_CONCAT(role.name)默认用逗号分隔,长度上限是 1024 字符,超长会被截断,需提前设SET SESSION group_concat_max_len = 10000 -
STRING_AGG(role.name, ', ')不限制长度,但 PostgreSQL 9.6+ 才支持ORDER BY子句,旧版本得套子查询 - 如果中间表有额外字段(如
created_at),想按时间排序合并,MySQL 要写GROUP_CONCAT(role.name ORDER BY ur.created_at),其中ur是中间表别名 - 别在
SELECT里混用聚合和非聚合字段,除非全在GROUP BY里列出来,否则 MySQL 5.7+ 严格模式直接报错
WHERE 条件放 JOIN 后还是 ON 里,结果差很多
中间表参与连接时,过滤条件放错位置,会导致 LEFT JOIN 变成 INNER JOIN 效果,或者漏掉本该保留的主表记录。
典型翻车现场:LEFT JOIN user_role ON user.id = user_role.user_id WHERE user_role.role_id = 3 —— 这个 WHERE 实际把左连接“降级”了,NULL 的 role_id 全被干掉。
- 想查「所有用户中哪些拥有角色 3」,条件必须写在
ON里:LEFT JOIN user_role ON user.id = user_role.user_id AND user_role.role_id = 3 - 想查「角色 3 对应的所有用户」,那就用
INNER JOIN,条件放WHERE或ON都行 - 涉及多个中间表时(比如用户-角色-权限),每个
ON只管上一级连接,别把跨表条件塞进去,容易逻辑混乱 - 索引有效性也受此影响:放在
ON的条件能更好利用中间表联合索引,WHERE里的可能触发全表扫描
中间表没索引,JOIN 一慢就卡死
中间表看着小,但一旦用户量过万、角色过百,没索引的 JOIN 就是全表嵌套扫描,响应从毫秒变秒级。这不是优化问题,是基础配置缺失。
错误认知:“中间表就两个字段,还建啥索引?”——它恰恰是最需要复合索引的地方。
- 必须建联合索引:
CREATE INDEX idx_user_role ON user_role(user_id, role_id),顺序不能反,因为多数查询以用户为起点 - 如果常按角色反查用户,再加一个
INDEX idx_role_user ON user_role(role_id, user_id) - 外键约束不是索引替代品:即使声明了
FOREIGN KEY,MySQL 也不会自动建索引,PostgreSQL 会,但仅限单字段外键 - 上线前用
EXPLAIN看执行计划,重点确认type是ref或eq_ref,不是ALL或index
中间表字段类型要和关联主表完全一致,比如 user_id 是 BIGINT UNSIGNED,中间表也得是,否则索引失效,JOIN 变慢,还查不出数据。










