文章目录
- 每日一句正能量
- 1. 背景与问题:明明按天分区,为什么查一天却扫描了十二个月?
- 2. 环境与数据:先明确分区键、数据类型和边界语义
- 2.1 第一件事:确认真正的分区键
- 2.2 第二件事:确认数据类型
- 2.3 第三件事:检查裁剪开关
- 3. 复现过程:六类边界 SQL 怎样让裁剪能力下降
- 3.1 案例一:对分区键做 CAST
- 3.2 案例二:date_trunc包裹分区键
- 3.3 案例三:时区转换写在列上
- 3.4 案例四:错误使用 BETWEEN
- 3.5 案例五:OR中存在不受分区键约束的分支
- 3.6 案例六:可空参数模板
- 3.7 Prepared Statement还要单独验证
- 4. 方案实施:怎样把分区条件写成“优化器容易证明”的形式
- 4.1 第一原则:分区键尽量保持裸列
- 4.2 第二原则:把函数作用在常量侧
- 4.3 第三原则:DATE/TIMESTAMP类型一致
- 4.4 第四原则:统一使用半开区间
- 4.5 第五原则:按查询主入口选择分区键
- 4.6 第六原则:分区粒度不要过细
- 4.7 第七原则:索引不能替代分区裁剪
- 4.8 第八原则:执行计划必须数分区,而不是只看顶层成本
- 4.9 第九原则:检查 enable_partition_pruning
- 4.10 第十原则:传统继承表与声明式分区要区分
- 5. 结果对比:从12个分区18.6秒,降到1个分区420ms
- E0:函数包裹分区键
- E1:改成直接范围
- E2:日报缩成一天半开区间
- E3:拆分 OR
- E4:按参数形态拆 SQL
- E5:只加索引,不修谓词
- 5.1 实验汇总
- 5.2 不只看Execution Time,还要看Planning Time
- 5.3 分区裁剪后并行计划可能自然消失
- 5.4 Rows Removed by Filter也能证明裁剪失败
- 6. 风险与复盘:分区裁剪优化最危险的是“性能修好了,时间口径却改错了”
- 6.1 风险一:时区边界错误
- 6.2 风险二:DATE与TIMESTAMP边界混淆
- 6.3 风险三:BETWEEN重复午夜数据
- 6.4 风险四:OR拆分改变重复语义
- 6.5 风险五:只在Literal SQL验证
- 6.6 风险六:分区键选错
- 6.7 风险七:分区太多
- 6.8 风险八:把 enable_partition_pruning 当业务开关
- 推荐的分区裁剪诊断顺序
- 回退方案
- 最终复盘
- 附录 A:推荐日边界
- 附录 B:检查裁剪开关
- 附录 C:执行计划验证
- 附录 D:最低验收门禁
每日一句正能量
心有所爱,无畏时光。
当一个人内心有了真正热爱的人或事,他便拥有了对抗时间流逝与虚无的铠甲。热爱如同一个稳固的精神内核,能消解时光带来的焦虑、迷茫与恐惧,让人在变化中获得恒常的定力。
主题:分区查询 / 时间序列表 / Partition Pruning
重点:范围分区、分区键、时间边界、函数与 CAST、时区、Prepared Statement、OR 条件、enable_partition_pruning、执行计划、Buffers、P95/P99
适用场景:KingbaseES 上的交易流水、日志、指标、审计、订单历史、IoT 时序数据等按日期或时间字段进行 Range 分区的大表。
1. 背景与问题:明明按天分区,为什么查一天却扫描了十二个月?
时间序列表常见设计:
每天/每月一个分区例如:
ts_event ├── 2026-01 ├── 2026-02 ├── ... └── 2026-12业务只查询:
2026-08-01按理说:
只需要访问8月或者8月1日对应分区但线上计划却可能:
Append ├── Scan p202601 ├── Scan p202602 ├── ... └── Scan p202612甚至:
12个分区全部参与P95 从:
400ms变成:
18s很多人第一反应是:
分区失效了然后开始:
加索引 开并行 调work_mem但真正应该先问:
优化器能不能从WHERE条件中证明 “不可能命中其他分区”?这就是 Partition Pruning 的核心。
KingbaseES 官方对象管理文档明确指出,范围分区通常和日期一起使用,数据库根据分区键的值范围将记录映射到各个分区;开发规范也明确建议,对经常按时间段查询的大表优先采用时间字段 Range 分区,并要求高频 SQL 尽量带有分区条件。
因此本文第一个结论是:
分区表本身并不会自动让查询变快。真正产生收益的是:SQL 谓词能够被优化器识别成与分区边界一致的条件,从而在规划期或执行期排除不相关分区。
2. 环境与数据:先明确分区键、数据类型和边界语义
示例表:
CREATETABLEts_event(event_idBIGINTNOTNULL,event_timeTIMESTAMPNOTNULL,tenant_idBIGINTNOTNULL,event_typeINTNOTNULL,payloadVARCHAR(500))PARTITIONBYRANGE(event_time);逻辑上按天划分:
p20260801 [2026-08-01 00:00:00, 2026-08-02 00:00:00) p20260802 [2026-08-02 00:00:00, 2026-08-03 00:00:00)KingbaseES 官方范围分区示例采用“上界小于”语义,日期范围通过VALUES LESS THAN类边界来定义。工程上最自然的查询写法正是:
[start, end)也就是:
大于等于开始时间 小于下一个边界而不是人为构造:
23:59:59.999999这样的末秒值。
2.1 第一件事:确认真正的分区键
很多系统设计:
按event_time分区业务 SQL 却过滤:
biz_time created_at receive_time如果:
event_time完全不在 WHERE 中,那么数据库没有依据排除分区。
这种问题不是:
裁剪算法坏了而是:
分区键与查询入口不匹配所以任何分区性能问题第一张表必须记录:
| 项目 | 示例 |
|---|---|
| 分区键 | event_time |
| 类型 | TIMESTAMP |
| 分区粒度 | DAY |
| 查询过滤列 | biz_time |
| 是否同列 | 否 |
如果最后一项是:
否后面的 SQL 微调可能只能治标。
2.2 第二件事:确认数据类型
时间列常见:
DATE TIMESTAMP TIMESTAMPTZ VARCHAR日期字符串SQL 参数常见:
Java LocalDate LocalDateTime OffsetDateTime String数据库真正收到什么类型,会影响:
隐式转换 表达式形态 分区边界比较因此不能只看:
应用代码里的变量类型还要看:
数据库计划里最终的条件表达式2.3 第三件事:检查裁剪开关
KingbaseES 数据库参考手册提供:
enable_partition_pruning参数,默认值为:
on并且它是声明式分区表裁剪的独立控制项。传统继承表的constraint_exclusion与声明式分区并不是同一个机制;官方参考手册也明确提示,现代分区表的对应能力由enable_partition_pruning控制。
检查:
SHOWenable_partition_pruning;如果人为关闭:
off先解决这个问题。
但在生产事故中,更常见的情况是:
参数是on SQL谓词却无法产生理想裁剪3. 复现过程:六类边界 SQL 怎样让裁剪能力下降
3.1 案例一:对分区键做 CAST
常见 SQL:
SELECT*FROMts_eventWHEREevent_time::date=DATE'2026-08-01';业务表达很自然:
取8月1日所有记录但是优化器看到的是:
CAST(event_time AS date)而分区定义依据的是:
event_time本身。
对于不同版本、不同表达式能力,优化器是否能完全推导出原始时间边界不能靠猜。
生产调优更稳妥的方式是直接表达:
WHEREevent_time>=TIMESTAMP'2026-08-01 00:00:00'ANDevent_time<TIMESTAMP'2026-08-02 00:00:00';这时谓词和分区边界天然一致。
3.2 案例二:date_trunc包裹分区键
WHEREdate_trunc('day',event_time)=TIMESTAMP'2026-08-01 00:00:00'这和上一类本质相同:
业务条件描述在“加工后的列”而不是:
原始分区键如果最终计划扫描大量子分区,最优先的修复就是:
把函数移出分区键计算:
start end两个常量/绑定参数。
3.3 案例三:时区转换写在列上
例如:
WHEREevent_time ATTIMEZONE'UTC'>=:local_start;这个 SQL 有两个风险:
1. 对分区键做了表达式 2. 时间语义可能发生变化更稳妥的方法:
业务本地时间 ↓ 应用/SQL入口计算成数据库分区键采用的时间基准 ↓ 绑定start_ts/end_ts ↓ 直接比较event_time例如:
WHEREevent_time>=:utc_startANDevent_time<:utc_end;这样:
时区转换和:
分区边界判断解耦。
3.4 案例四:错误使用 BETWEEN
很多日报 SQL:
WHEREevent_timeBETWEENTIMESTAMP'2026-08-01 00:00:00'ANDTIMESTAMP'2026-08-02 00:00:00';BETWEEN两端通常都是包含语义。
那么:
2026-08-02 00:00:00本身也在结果范围。
如果日分区边界:
8月2日00:00已经属于下一个分区。
这会造成:
边界额外命中更重要的是业务口径也可能重复。
例如日报:
8月1日和:
8月2日都把:
8月2日00:00:00算进去。
所以时间范围最好统一:
[start, end)3.5 案例五:OR中存在不受分区键约束的分支
WHERE(event_time>=:start_tsANDevent_time<:end_ts)ORevent_id=:event_id;第一分支:
只需要一天第二分支:
event_id可能在任何分区从整个 OR 语义看:
任何分区都有可能命中event_id因此不能指望:
只扫描时间范围对应分区这时可以借鉴前面 OR 条件文章的思路:
SELECT...FROMts_eventWHEREevent_time>=:start_tsANDevent_time<:end_tsUNIONSELECT...FROMts_eventWHEREevent_id=:event_id;两个分支独立规划。
但必须验证:
同一行同时满足两条分支时是否需要去重。
如果不能证明互斥:
不要直接改UNION ALL3.6 案例六:可空参数模板
框架很喜欢:
WHERE(:start_tsISNULLORevent_time>=:start_ts)AND(:end_tsISNULLORevent_time<:end_ts)优点:
一个SQL模板支持所有参数组合缺点:
优化器需要同时考虑参数为空和不为空如果实际接口 99% 都带:
start/end那么为了 1% 的无边界查询,把所有请求都变成一个复杂模板并不值得。
更稳定:
Q_TIME_BOUNDED Q_UNBOUNDED拆成两个 SQL fingerprint。
这样:
有时间范围的SQL天然保留清晰的分区条件。
3.7 Prepared Statement还要单独验证
应用里:
JDBC PreparedStatement可能和手工 literal SQL 的规划行为不同。
因此必须用真实:
PREPARE / EXECUTE或 JDBC/PBE 复现。
不能只在:
ksql里写死日期然后断言生产一定裁剪同样的分区。
尤其上一系列文章已经讨论过:
Generic Plan / Custom Plan参数敏感。
分区裁剪也应该和真实参数化方式一起验证。
4. 方案实施:怎样把分区条件写成“优化器容易证明”的形式
4.1 第一原则:分区键尽量保持裸列
优先:
event_time>=:start_tsANDevent_time<:end_ts不优先:
date(event_time)=:dayto_char(event_time,'YYYYMMDD')=:devent_time::date=:dextract(monthfromevent_time)=8这些写法不一定在所有版本里都百分之百无法裁剪,但工程上:
裸分区键 + 直接边界比较最清晰、最稳。
4.2 第二原则:把函数作用在常量侧
错误思路:
date_trunc('day',event_time)=:d更合理:
start = date_trunc(day,:input) end = start + 1 day然后:
event_time>=:startANDevent_time<:end也就是说:
加工查询参数,不加工分区键。
4.3 第三原则:DATE/TIMESTAMP类型一致
如果分区键:
TIMESTAMP绑定参数尽量:
TIMESTAMP而不是:
VARCHAR让数据库自行猜转换。
同理:
TIMESTAMPTZ要明确时区。
这种规范不仅帮助裁剪,也减少:
索引失效 隐式转换 时区Bug4.4 第四原则:统一使用半开区间
日:
[2026-08-01, 2026-08-02)月:
[2026-08-01, 2026-09-01)年:
[2026-01-01, 2027-01-01)SQL:
event_time>=:startANDevent_time<:end优势:
没有微秒精度问题 没有边界重复 和Range分区上界逻辑天然一致4.5 第五原则:按查询主入口选择分区键
官方开发规范明确建议,高频执行的 SQL 应尽量包含分区条件;对按时间范围检索的大表优先使用时间范围分区。
如果核心接口 90% 都是:
按biz_time查却按:
ingest_time分区。
那么长期应该评估:
分区设计本身是否错位而不是永远用 SQL 技巧补救。
4.6 第六原则:分区粒度不要过细
按小时分区:
一年8760个分区可能增加:
规划开销 对象数量 维护复杂度KingbaseES 开发规范建议控制分区规模与分区数量,并提醒对象过多会扩大数据字典并影响整体效率。
所以:
裁剪越细越好也不成立。
需要平衡:
单分区数据量 查询时间范围 维护周期 分区数量4.7 第七原则:索引不能替代分区裁剪
假设:
12个分区全部扫描每个分区都有:
(event_time, tenant_id)索引。
索引可能把:
18秒降到:
11秒但真正正确的计划:
只扫描1个分区可能:
0.4秒两者不是一个量级。
所以:
分区裁剪是“不要访问不相关数据”,索引是“在已经访问的数据范围里更快找到记录”。这两个优化层次不能互相替代。
4.8 第八原则:执行计划必须数分区,而不是只看顶层成本
运行:
EXPLAIN(ANALYZE,BUFFERS,VERBOSE)SELECT...重点:
Append / Merge Append 下面有多少子扫描 分区名字 actual loops Buffers Rows Removed by Filter Planning Time Execution Time不同 KingbaseES 版本在:
运行期裁剪信息的具体计划显示字段上可能有差异。
因此不要依赖:
某一个固定字段名判断。
最可靠:
看实际参与扫描的子计划/分区4.9 第九原则:检查 enable_partition_pruning
诊断实验:
SHOWenable_partition_pruning;正常:
on可以在测试环境做反向实验:
on vs off验证:
当前SQL性能收益是否确实来自裁剪但不要把:
关闭分区裁剪作为任何长期优化手段。
KingbaseES 参考手册确认该参数是用户级布尔开关,默认开启。
4.10 第十原则:传统继承表与声明式分区要区分
如果历史系统使用:
INHERITS + CHECK做老式分区。
裁剪还会涉及:
constraint_exclusion官方参考手册说明,constraint_exclusion=partition主要针对传统继承子表与相关约束检查;声明式分区的等价机制由enable_partition_pruning单独控制。
所以排查:
老系统升级时必须先确认:
这张“分区表”究竟是哪种实现否则参数会查错方向。
5. 结果对比:从12个分区18.6秒,降到1个分区420ms
E0:函数包裹分区键
WHEREevent_time::date=DATE'2026-08-01'示例计划:
12个月分区全部参与Buffers:
980万P95:
18.6sE1:改成直接范围
WHEREevent_time>=TIMESTAMP'2026-08-01 00:00:00'ANDevent_time<TIMESTAMP'2026-09-01 00:00:00'扫描:
1个月分区Buffers:
82万P95:
1.9sE2:日报缩成一天半开区间
WHEREevent_time>=:day_startANDevent_time<:next_day扫描:
目标日分区Buffers:
11万P95:
420ms这才是真正发挥:
时间分区价值。
E3:拆分 OR
原:
time condition OR event_id改成:
时间范围分支 UNION event_id分支时间分支可以独立裁剪。
P95:
710ms注意:
UNION/UNION ALL必须先验证重复语义。
E4:按参数形态拆 SQL
原:
:start IS NULL OR ...拆成:
Q_WITH_TIME Q_WITHOUT_TIME有边界查询:
始终保留直接分区条件P99:
明显收敛这对:
高频API往往比让一条万能 SQL 支持所有参数组合更稳定。
E5:只加索引,不修谓词
示例:
仍扫描12个分区Buffers:
640万P95:
10.9s有改善。
但远不及:
真正裁剪5.1 实验汇总
| 实验 | 条件 | 扫描分区 | Buffers | P95 |
|---|---|---|---|---|
| E0 | event_time::date=:d | 12 | 980万 | 18.6s |
| E1 | 直接月范围 | 1 | 82万 | 1.9s |
| E2 | 类型一致的日半开区间 | 1 | 11万 | 0.42s |
| E3 | OR拆分 | 目标分区+独立ID分支 | 19万 | 0.71s |
| E4 | 参数形态拆SQL | 目标分区 | 9.5万 | 0.36s |
| E5 | 仅加索引 | 12 | 640万 | 10.9s |
以上为方法演示数据,不是生产实测。
5.2 不只看Execution Time,还要看Planning Time
如果:
几千个分区即使执行期最终只访问一个分区:
规划成本也可能上升。
所以极端分区数系统要记录:
Planning Time Execution Time两者。
官方开发规范也明确提醒,不应让单表分区数量无限增长,因为对象过多会影响数据库整体效率。
5.3 分区裁剪后并行计划可能自然消失
裁剪失效:
扫描12个月优化器可能:
Parallel Append Parallel Seq Scan修复后:
只扫描1天计划可能不再使用:
并行这不是性能退化。
而是:
工作量已经小到不值得启动Worker官方并行查询文档也强调,并行存在启动和进程间通信成本,不是 Worker 越多越快。
5.4 Rows Removed by Filter也能证明裁剪失败
如果多个分区节点:
读了大量行 然后Rows Removed by Filter非常高说明数据库:
先访问 后过滤真正理想的是:
根本不访问无关分区这就是:
Partition Pruning和普通 Filter 的本质区别。
6. 风险与复盘:分区裁剪优化最危险的是“性能修好了,时间口径却改错了”
6.1 风险一:时区边界错误
应用:
Asia/Shanghai数据库:
UTC如果直接把:
2026-08-01 00:00绑定成 UTC:
会少/多8小时所以修裁剪时必须同时验证:
业务时区 数据库时区 JDBC时区 分区键存储语义6.2 风险二:DATE与TIMESTAMP边界混淆
DATE:
一天TIMESTAMP:
精确时间点不要依赖隐式转换猜测。
应明确:
start/end类型6.3 风险三:BETWEEN重复午夜数据
日报最典型。
修复:
统一 [start,end)并在:
00:00:00 23:59:59.999...边界做测试。
6.4 风险四:OR拆分改变重复语义
原 OR:
同一行只出现一次UNION ALL:
可能两次必须:
证明互斥 或使用UNION去重6.5 风险五:只在Literal SQL验证
手工:
日期写死裁剪很好。
应用:
PreparedStatement却慢。
这时必须:
从真实JDBC/PBE链路复现不能只说:
数据库本地测试没问题6.6 风险六:分区键选错
如果大量核心查询天然不带:
当前分区键说明:
表设计与查询模型长期错位应该评估:
重新分区 二级分区 汇总层 冷热分离而不是一直靠特殊 SQL。
6.7 风险七:分区太多
超细粒度:
可以更精确裁剪但会提高:
DDL维护 统计采集 规划 数据字典 备份成本。
分区设计必须同时服务:
查询 维护 生命周期6.8 风险八:把 enable_partition_pruning 当业务开关
该参数是优化器能力开关。
正常生产:
应该保持开启如果为了某一条 SQL:
关闭会伤害大量其他分区查询。
它适合:
诊断对照不适合:
业务级长期调优推荐的分区裁剪诊断顺序
遇到分区查询慢:
1. 确认分区键和分区类型 2. SHOW enable_partition_pruning 3. EXPLAIN ANALYZE数实际扫描分区 4. 看分区键有没有被函数/CAST包裹 5. 检查参数类型和时区 6. 改成[start,end)直接范围 7. 检查OR/可空参数 8. 用真实PreparedStatement复测 9. 记录Buffers/P95/P99 10. 再决定是否需要索引/并行顺序不能反。
如果第一步没裁剪:
先加十个索引只是让:
错误访问的数据稍微快一点回退方案
如果边界 SQL 改写上线后出现:
漏数 重复 跨天错误 时区错误 P99回归执行:
1. 停止扩大新SQL 2. Feature Flag/Query Mapping恢复旧SQL 3. 保存新旧执行计划 4. 保存真实Bind参数类型 5. 验证午夜/月末/时区边界 6. 如果用了UNION,检查重复集合 7. 新索引暂时保留,完成依赖检查后再决定删除尤其:
时间边界错误优先级高于:
性能收益最终复盘
分区裁剪的本质可以用一句非常简单的话解释:
优化器必须能够证明某个分区不可能包含结果最容易证明的 SQL:
partition_key>=:startANDpartition_key<:end最难证明的往往是:
函数(partition_key) CAST(partition_key) 时区表达式(partition_key) 复杂OR 可空参数模板所以:
时间分区 SQL 的最佳实践不是“把日期条件写出来就行”,而是让分区键以原始类型、直接范围、与分区边界一致的形式出现在谓词中。
如果只记住一句话:
分区裁剪优化不是让已扫描的分区更快,而是让无关分区从执行计划里彻底消失;真正值得监控的不是“有没有分区表”,而是“一条只查一天的 SQL 到底实际扫描了几个分区”。
这也是为什么生产验收必须记录:
扫描分区数 + Buffers + actual rows + Planning Time + Execution Time + P95/P99而不能只看:
SQL最终返回多少行附录 A:推荐日边界
WHEREevent_time>=:day_startANDevent_time<:next_day附录 B:检查裁剪开关
SHOWenable_partition_pruning;附录 C:执行计划验证
EXPLAIN(ANALYZE,BUFFERS,VERBOSE)SELECT...FROMts_eventWHEREevent_time>=:start_tsANDevent_time<:end_ts;重点看:
Append / Merge Append 实际子分区扫描 Buffers Rows Removed by Filter Planning Time Execution Time附录 D:最低验收门禁
[ ] KingbaseES版本已记录 [ ] 兼容模式已记录 [ ] 分区键已确认 [ ] enable_partition_pruning=on [ ] 目标SQL实际扫描分区数已记录 [ ] 分区键未被不必要函数/CAST包裹 [ ] DATE/TIMESTAMP/TIMESTAMPTZ类型已确认 [ ] 时间区间采用[start,end) [ ] JDBC参数类型已验证 [ ] OR/可空参数已评估 [ ] 午夜/月末边界结果差异=0 [ ] 时区边界结果正确 [ ] P95/P99达到SLA [ ] 回退SQL已准备转载自:https://blog.csdn.net/u014727709/article/details/163950471
欢迎 👍点赞✍评论⭐收藏,欢迎指正