文章目录
- 每日一句正能量
- 1. 背景与问题:把子查询改成JOIN,可能快了,也可能把权限结果改错了
- 2. 环境与数据:用真实的一对多权限模型验证,而不是拿唯一键做演示
- 2.1 EXISTS 的本质是 Semi Join 语义
- 2.2 普通 JOIN 是行组合语义
- 2.3 IN 与 EXISTS 也不能只看写法
- 2.4 NOT IN 是更危险的语义边界
- 3. 复现过程:权限 EXISTS 为什么会从2秒退化到40秒?
- 3.1 原始相关 EXISTS
- 3.2 第一步仍然是 ANALYZE
- 3.3 真正问题:权限表查找没有复合索引
- 4. 方案实施:五类查询改写必须逐一证明语义
- 4.1 EXISTS → JOIN:只有右侧唯一时才可直接等价
- 4.2 JOIN + DISTINCT 能恢复结果,但不一定更快
- 4.3 EXISTS 和 JOIN 可能产生同类底层计划
- 4.4 NOT EXISTS 通常比手工 LEFT JOIN ... IS NULL 更直接
- 4.5 NOT IN → NOT EXISTS 前先处理 NULL
- 4.6 权限过滤索引应围绕“谁查什么”设计
- 4.7 热点用户必须单独测
- 4.8 权限表的一对多是正常业务,不要为了JOIN强行加唯一约束
- 4.9 如果确实要JOIN,可以先去重权限键
- 4.10 大权限集合可以先预聚合
- 4.11 相关标量子查询比EXISTS更值得警惕
- 4.12 OR 里的相关子查询更容易阻碍去相关
- 5. 结果对比:性能最快的E3为什么反而不能上线?
- E0:相关 EXISTS + 无合适索引
- E1:ANALYZE
- E2:EXISTS + 权限复合索引
- E3:直接改普通 JOIN
- E4:JOIN + DISTINCT
- E5:反向过滤使用 NOT EXISTS
- 5.1 汇总
- 5.2 结果校验不能只比较COUNT
- 5.3 权限SQL还必须做“泄漏校验”
- 5.4 Buffer Read 是判断是否真的减少工作的关键
- 5.5 新索引也要验写成本
- 6. 风险与复盘:子查询改JOIN最危险的是“语法更简单,语义更复杂”
- 6.1 风险一:一对多重复
- 6.2 风险二:NOT IN 的 NULL
- 6.3 风险三:优化器本来已经做了Semi Join
- 6.4 风险四:相关子查询loops被忽略
- 6.5 风险五:JOIN + DISTINCT制造隐藏Sort/Hash
- 6.6 风险六:权限索引导致写放大
- 6.7 风险七:热点用户导致计划参数敏感
- 推荐的等价性验证流程
- 推荐性能诊断顺序
- 回退方案
- 最终复盘
- 附录 A:权限 EXISTS
- 附录 B:检查JOIN重复
- 附录 C:执行计划
- 附录 D:最低验收门禁
每日一句正能量
人生就是一场旅行,不在乎目的地,在乎的应该是沿途的风景以及看风景的心情。
重要的不是你获得了什么头衔,而是你如何感受每一天的朝阳与晚风,如何与同行者分享那些瞬间。
主题:查询改写 / 子查询与JOIN / 权限过滤
重点:EXISTS、IN、NOT EXISTS、NOT IN、Semi Join、Anti Join、重复行、NULL语义、执行计划、索引与并发验证
适用场景:KingbaseES 上的行级权限过滤、租户隔离、组织权限、订单可见性、白名单/黑名单、数据授权等复杂查询场景。
1. 背景与问题:把子查询改成JOIN,可能快了,也可能把权限结果改错了
数据库优化里流传很广的一句话是:
子查询慢,改 JOIN 就快。这句话最大的问题,不是“有时不快”。
而是:
有些子查询和 JOIN 在业务语义上根本不等价。
权限过滤就是最容易踩坑的场景。
例如:
SELECTo.order_id,o.amountFROMbiz_order oWHEREEXISTS(SELECT1FROMuser_order_permission pWHEREp.user_id=:user_idANDp.tenant_id=o.tenant_idANDp.order_id=o.order_id);这段 SQL 的业务含义非常清楚:
只要当前用户至少拥有一条匹配权限 就返回这条订单。这里的核心不是“把权限表连进来”。
而是:
Existence即“是否存在”。
如果开发直接改成:
SELECTo.order_id,o.amountFROMbiz_order oJOINuser_order_permission pONp.tenant_id=o.tenant_idANDp.order_id=o.order_idWHEREp.user_id=:user_id;假设一个用户对同一订单同时拥有:
角色权限 组织权限 项目权限 临时授权 区域授权五条记录。
那么:
EXISTS返回:
1条订单而普通JOIN可能返回:
5条订单性能可能真的更快。
但结果已经错了。
这类优化事故最危险的地方就在于:
SQL执行成功 没有报错 延迟下降 业务还能跑只有报表金额、分页、订单数量或下游接口悄悄出现重复。
所以本文第一个结论是:
查询改写必须先验证等价语义,再比较执行计划。性能测试排在第二位。
KingbaseES 官方文档对连接执行有 Semi Join、Anti Join 等概念;而执行计划分析也强调,最终连接方式由优化器根据统计信息和代价进行选择。很多EXISTS、IN形式本身就有机会被优化器解除子查询结构,转成半连接执行,因此“SQL 文本里看到子查询”并不等于“数据库一定逐行执行子查询”。
2. 环境与数据:用真实的一对多权限模型验证,而不是拿唯一键做演示
示例环境:
数据库: KingbaseES V9 订单: biz_order 订单量: 8000万 权限: user_order_permission 权限记录: 2.4亿 租户: 3000+ 用户: 120万权限表:
CREATETABLEuser_order_permission(user_idBIGINTNOTNULL,tenant_idBIGINTNOTNULL,order_idBIGINTNOTNULL,role_codeVARCHAR(32),org_idBIGINT,valid_flagINTNOTNULLDEFAULT1);这里故意不设置:
(user_id, tenant_id, order_id)唯一。
因为真实权限系统通常允许:
同一个人 通过多个授权来源 看到同一数据这恰恰是判断EXISTS与普通JOIN是否等价的关键。
2.1 EXISTS 的本质是 Semi Join 语义
EXISTS只回答:
有没有找到第一条满足条件的权限以后,逻辑上就不需要把其余匹配权限投影到结果中。
因此它对应的是:
Semi Join思想:
左侧业务行 只保留一次2.2 普通 JOIN 是行组合语义
JOIN 做的是:
左行 × 所有匹配右行如果:
1条订单 匹配5条权限结果就是:
最多5个组合所以两者只有在可以证明:
右表连接键唯一或者:
业务允许重复时才能直接说语义等价。
2.3 IN 与 EXISTS 也不能只看写法
例如:
WHEREorder_idIN(SELECTorder_idFROMuser_order_permissionWHEREuser_id=:uid)在很多等值场景中,优化器有机会转换成:
Semi Join所以:
IN一定慢 EXISTS一定快同样不是可靠结论。
最终还是:
EXPLAIN ANALYZE说话。
2.4 NOT IN 是更危险的语义边界
例如:
WHEREorder_idNOTIN(SELECTorder_idFROMblacklist)如果子查询中存在:
NULLSQL 三值逻辑会让比较产生:
UNKNOWN结果可能与开发直觉完全不同。
KingbaseES SQL 调优指南也专门给出NOT IN → NOT EXISTS的优化建议,但明确附带条件:
连接列不存在NULL值时两者才可按该规则视为等价,并可能利用不同 Anti Join 方式优化。
所以:
NOT IN 改 NOT EXISTS 前必须先证明 NULL 条件,而不是只为了拿到 Hash Anti Join。
3. 复现过程:权限 EXISTS 为什么会从2秒退化到40秒?
3.1 原始相关 EXISTS
SELECTo.order_id,o.amountFROMbiz_order oWHEREo.tenant_id=:tenant_idANDo.status=1ANDEXISTS(SELECT1FROMuser_order_permission pWHEREp.user_id=:user_idANDp.tenant_id=o.tenant_idANDp.order_id=o.order_idANDp.valid_flag=1);如果权限表没有合适索引,执行计划可能出现:
Outer: 大量订单 Inner/SubPlan: 权限表反复扫描例如:
Outer actual rows: 800万 Inner loops: 800万这时确实可能非常慢。
但慢的根因并不是:
EXISTS三个字而是:
相关执行 × 大量Outer × Inner缺乏低成本访问路径3.2 第一步仍然是 ANALYZE
KingbaseES 官方执行计划分析流程明确建议:
先检查估算是否准确 不准确则ANALYZE 再重新查看计划执行:
ANALYZEbiz_order;ANALYZEuser_order_permission;假设 P95:
42s →31s说明统计修复有效。
但仍然很慢。
3.3 真正问题:权限表查找没有复合索引
每次权限判断条件:
user_id tenant_id order_id valid_flag如果只有:
order_id单列索引,仍可能访问大量无关权限。
建立候选:
CREATEINDEXidx_perm_user_tenant_order_validONuser_order_permission(user_id,tenant_id,order_id,valid_flag);重新 ANALYZE。
计划可能变成:
Nested Loop Semi Join 或 有效的Semi Join路径 + Inner Index ScanP95:
31s →2.4s这说明:
保留 EXISTS 语义,也完全可能把性能问题解决。
没有必要为了速度先改 JOIN。
4. 方案实施:五类查询改写必须逐一证明语义
4.1 EXISTS → JOIN:只有右侧唯一时才可直接等价
原:
WHEREEXISTS(SELECT1FROMpermission pWHEREp.order_id=o.order_id)改:
JOINpermission pONp.order_id=o.order_id必须先证明:
SELECTorder_id,COUNT(*)FROMpermissionGROUPBYorder_idHAVINGCOUNT(*)>1;结果:
0如果不是 0:
不能直接改4.2 JOIN + DISTINCT 能恢复结果,但不一定更快
为了修重复,很多人继续:
SELECTDISTINCTo.*FROMorderoJOINpermission p...语义可能恢复到:
每订单一行但执行计划可能增加:
Sort Unique HashAggregate也就是说:
先把1行放大成5行 再花资源去重成1行从执行模型上并不漂亮。
如果业务本质就是:
是否存在权限直接EXISTS更能表达意图。
4.3 EXISTS 和 JOIN 可能产生同类底层计划
现代 CBO 并不是逐字翻译 SQL。
如果条件可去相关,EXISTS可以被转换成:
Semi Join而不是真的:
外表每一行执行一次独立SQL所以优化时一定要区分:
SQL语法形态和:
物理执行计划KingbaseES 官方查询与子查询文档、连接计划文档都表明,查询优化器会根据条件转换和代价选择 Merge、Hash、Nested Loop,以及 Semi/Anti 等连接形式。
因此:
如果 EXISTS 已经被优化成优良的 Semi Join,再人工改成 JOIN 很可能没有收益。
4.4 NOT EXISTS 通常比手工 LEFT JOIN … IS NULL 更直接
反权限场景:
找出用户无权访问的记录可以:
WHERENOTEXISTS(...)也有人写:
LEFTJOINpermission p...WHEREp.order_idISNULL在条件满足时,两者可能被优化器转换到类似 Anti Join。
所以也不能说:
LEFT JOIN一定更快真正对比:
Hash Anti Join Nested Loop Anti Merge Anti以及:
实际扫描量4.5 NOT IN → NOT EXISTS 前先处理 NULL
官方 SQL 调优指南明确指出:
连接列不存在NULL时可以把NOT IN转成NOT EXISTS,并可能通过 Hash Anti Join 获得更优执行。
因此生产改写流程必须:
1. 查看字段NOT NULL约束 2. 检查历史真实NULL 3. 确认业务是否应忽略NULL 4. 再进行改写而不是:
Advisor建议了 就直接批量替换4.6 权限过滤索引应围绕“谁查什么”设计
权限 EXISTS 常见条件:
user_id tenant_id resource_id valid_flag索引:
(user_id, tenant_id, resource_id, valid_flag)很自然。
但如果系统查询模式是:
先按tenant+resource查可见用户索引顺序就可能不同。
所以要根据:
真实查询入口 选择性 热点用户 租户规模设计。
4.7 热点用户必须单独测
普通用户:
100条权限管理员:
3000万条权限同一 SQL 的最佳计划可能不同。
热点管理员场景:
Semi Join构建权限集合可能比:
逐订单点查权限更合适。
所以测试参数必须至少分:
普通用户 部门管理员 超级管理员4.8 权限表的一对多是正常业务,不要为了JOIN强行加唯一约束
为了让:
JOIN不重复而把权限表设计成:
UNIQUE(user_id,order_id)可能直接破坏授权模型。
正确方式应该是:
SQL匹配业务语义而不是:
为了SQL方便修改权限语义4.9 如果确实要JOIN,可以先去重权限键
例如:
JOIN(SELECTDISTINCTuser_id,tenant_id,order_idFROMuser_order_permissionWHEREuser_id=:uidANDvalid_flag=1)p这在某些场景有意义:
先把多来源授权压成资源集合 再与大业务表Join尤其管理员权限很多时。
但要看:
DISTINCT成本 权限集合大小 后续Join方式4.10 大权限集合可以先预聚合
例如同一用户:
500万授权明细但实际资源:
100万order_id可以先:
SELECT DISTINCT order_id形成较小集合。
如果这个集合会被一个复杂查询多次使用,还可以评估:
CTE 临时表 物化结果这里就与上一篇 CTE 文章衔接起来:
物化还是内联仍应由结果规模和重复使用成本决定。
4.11 相关标量子查询比EXISTS更值得警惕
例如:
SELECTo.order_id,(SELECTMAX(p.role_code)FROMpermission pWHEREp.order_id=o.order_id)role_codeFROMbiz_order o;这不是存在性判断。
它需要:
每个外表行得到一个标量结果如果优化器无法很好去相关,可能形成大量 SubPlan loops。
这类 SQL 改成:
预聚合 + LEFT JOIN往往更有价值:
LEFTJOIN(SELECTorder_id,MAX(role_code)role_codeFROMpermissionGROUPBYorder_id)pONp.order_id=o.order_id但仍然要检查:
结果等价 聚合范围 过滤能否前推4.12 OR 里的相关子查询更容易阻碍去相关
类似:
WHEREEXISTS(...)OREXISTS(...)复杂 OR 条件可能让优化器难以解除子查询结构。
KingbaseAnalyticsDB 官方查询文档也提到,某些位于 SELECT 列表或 OR 条件中的相关子查询可能按外层每一行执行,并建议通过 JOIN 或拆分查询重写。
对于 KingbaseES 生产 SQL,仍需以本版本实际计划为准,但这类结构值得重点检查:
SubPlan loops5. 结果对比:性能最快的E3为什么反而不能上线?
示例实验:
E0:相关 EXISTS + 无合适索引
Outer: 800万 Inner loops: 800万 Buffers Read: 1800万 P95: 42s正确性:
PASSE1:ANALYZE
P95: 31s正确性:
PASSE2:EXISTS + 权限复合索引
Plan: Semi Join / Index Lookup Buffers: 85万 P95: 2.4s正确性:
PASSE3:直接改普通 JOIN
Plan: Hash Join P95: 1.7s看上去:
最快但是:
结果行数倍率: 3.7×因为同一订单有多个权限来源。
正确性:
FAIL所以 E3 不能上线。
这就是本文最重要的实验结论:
性能测试只有在语义等价以后才有意义。错误结果的 1.7 秒,没有任何优化价值。
E4:JOIN + DISTINCT
Hash Join →HashAggregate / Unique结果:
正确P95:
4.1s反而比:
EXISTS + 索引 2.4s更慢。
E5:反向过滤使用 NOT EXISTS
如果需求:
找没有权限的订单测试可能得到:
Hash Anti Join / Index Anti P95=1.9s并且语义比:
NOT IN在 NULL 场景下更可控。
5.1 汇总
| 实验 | 写法 | 计划 | P95 | 结果 |
|---|---|---|---|---|
| E0 | 相关EXISTS | 重复SubPlan/Loop | 42s | 正确 |
| E1 | EXISTS+ANALYZE | 改进计划 | 31s | 正确 |
| E2 | EXISTS+复合索引 | Semi Join/Index | 2.4s | 正确 |
| E3 | 普通JOIN | Hash Join | 1.7s | 错误,重复 |
| E4 | JOIN+DISTINCT | Join+去重 | 4.1s | 正确 |
| E5 | NOT EXISTS | Anti Join | 1.9s | 正确 |
以上为方法演示数据,不是生产实测。
5.2 结果校验不能只比较COUNT
如果:
JOIN重复两条 同时漏两条总 COUNT 可能刚好相等。
所以必须比较:
主键集合例如:
EXCEPT双向差集。
至少:
Old EXCEPT New New EXCEPT Old都必须是:
0行5.3 权限SQL还必须做“泄漏校验”
普通性能 SQL:
多一行可能只是业务 Bug。
权限 SQL:
多一行可能是:
越权泄漏所以验收至少包括:
无权用户 临界角色 跨租户 失效权限 重复授权 管理员 NULL值5.4 Buffer Read 是判断是否真的减少工作的关键
E0:
1800万E2:
85万说明复合索引真正减少了权限表访问。
而不是:
刚好缓存更热这类数据比单次 Execution Time 更有复用价值。
5.5 新索引也要验写成本
权限系统可能高频:
授权 撤权 批量同步增加复合索引以后需要测:
INSERT DELETE UPDATE WAL 索引空间不能为了读 SQL:
2.4s把权限变更写入拖慢十倍。
6. 风险与复盘:子查询改JOIN最危险的是“语法更简单,语义更复杂”
6.1 风险一:一对多重复
这是权限过滤最常见事故。
解决:
保持EXISTS 或 先去重右侧集合而不是用:
DISTINCT掩盖一切问题。
6.2 风险二:NOT IN 的 NULL
如果右侧存在 NULL:
NOT IN可能与NOT EXISTS得到完全不同结果。
改写前一定检查:
约束 真实数据6.3 风险三:优化器本来已经做了Semi Join
如果计划:
Hash Semi Join说明 EXISTS 已经被很好去相关。
此时人工改 JOIN:
大概率只是改变语义不一定带来物理执行收益。
6.4 风险四:相关子查询loops被忽略
计划中单次 Inner:
0.01ms看起来快。
但:
loops=800万总体就很贵。
和 Nested Loop 文章一样:
time × loops必须一起看。
6.5 风险五:JOIN + DISTINCT制造隐藏Sort/Hash
为了修重复:
DISTINCT会引入:
Sort/Unique HashAggregate并可能产生临时文件。
所以“JOIN版本看起来简洁”不代表执行更省。
6.6 风险六:权限索引导致写放大
复合索引越多:
授权写入越慢 WAL越多读写必须一起验收。
6.7 风险七:热点用户导致计划参数敏感
普通员工:
100个资源超级管理员:
3000万资源同一 SQL:
NestLoop Semi未必同时适合。
需要参数分组测试:
普通 高权限 超级管理员推荐的等价性验证流程
任何:
子查询 → JOIN改写,都先回答:
1. 原查询是存在性、反存在性、标量还是集合? 2. JOIN右侧是否唯一? 3. NULL语义是否一致? 4. 重复行是否允许? 5. 聚合是否改变? 6. 主键集合是否完全一致? 7. 权限边界是否完全一致?然后才进入:
EXPLAIN ANALYZE推荐性能诊断顺序
原SQL ↓ EXPLAIN ANALYZE ↓ 检查SubPlan / Semi / Anti / Join ↓ estimated vs actual ↓ loops ↓ ANALYZE ↓ 权限键索引 ↓ EXISTS/IN/JOIN对照 ↓ 结果集合差分 ↓ 并发验收回退方案
如果 JOIN 改写上线后出现:
重复 漏数 越权 P95回归 写入下降立即:
1. 停止扩大新SQL流量 2. Feature Flag切回原EXISTS/IN 3. 保存新旧执行计划 4. 保存新旧结果差集 5. 做越权泄漏复核 6. 新索引先保留,确认其他SQL依赖后再决定删除 7. 恢复所有仅用于实验的join参数权限 SQL 的回退优先级应该是:
正确性 > 安全性 > 性能而不是反过来。
最终复盘
“子查询改 JOIN 是否一定更快”的答案非常明确:
不一定。原因有三层。
第一层:
优化器可能早就把子查询变成了Semi/Anti Join第二层:
普通JOIN可能产生更多行,语义根本不等价第三层:
真正的性能瓶颈往往是统计信息、索引、相关循环次数和输入规模所以真正成熟的优化思路不是:
看到子查询 →机械改JOIN而是:
确认业务语义 →查看真实计划 →判断是否已去相关 →修统计/索引 →再比较不同SQL形态 →用结果集合证明等价如果只记住一句话:
子查询是否应该改 JOIN,首先是一个“关系语义是否等价”的问题,其次才是性能问题;如果语义不等价,再快的执行计划也是错误计划。
附录 A:权限 EXISTS
SELECTo.order_idFROMbiz_order oWHEREEXISTS(SELECT1FROMuser_order_permission pWHEREp.user_id=:uidANDp.tenant_id=o.tenant_idANDp.order_id=o.order_id);附录 B:检查JOIN重复
SELECTorder_id,COUNT(*)FROM(SELECTo.order_idFROMbiz_order oJOINuser_order_permission pONp.order_id=o.order_idANDp.tenant_id=o.tenant_idWHEREp.user_id=:uid)xGROUPBYorder_idHAVINGCOUNT(*)>1;附录 C:执行计划
EXPLAIN(ANALYZE,BUFFERS,VERBOSE)SELECT...;重点:
Semi Join Anti Join Hash Join Nested Loop SubPlan Loops Rows Removed Buffers Execution Time附录 D:最低验收门禁
[ ] 原SQL业务语义已分类 [ ] JOIN侧唯一性已证明或明确不存在 [ ] NULL语义已验证 [ ] 主键集合双向差异=0 [ ] 越权行数=0 [ ] estimated/actual无重大未解释偏差 [ ] SubPlan loops已量化 [ ] 权限索引有效 [ ] P95/P99达到SLA [ ] 新索引写成本通过 [ ] 热点管理员场景通过 [ ] 回退SQL已准备转载自:https://blog.csdn.net/u014727709/article/details/163949536
欢迎 👍点赞✍评论⭐收藏,欢迎指正