子查询必须返回与外层user_id类型严格匹配的单列(如bigint),否则隐式转换致索引失效;应避免in含null、慎用session变量、优先用exists关联下推,并确保每层嵌套均命中索引。

子查询返回单列且类型必须严格匹配
外层字段是 user_id(BIGINT),子查询却写成 SELECT username FROM allowed_users,数据库会隐式转换整列,导致索引失效、全表扫描。MySQL 在遇到类型不一致时,宁可放弃索引也不报错,只在 EXPLAIN 中暴露 type: ALL。
必须显式限定子查询只返回主键列,且类型对齐:
-
user_id IN (SELECT id FROM allowed_users WHERE role = 'sales')——id类型和外层user_id一致 - 若权限表用字符串 ID(如雪花 ID),子查询也得返回
VARCHAR,不能混用CAST(id AS CHAR)临时补救,应从建表就统一 - 别用
SELECT *或SELECT 1以外的常量——SELECT 'x'会被优化器误判为非确定性表达式
优先用 EXISTS 而不是 IN 做权限判断
IN 遇到子查询结果含 NULL 时,整行被丢弃,逻辑上等价于 WHERE FALSE,极易引发“查不到数据但没报错”的静默失败。而 EXISTS 天然规避该问题,且支持关联下推:
- 错误写法:
WHERE user_id IN (SELECT user_id FROM team_members WHERE team_id = ?)—— 若team_members.user_id允许 NULL,结果不可控 - 正确写法:
WHERE EXISTS (SELECT 1 FROM team_members WHERE team_members.user_id = users.id AND team_members.team_id = ?)—— 关联字段users.id直接下推,执行计划更优 - 递归权限场景(如部门树)别硬塞进
EXISTS,先用WITH RECURSIVE物化路径,再 JOIN,否则优化器可能放弃索引
子查询里不能依赖应用层 session 变量
像 current_user() 这类函数看似方便,但在连接池复用、长事务、读写分离等场景下极不稳定。PostgreSQL 的 current_setting('app.user_id') 或 MySQL 的 @user_id 需要每次查询前显式 SET,而 ORM 很少自动做这件事。
安全做法是把用户上下文作为参数传入:
- SQL 层面:用
WHERE EXISTS (SELECT 1 FROM access_policy p WHERE p.user_id = ? AND p.resource_id = users.id) - 应用层:确保所有 DAO 方法都显式接收
currentUserId参数,不从 ThreadLocal 或 static 变量取值 - MyBatis 场景下,避免在
<select></select>标签里调用${session.userId},改用#{userId}绑定参数
嵌套深度超 2 层时务必检查执行计划
三层子查询(如用户→团队→部门→上级部门)叠加后,MySQL 8.0+ 可能退化为物化临时表,PostgreSQL 则倾向走 Nested Loop,性能断崖式下跌。别只看语义对不对,重点看 EXPLAIN ANALYZE 输出中是否有 Merge Join 或 Materialize 步骤。
简单验证方式:
- 执行
EXPLAIN FORMAT=JSON,检查"query_block": {"table": {"access_type": "ref"}}是否存在;若出现"access_type": "ALL",说明某层子查询没走索引 -
sales_teams.id、team_members.team_id、team_members.user_id这三列必须都有单独索引或联合索引 - 如果权限规则本身带时间条件(如“仅限近 7 天生效的权限”),记得给
valid_from和valid_to加范围索引











