权限判断应优先用exists而非in,避免null导致数据丢失;角色继承需用with recursive展开;位运算判断必须移至外层;grouping sets前须用子查询清洗维度并处理null。

WHERE里用IN还是EXISTS做权限判断
直接用IN查权限,大概率漏数据——只要子查询结果里有NULL,整行就静默丢弃。比如user_id IN (SELECT user_id FROM team_members WHERE team_id = 101),若team_members.user_id允许为空,哪怕有99个有效ID,结果也可能为空。
必须改用EXISTS,并显式关联外层字段:EXISTS (SELECT 1 FROM team_members WHERE team_members.user_id = users.id AND team_members.team_id = 101)。这样既规避NULL陷阱,又能让优化器下推users.id条件,索引更可能命中。
-
SELECT *或SELECT 'x'在子查询里不如SELECT 1稳妥,后者明确告诉优化器“只关心是否存在” - 子查询字段类型必须和外层严格一致:外层是
BIGINT,子查询就不能返回VARCHARID,否则隐式转换会让索引失效 - 别依赖
current_user()这类会话变量,连接池复用时容易串用户
多层角色继承必须先展开再过滤
权限不是扁平的,“角色A继承B,B继承C”,硬塞进EXISTS子查询里,数据库没法递归执行。你写的EXISTS (SELECT ... WHERE role_id IN (?, ?, ?))只覆盖直接分配的角色,漏掉继承链上的所有权限。
正确做法是用WITH RECURSIVE先算出用户最终拥有的全部角色ID,再拿这个集合去匹配权限表。PostgreSQL示例:
WITH RECURSIVE user_roles AS (
SELECT role_id FROM role_user WHERE user_id = 123
UNION
SELECT ri.child_id FROM role_inherit ri
INNER JOIN user_roles ur ON ri.parent_id = ur.role_id
)
SELECT r.* FROM resource r
JOIN role_permission rp ON rp.resource_id = r.id
JOIN user_roles ur ON ur.role_id = rp.role_id
WHERE rp.permission_code = 'edit_article';
- 递归CTE必须有非递归部分(第一行
SELECT)和递归部分(UNION后),缺一不可 - PostgreSQL默认递归深度100,长链要加
SEARCH DEPTH FIRST BY role_id SET ordercol并配合WHERE ordercol - 必须加
DISTINCT或GROUP BY,继承可能导致同一资源被多次匹配
位运算权限掩码不能塞进子查询WHERE
想在子查询里写(mask & 4) = 4筛选有删除权限的角色?语法上多数数据库直接报错,MySQL报Invalid use of group function,SQL Server卡在类型转换失败。根本原因是子查询的WHERE作用域看不到外层字段,也没法对聚合结果做位运算。
唯一安全姿势:子查询只负责取原始整数字段(如role_mask),位判断一律挪到外层WHERE或ON中。例如:
SELECT u.* FROM users u INNER JOIN ( SELECT user_id, role_mask FROM user_roles WHERE role_type = 'admin' ) r ON u.id = r.user_id WHERE (r.role_mask & 4) = 4;
- 子查询输出
role_mask必须是未加工的整数,不能是BIT_AND(mask)之类聚合结果 - 若需同时满足多个位(如读+写),别堆在
WHERE (mask & 3) = 3里,优先拆成多个EXISTS或用JOIN组合 - 位运算字段加索引基本无效,但至少保证语法合法、不触发隐式转换
GROUPING SETS需要子查询预处理维度
GROUPING SETS本身不支持嵌套,比如GROUPING SETS ((a), (b, c))合法,但GROUPING SETS ((a), (GROUPING SETS (b, c)))直接语法错误。你想按“地区+产品线(已合并为mobile/desktop)+季度”做小计,就得先在子查询里把原始产品分类映射好。
子查询负责清洗:时间截断、维度归并、空值填充;外层再用GROUPING SETS汇总。关键点是原始NULL必须处理掉,否则GROUPING()函数会误判:
SELECT
COALESCE(region, '[Unknown]') AS region,
product_group,
DATE_TRUNC('quarter', order_date) AS quarter,
SUM(amount)
FROM (
SELECT
region,
CASE
WHEN product IN ('laptop', 'tablet') THEN 'mobile'
ELSE 'desktop'
END AS product_group,
order_date,
amount
FROM orders
) AS t
GROUP BY GROUPING SETS (
(region, product_group, quarter),
(region, product_group),
(region),
()
);
- 子查询必须带别名(如
AS t),否则多数数据库报错 -
COALESCE或CASE必须出现在子查询里,不能留到外层——原始NULL会污染GROUPING()判断 - 子查询若含JOIN或窗口函数,建议用
WITH MATERIALIZED(PostgreSQL 12+)避免重复计算
复杂点在于权限逻辑从来不是单一层级的:位掩码、角色继承、维度聚合各自有约束边界,强行揉进一个子查询只会让语义混乱、性能崩坏、调试困难。把每层职责切干净——子查询只取数,外层负责判断;递归只展开,不参与过滤;预处理只清洗,不汇总——才能稳住执行计划。











