分区表是 Oracle 里最能"化腐朽为神奇"的特性。我见过一张 30 亿行的历史表,全表扫描要 4 小时,改成按月分区后,查最近一个月的数据只要 30 秒——因为优化器做了分区剪枝(Partition Pruning),只扫 1/36 的数据。但分区表不是银弹,设计错了反而更慢。比如有人把分区键设成 UUID,分区剪枝完全失效;有人用了 1000 个分区,解析计划的时间比执行还长。这篇文章把三种分区策略的适用场景、性能边界、以及设计陷阱拆清楚。
1 范围分区:时间序列数据的天然归宿
范围分区(Range Partitioning)是最常用的分区策略,特别适合时间序列数据:日志、订单、交易流水。分区键通常是 DATE 或 TIMESTAMP,按天、按月、按季度分区。核心优势是分区剪枝:WHERE create_time BETWEEN SYSDATE-30 AND SYSDATE优化器知道只需要扫最近 30 天的分区,其他分区直接跳过。剪枝发生在解析阶段,不需要访问分区表的数据字典,所以开销极低。我们测过,30 亿行表按月分区(36 个分区),查最近一个月的逻辑读从 1.2 亿降到 35 万,提升 340 倍。
范围分区的另一个优势是分区维护:旧分区可以单独 DROP 或 TRUNCATE,不影响其他分区。比如保留 3 年数据,每月 DROP 最旧的分区,比 DELETE 快得多,而且不产生大量 redo 和 undo。但范围分区有个前提:查询必须带分区键的谓词。如果查询是 WHERE order_id = 12345,不带 create_time,优化器无法剪枝,所有分区都要扫,性能比不分区还慢——因为分区表多了分区层级的索引和元数据开销。所以我们设计分区表时,第一条铁律就是:确保 80% 的查询都带分区键谓词。如果做不到,别分区。
2 哈希分区:打散热点,但剪枝靠等值
哈希分区(Hash Partitioning)按分区键的哈希值取模分布数据,目的是把数据均匀打散到各个分区,避免单个分区成为热点。典型场景是:用户表按 user_id 哈希分区,订单表按 order_id 哈希分区。哈希分区的优势是 I/O 均衡:每个分区的数据量、行数、块数大致相等,并行扫描时每个并行进程的工作量均衡。但哈希分区的剪枝条件很苛刻:只有等值查询(WHERE user_id = 123)才能剪枝,范围查询(WHERE user_id BETWEEN 100 AND 200)无法剪枝——因为哈希值是乱的,100 和 200 可能落在同一个分区,也可能落在不同分区,优化器不敢赌,只能全扫。所以哈希分区适合"点查多、范围查少"的场景。
哈希分区的分区数必须是 2 的幂次方(2、4、8、16、32...),这不是 Oracle 的限制,是数学上的建议:2^n 取模时,哈希分布最均匀。如果设 10 个分区,某些分区的数据量可能比其他分区多 30%,并行扫描时出现"长尾"——一个进程忙得要死,其他进程闲着。我们给一个客户做哈希分区时,一开始设了 12 个分区,发现最大分区比最小分区多 45% 的数据。改成 16 个分区后,差异降到 8%。这个经验我记到现在:哈希分区数必须是 2^n,别偷懒。
3 复合分区:范围+哈希的两层架构
复合分区(Composite Partitioning)是"先范围、再哈希"的两层结构:第一层分区键做范围分区(比如按月),每个范围分区内再按第二键做哈希子分区(比如按 user_id)。这种设计兼顾了时间维度的剪枝和用户维度的打散。典型场景是:日志表,按月分区(方便清理旧数据),每月内按 user_id 哈希(避免某个月份内个别用户的数据过于集中)。复合分区的优势是灵活,但代价是元数据开销大:如果按月分 36 个分区,每个分区再哈希 16 个子分区,总共 576 个子分区,数据字典里的分区对象就有 576 个,解析 SQL 时需要遍历这些元数据,硬解析时间增加。我们测过,1000 个子分区的表,硬解析时间比不分区表多 30ms,对于 OLTP 的短查询(本身执行 5ms),这 30ms 的解析开销很致命。所以复合分区适合大表、复杂分析查询,不适合高频短查询。
4 实验:三种分区策略的剪枝效率对比
实验环境:Oracle 19c,测试表 event_logs,3 亿行,字段:log_id(NUMBER)、user_id(NUMBER)、create_time(DATE)、content(VARCHAR2)。构造三种分区策略,对比相同查询下的逻辑读和执行时间。
-- 策略 A:范围分区(按月)
CREATE TABLE event_logs_range (
log_id NUMBER,
user_id NUMBER,
create_time DATE,
content VARCHAR2(4000)
) PARTITION BY RANGE (create_time) INTERVAL (NUMTODSINTERVAL(1, 'MONTH')) (
PARTITION p_init VALUES LESS THAN (TO_DATE('2024-01-01','YYYY-MM-DD'))
);
-- 策略 B:哈希分区(16 个分区)
CREATE TABLE event_logs_hash (
log_id NUMBER,
user_id NUMBER,
create_time DATE,
content VARCHAR2(4000)
) PARTITION BY HASH (user_id) PARTITIONS 16;
-- 策略 C:复合分区(范围+哈希)
CREATE TABLE event_logs_comp (
log_id NUMBER,
user_id NUMBER,
create_time DATE,
content VARCHAR2(4000)
) PARTITION BY RANGE (create_time) INTERVAL (NUMTODSINTERVAL(1, 'MONTH'))
SUBPARTITION BY HASH (user_id) SUBPARTITIONS 16 (
PARTITION p_init VALUES LESS THAN (TO_DATE('2024-01-01','YYYY-MM-DD'))
);
-- 查询 1:时间范围查询(带分区键)
SELECT COUNT(*) FROM event_logs_xxx
WHERE create_time BETWEEN SYSDATE-30 AND SYSDATE;
-- 范围分区:逻辑读 35 万,时间 2.1 秒(剪枝 35/36 分区)
-- 哈希分区:逻辑读 1.2 亿,时间 185 秒(无法剪枝,全表扫描)
-- 复合分区:逻辑读 38 万,时间 2.3 秒(外层范围剪枝有效)
-- 查询 2:用户等值查询(带哈希键)
SELECT COUNT(*) FROM event_logs_xxx WHERE user_id = 123456;
-- 范围分区:逻辑读 1.2 亿,时间 190 秒(无法剪枝)
-- 哈希分区:逻辑读 85 万,时间 4.5 秒(剪枝到 1/16 分区)
-- 复合分区:逻辑读 92 万,时间 4.8 秒(子分区剪枝有效)
-- 查询 3:时间范围 + 用户等值(复合条件)
SELECT COUNT(*) FROM event_logs_xxx
WHERE create_time BETWEEN SYSDATE-30 AND SYSDATE
AND user_id = 123456;
-- 范围分区:逻辑读 35 万,时间 2.2 秒(时间剪枝后 user_id 过滤)
-- 哈希分区:逻辑读 85 万,时间 4.6 秒(user_id 剪枝,但时间范围全扫该分区)
-- 复合分区:逻辑读 2.5 万,时间 0.8 秒(双层剪枝:先时间范围,再 user_id 哈希)
实验结果很直观:范围分区在时间查询上无敌,但在用户查询上完全失效;哈希分区在用户点查上优秀,但在时间范围上垃圾;复合分区在复合条件上最强,但元数据开销大。所以选型原则是:如果时间查询占 80%,选范围分区;如果用户点查占 80%,选哈希分区;如果两种查询都很多(各占 40%),选复合分区,但要接受解析开销。另外,INTERVAL 分区(自动按月/按天生成分区)虽然方便,但分区数无限增长后,数据字典会膨胀,建议定期合并旧分区(MERGE PARTITIONS)或 DROP 过期分区。
【踩坑笔记】分区表设计的第一原则:分区键必须出现在查询的 WHERE 子句里,而且最好是等值或范围谓词。如果查询条件是函数(比如 WHERE TRUNC(create_time) = '2024-01-01'),分区剪枝会失效,因为优化器无法把函数应用到分区边界上。正确写法是:WHERE create_time >= TO_DATE('2024-01-01') AND create_time < TO_DATE('2024-01-02')。
5 分区索引:本地索引 vs 全局索引的博弈
分区表上的索引分两种:本地索引(Local Index):索引按表的分区策略分区,每个表分区对应一个索引分区。优势是分区维护方便:DROP 表分区时,对应的索引分区自动 DROP,不需要重建整个索引。劣势是索引扫描不能跨分区有序——如果查询需要 ORDER BY 非分区键,每个索引分区内部有序,但跨分区需要额外排序。全局索引(Global Index):索引不按表分区,是一个整体。优势是支持任意列的有序扫描,劣势是分区维护成本高:DROP 表分区后,全局索引会失效(UNUSABLE),需要重建。我们给一个客户做分区表时,因为需要支持 ORDER BY order_id,建了全局索引。结果每月 DROP 旧分区后,全局索引失效,重建索引要 3 小时,期间查询走全表扫描,性能暴跌。最后改成本地索引 + 应用层排序,虽然数据库端多了排序开销,但分区维护从 3 小时降到 5 分钟。这个 trade-off 值得。
总结:分区表设计没有标准答案,只有场景答案。范围分区适合时间序列,哈希分区适合打散热点,复合分区适合复杂查询。但无论哪种,分区键必须出现在 WHERE 里,否则分区就是摆设。另外,分区数别太多(<1000),子分区数别太多(<100),否则硬解析和数据字典维护会吃掉所有性能收益。最后送一句话:分区表是把双刃剑,用好了化腐朽为神奇,用错了化神奇为腐朽。设计之前,先画一张查询类型分布图,看看你的 WHERE 子句长什么样,再决定分不分、怎么分。