子查询本身不提供权限控制能力,仅是sql语法结构;所谓“通过子查询实现权限控制”,本质是用其动态生成where过滤条件,将权限逻辑下沉至sql层,但易因类型不匹配、null处理不当或索引缺失导致权限绕过或性能崩盘。

子查询本身不提供权限控制能力,它只是SQL语法结构;所谓“通过子查询实现权限控制”,本质是用子查询动态生成过滤条件,把权限逻辑下沉到SQL层——这可行,但极易因类型不匹配、NULL处理或索引缺失导致权限绕过或性能崩盘。
WHERE 中用 IN 还是 EXISTS?选错就丢数据
用 user_id IN (SELECT id FROM allowed_users WHERE role = ?) 看似直观,但一旦子查询返回 NULL(比如某条记录的 id 是 NULL),整行就会被过滤掉,用户可能查不到本该看到的数据。而 EXISTS 不受 NULL 影响,且数据库能下推关联字段、提前终止扫描。
- 优先写成
EXISTS (SELECT 1 FROM allowed_users au WHERE au.id = u.user_id AND au.role = ?) -
SELECT 1比SELECT *或SELECT id更轻量,优化器也更易识别语义 - 如果
allowed_users表没有(id, role)复合索引,EXISTS也会退化为全表扫描
类型隐式转换让索引彻底失效
MySQL 对 IN 右侧子查询的字段类型极其敏感。外层字段是 BIGINT(如雪花 ID),子查询却返回 VARCHAR,就会触发全表隐式转换——执行计划里 type 显示 ALL,而不是 ref 或 eq_ref。
- 检查执行计划:
EXPLAIN SELECT * FROM users u WHERE u.id IN (SELECT user_id FROM permissions) - 确保子查询字段类型与外层完全一致,必要时显式
CAST(user_id AS BIGINT) - 避免在子查询中对字段做函数操作,比如
UPPER(email),会直接让索引失效
多级权限继承必须用递归 CTE,别硬塞进 EXISTS
当权限规则涉及“部门→子部门→岗位”这类树形继承时,用普通子查询嵌套 JOIN 容易漏父节点或重复扫描。递归 CTE 能一次性拉出所有可达节点,但不能直接塞进 EXISTS 的子句里——多数数据库不支持在 EXISTS 内部使用递归定义。
- 先用
WITH RECURSIVE dept_tree AS (...)物化权限路径 - 再用
EXISTS (SELECT 1 FROM dept_tree dt WHERE dt.id = u.dept_id)做最终判断 - PostgreSQL 支持
MATERIALIZED提示,MySQL 8.0+ 可用WITH+TEMPORARY TABLE替代
最危险的不是写不出子查询,而是没验证它在 NULL、类型边界、空结果集、大数据量下的行为——这些地方一松动,权限就漏了。










