left join必须用on过滤右表条件,否则退化为inner join;多层权限继承需with recursive展开角色树;排除型校验用left join + is null;重复权限须用distinct或group by去重。

LEFT JOIN必须配合ON子句过滤,否则退化为INNER JOIN
权限校验中常见错误是把右表条件写在WHERE里,比如WHERE p.code = 'edit_post'。这会让LEFT JOIN permissions p实际变成内连接——所有没匹配到权限的用户行全被剔除,和“查所有用户及其编辑权限(含空)”的本意完全相反。
正确做法是把权限筛选下推到ON子句:LEFT JOIN permissions p ON r.role_id = p.role_id AND p.code = 'edit_post'。这样左表用户始终保留,没权限的p.*字段自然为NULL。
- 验证是否生效:执行
EXPLAIN,若Extra列出现Using where且涉及右表字段,说明已退化 - 多个权限条件要合并进同一个
ON,不要拆成多个WHERE或额外AND - 状态字段如
p.is_active = 1也必须放ON里,否则停用权限会污染结果
多层权限继承必须先用WITH RECURSIVE展开角色树
当角色A继承B、B继承C时,单纯两层LEFT JOIN只能拿到A直接关联的权限,B和C的权限不会自动透传。这是因为JOIN是静态关联,不支持递归查找。
真实权限校验必须分两步:先用WITH RECURSIVE算出用户最终拥有的全部角色ID集合,再拿这个集合去关联权限表。例如:
WITH RECURSIVE user_all_roles AS ( SELECT role_id FROM user_roles WHERE user_id = 123 UNION SELECT ri.child_role_id FROM role_inherit ri INNER JOIN user_all_roles uar ON ri.parent_role_id = uar.role_id ) SELECT DISTINCT u.user_id, p.code FROM users u LEFT JOIN user_all_roles ur ON u.user_id = 123 LEFT JOIN role_permissions rp ON rp.role_id = ur.role_id LEFT JOIN permissions p ON rp.permission_id = p.id;
- 递归CTE必须包含非递归种子(如
user_roles初始行)和递归部分(UNION后查询) - MySQL 5.7不支持
WITH RECURSIVE,需改用应用层循环或预计算路径字段(如role_path) - 避免在递归CTE里加
WHERE过滤,否则可能截断继承链
用LEFT JOIN + IS NULL实现“排除型权限校验”
有些场景不是“必须有某权限”,而是“不能有某禁止权限”,比如审计员可以看所有订单,但不能看VIP客户的退款明细。这时不能靠WHERE p.code != 'refund_vip'——它会漏掉根本没分配任何权限的用户。
正确逻辑是:先LEFT JOIN禁止权限表,再用WHERE p.id IS NULL筛出“未被赋予该禁止权限”的用户:
SELECT u.user_id, u.username FROM users u LEFT JOIN role_permissions rp ON u.user_id = rp.user_id LEFT JOIN permissions p ON rp.permission_id = p.id AND p.code = 'refund_vip' WHERE p.id IS NULL;
- 注意
AND p.code = 'refund_vip'必须在ON里,否则IS NULL判断失效 - 若禁止权限通过角色继承而来,同样要先用
WITH RECURSIVE展开角色树,再LEFT JOIN - 这种写法天然兼容“用户无任何权限”的情况,不需要额外
OR补空逻辑
DISTINCT或GROUP BY是去重刚需,别依赖前端
一个用户属于多个角色,而不同角色都配置了同一菜单或权限,直接LEFT JOIN会导致同一权限重复出现多行。比如用户同时有“管理员”和“编辑”两个角色,都绑定了edit_post,结果就会返回两条记录。
后端必须主动去重,否则传输冗余数据、分页错乱、前端JS去重成本高:
- 简单场景用
DISTINCT,但只选权限相关字段(如p.id,p.code),别带roles.name等易变字段 - 需要保留来源角色信息时,用
GROUP BY p.id+STRING_AGG(roles.name, ',')(PostgreSQL)或GROUP_CONCAT(roles.name)(MySQL) - 若角色有优先级,可用
ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY roles.priority DESC)取最高优角色
层级越深、角色越多,重复风险越高;一旦发现结果行数异常膨胀,第一反应就该检查是否漏了去重逻辑。










