EXISTS比IN更可靠,因IN遇空子查询或NULL值时整行被丢弃,而EXISTS只判断存在性、不受NULL影响且支持关联下推;但需确保子查询显式关联、字段类型一致并走索引。

WHERE里用EXISTS还是IN做权限判断
直接用IN做权限过滤,大概率查不到数据——不是逻辑写错了,而是IN遇到空子查询或NULL值时返回FALSE,不是UNKNOWN,整行就被无声丢弃。比如:WHERE user_id IN (SELECT user_id FROM team_members WHERE team_id = ?),只要子查询没结果,哪怕用户本该有权限,也查不到任何行。
改用EXISTS更可靠:EXISTS只判断是否存在匹配行,不依赖值比较,天然避开NULL陷阱,且支持关联下推,让优化器能把外层字段(如users.id)直接传进子查询条件里,更容易走索引。
- 必须显式关联:子查询里要写
team_members.user_id = users.id,否则变成非相关子查询,可能误判或全表扫 - 子查询里只写
SELECT 1,别用SELECT *或SELECT 'x',语义清晰、优化器友好 - 如果权限表很小(比如
team_members只有几百行),优先考虑把EXISTS改写成JOIN,避免相关子查询被反复执行
子查询字段类型不匹配导致索引失效
外层字段是BIGINT(比如雪花ID),子查询却返回VARCHAR,MySQL或PostgreSQL不会报错,但会触发隐式转换,导致索引完全失效,EXPLAIN里看到type: ALL就是这个原因。
典型错误:user_id IN (SELECT user_code FROM permissions),其中user_code是字符串型ID。数据库会把整列user_code转成数字去比对,没法用索引。
- 建表时就统一ID类型:如果主键用
BIGINT,权限表的外键也必须是BIGINT - 实在无法改表,子查询里用
CAST(user_code AS SIGNED)强制转换,但这是补救,不是方案 - 检查执行计划:
EXPLAIN看子查询部分是否用了ref或eq_ref,而不是ALL或index
多级部门/角色继承必须先展开再过滤
权限不是扁平的,“管理员→编辑员→查看员”这种继承链,硬塞进EXISTS子查询里,数据库没法递归执行。你写的role_id IN (1,2,3)只覆盖直接分配的角色,漏掉所有继承来的权限。
正确做法是用WITH RECURSIVE先算出用户最终拥有的全部角色或部门ID集合,再拿这个结果集去匹配资源。PostgreSQL和MySQL 8.0+都支持,SQL Server用UNION ALL模拟。
- 递归CTE必须有两部分:非递归锚点(如
SELECT role_id FROM role_user WHERE user_id = ?)和递归成员(UNION ALL SELECT ... FROM role_inherit JOIN user_roles ...) - 加
LEVEL或类似字段限制递归深度,防止死循环;PostgreSQL默认上限100,长链要显式控制 - 最终主查询别直接套在
EXISTS里,先物化路径,再JOIN,否则优化器可能放弃索引
别依赖数据库会话变量传用户上下文
CURRENT_USER()、@user_id、current_setting('app.user_id')看着方便,但在连接池复用、读写分离、长事务场景下极不稳定。ORM(如MyBatis)通常不会自动帮你SET这些变量,线程间容易串数据。
安全做法是把用户标识作为参数显式传入SQL:EXISTS (SELECT 1 FROM access_policy p WHERE p.user_id = ? AND p.resource_id = r.id),应用层确保每个DAO方法都接收currentUserId参数。
- MyBatis里禁用
${session.userId},改用#{userId}绑定参数 - 若用视图封装权限逻辑,视图里也不能调用会话函数,得靠外部传参(如通过
WHERE dept_id = ?) - SqlSugar或MyBatis-Plus拦截器虽能自动注入,但底层仍是把用户ID作为参数拼进SQL,不是靠会话变量
EXISTS子查询就跑10万次。性能瓶颈往往不在SQL写法多炫酷,而在有没有让每一轮子查询都命中索引。

















