数据库基础体系 · 第 109/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
Oracle 分区与大表治理:Range、List、Hash、交换和裁剪
Oracle 分区表(partitioned table)不是把一张逻辑表拆成多张需要应用自行维护的表,而是由 Oracle 维护一张统一的逻辑表,并将数据、索引和部分元数据按分区组织。应用仍然查询原表名,优化器和执行引擎根据谓词决定需要访问哪些分区。
分区解决的主要问题是:
- 将大表按业务维度组织,降低单次维护的数据范围;
- 让查询能够跳过不相关的数据,这称为分区裁剪(partition pruning);
- 让历史数据归档、删除、装载可以按分区完成;
- 为分区级索引、分区交换、分区级统计信息和并行处理提供基础。
分区并不会自动解决所有性能问题。一个没有合适谓词的查询,仍然可能扫描整张分区表;一个设计不当的分区键,也可能导致数据倾斜、分区数量失控或维护复杂度增加。
一、分区的基本模型
1.1 逻辑表与物理分区
假设有一张订单表:
CREATE TABLE orders (
order_id NUMBER NOT NULL,
order_date DATE NOT NULL,
customer_id NUMBER NOT NULL,
region VARCHAR2(10) NOT NULL,
amount NUMBER(18, 2) NOT NULL
)
PARTITION BY RANGE (order_date)
(
PARTITION p2024q1 VALUES LESS THAN (DATE '2024-04-01'),
PARTITION p2024q2 VALUES LESS THAN (DATE '2024-07-01'),
PARTITION p2024q3 VALUES LESS THAN (DATE '2024-10-01'),
PARTITION p2024q4 VALUES LESS THAN (DATE '2025-01-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
这里:
orders是逻辑表;order_date是分区键;p2024q1到pmax是分区;- 每一行根据
order_date被放入且只能放入一个分区; MAXVALUE表示大于此前所有边界的值。
对于 Range 分区,分区边界通常使用半开区间理解:
p2024q1: order_date < 2024-04-01
p2024q2: 2024-04-01 <= order_date < 2024-07-01
p2024q3: 2024-07-01 <= order_date < 2024-10-01
p2024q4: 2024-10-01 <= order_date < 2025-01-01
pmax: order_date >= 2025-01-01
因此下面的插入会进入不同分区:
INSERT INTO orders VALUES
(1, DATE '2024-03-31', 101, 'CN', 100.00);
INSERT INTO orders VALUES
(2, DATE '2024-04-01', 102, 'CN', 200.00);
INSERT INTO orders VALUES
(3, DATE '2025-02-01', 103, 'US', 300.00);
结果分别是:
1 -> p2024q1
2 -> p2024q2
3 -> pmax
分区键不是普通索引列。它决定的是行的物理归属。这也是分区键设计与索引设计的根本区别:索引主要改变访问路径,分区改变数据组织边界。
1.2 分区键与跨分区更新
如果更新一行后,分区键值使它应该进入另一个分区,Oracle 需要执行一次跨分区移动。
默认情况下,直接执行这类更新可能失败:
UPDATE orders
SET order_date = DATE '2024-07-01'
WHERE order_id = 2;
如果该行原本在 p2024q2,更新后应该进入 p2024q3,Oracle 可能报告:
ORA-14402: updating partition key column would cause a partition change
可以显式启用行移动:
ALTER TABLE orders ENABLE ROW MOVEMENT;
启用后,Oracle 可以将行从旧分区移动到新分区,但这不是零成本操作:
- 删除旧分区中的行;
- 在新分区中插入行;
- 维护相关索引;
- 产生相应的重做和撤销信息;
- 可能改变行的位置和
ROWID。
因此,ENABLE ROW MOVEMENT 不是性能优化开关。只有在确实允许分区键更新,且应用能够接受行移动语义时才应启用。
二、Range 分区:按有序边界组织数据
2.1 Range 的形式化定义
设分区键值为 ,边界为:
则第 个分区通常表示:
第一个分区可以理解为:
最后使用 MAXVALUE 时,表示剩余的上界空间。
Range 分区适合分区键具有自然顺序的场景:
- 时间;
- 自增编号区间;
- 金额区间;
- 租户编号区间;
- 地理编码或版本号区间。
最常见的是时间分区,因为数据通常按时间增长,旧数据也有明确的生命周期。
2.2 时间分区的边界陷阱
Oracle 的 DATE 包含日期和时间。下面的条件:
DATE '2024-04-01'
表示:
2024-04-01 00:00:00
所以:
WHERE order_date < DATE '2024-04-01'
能够覆盖整个 3 月数据,而:
WHERE order_date >= DATE '2024-04-01'
AND order_date < DATE '2024-05-01'
覆盖的是 4 月整月。
不建议用下面这种闭区间表达月末:
WHERE order_date BETWEEN
DATE '2024-04-01'
AND DATE '2024-04-30'
原因是它只到 2024-04-30 00:00:00,并不包含 4 月 30 日当天的其他时间。时间范围更可靠的写法是:
WHERE order_date >= DATE '2024-04-01'
AND order_date < DATE '2024-05-01'
这也正好符合 Range 分区的半开区间模型。
2.3 手工 Range 与 Interval Range
手工创建的分区需要提前建立边界:
ALTER TABLE orders
ADD PARTITION p2025q1 VALUES LESS THAN (DATE '2025-04-01');
如果数据会持续进入新的时间范围,可以使用 Interval 分区自动创建后续分区。例如:
CREATE TABLE event_log (
event_id NUMBER NOT NULL,
event_time TIMESTAMP NOT NULL,
payload VARCHAR2(4000)
)
PARTITION BY RANGE (event_time)
INTERVAL (NUMTOYMINTERVAL(1, 'MONTH'))
(
PARTITION p_before_2024
VALUES LESS THAN (TIMESTAMP '2024-01-01 00:00:00')
);
其语义是:
- 小于
2024-01-01的数据进入初始分区; - 之后每个月需要新分区时,Oracle 根据间隔自动创建;
- 分区命名、权限、存储属性和运维监控需要在目标版本上仔细验证;
- 自动创建分区不能替代生命周期管理,旧分区仍需要归档或删除策略。
Interval 分区适合连续时间流,但也有边界:如果业务产生了远未来的异常时间值,Oracle 可能自动创建大量中间分区。生产环境必须校验时间输入,并监控分区数量。
2.4 Range 的反例
如果数据的主要访问条件是:
WHERE customer_id = :id
却按照 order_date 划分 Range,查询可能需要访问所有时间分区。虽然每个分区内部可以使用 customer_id 索引,但分区本身没有帮助定位客户数据。
反过来,如果按照 customer_id 做大量 Range 分区,而业务主要是按时间清理历史数据,则历史删除会失去天然的时间边界。
分区键应同时考虑:
- 主要过滤条件;
- 数据生命周期;
- 数据增长方向;
- 分区数量;
- 数据分布是否均匀;
- 分区维护动作是否自然。
三、List 分区:按离散集合组织数据
3.1 List 的定义
List 分区不是比较大小,而是将分区键值映射到一个离散集合。
例如:
CREATE TABLE customer_profile (
customer_id NUMBER NOT NULL,
region VARCHAR2(10) NOT NULL,
name VARCHAR2(100) NOT NULL
)
PARTITION BY LIST (region)
(
PARTITION p_cn VALUES ('CN'),
PARTITION p_us VALUES ('US'),
PARTITION p_eu VALUES ('DE', 'FR', 'GB'),
PARTITION p_other VALUES (DEFAULT)
);
其集合关系为:
p_cn: region ∈ {'CN'}
p_us: region ∈ {'US'}
p_eu: region ∈ {'DE', 'FR', 'GB'}
p_other: region 不属于上述集合
List 分区适合:
- 国家或区域;
- 业务类型;
- 状态;
- 产品线;
- 租户组;
- 有限且有业务含义的枚举值。
3.2 DEFAULT 分区的作用与限制
DEFAULT 分区可以接收尚未显式列出的值:
PARTITION p_other VALUES (DEFAULT)
它不是“自动创建一个新分区”,而是将所有未匹配值放入同一个分区。
例如插入:
INSERT INTO customer_profile
VALUES (1, 'CN', 'Alice');
INSERT INTO customer_profile
VALUES (2, 'JP', 'Bob');
结果:
'CN' -> p_cn
'JP' -> p_other
如果以后要增加日本分区:
ALTER TABLE customer_profile
SPLIT PARTITION p_other
VALUES ('JP')
INTO (
PARTITION p_jp,
PARTITION p_other
);
执行前需要确认 p_other 中确实存在可被拆分的数据,并评估:
- 拆分是否需要扫描数据;
- 分区索引和全局索引如何维护;
- DDL 锁等待;
- 业务高峰期是否会产生明显影响。
如果不需要保留 DEFAULT 分区,可以将所有值拆入新分区;如果仍要接收其他值,则必须保留剩余分区。
3.3 NULL 值
分区键允许为空时,必须明确考虑 NULL 的归属:
- Range 分区中,空值在排序意义上通常会进入最小边界分区;
- List 分区可以显式列出
NULL,也可以由DEFAULT接收; - Hash 分区会对包括空值在内的键值执行哈希分布。
工程上更推荐对作为分区键的业务字段使用 NOT NULL。否则,空值集中到某个 Range 或 List 分区,可能与业务对数据生命周期的预期不一致。
3.4 List 的反例
List 分区不适合高基数字段。例如用用户 ID 做 List 分区:
p_1 VALUES (1)
p_2 VALUES (2)
p_3 VALUES (3)
...
这会导致:
- 分区数量随用户增长;
- DDL、统计信息和字典管理成本增加;
- 分区裁剪收益并不一定高于普通索引;
- 分区维护和权限管理变得困难。
对于高基数、需要均匀分散数据的键,Hash 通常更合适。
四、Hash 分区:按哈希值均匀分布数据
4.1 Hash 的定义
Hash 分区先对分区键计算内部哈希值,再根据分区数量选择目标分区:
其中:
- 是 Oracle 使用的哈希映射;
- 是分区数量;
key是分区键。
实际分区归属使用 Oracle 内部语义决定,应用不应自行复制这个哈希算法。
示例:
CREATE TABLE access_log (
log_id NUMBER NOT NULL,
user_id NUMBER NOT NULL,
access_time TIMESTAMP NOT NULL,
operation VARCHAR2(30) NOT NULL
)
PARTITION BY HASH (user_id)
PARTITIONS 8;
Hash 分区的主要目标是均匀分布,而不是表达时间顺序或业务集合。
适合的场景包括:
- 按高基数 ID 分散热点;
- 让并行访问尽量分布到多个分区;
- 为分区级并行处理或分区级连接提供组织基础;
- 没有自然范围边界,但希望避免单个数据段过大的场景。
4.2 Hash 不等于分片
Hash 分区仍然位于同一个 Oracle 数据库逻辑实例和数据库存储体系中。它不是:
- 跨数据库节点的分片;
- 自动跨服务器的数据分布;
- 自动解决跨分区事务的机制;
- 自动解决热点索引块的机制。
如果所有插入都集中更新同一个全局索引根或某个业务热点,即使表做了 Hash 分区,也可能仍存在竞争。
4.3 Hash 裁剪的边界
对 Hash 分区表执行:
SELECT *
FROM access_log
WHERE user_id = :user_id;
优化器通常可以根据等值条件确定需要访问的 Hash 分区,或者将访问范围缩小到少数分区。
但下面的范围条件:
SELECT *
FROM access_log
WHERE user_id BETWEEN :a AND :b;
通常无法像 Range 分区那样通过数值连续性排除大量分区。因为数值相邻的 user_id 不代表哈希值相邻。
因此:
Range + 时间条件 -> 天然适合按时间裁剪
Hash + 等值条件 -> 可能精确定位分区
Hash + 数值范围条件 -> 通常难以裁剪
Hash 分区的收益主要是均匀分布和等值访问,而不是范围查询。
4.4 Hash 分区数量的考虑
分区数量不是越多越好:
- 太少:单个分区仍然很大,平行度和维护粒度有限;
- 太多:数据字典、统计信息、分区索引和维护操作成本增加;
- 数据量增长后调整分区数量可能需要拆分、合并或重组;
- 现有数据分布不均时,增加分区不一定立即修复历史倾斜。
应根据数据规模、并发模式、维护操作和目标版本能力确定,而不是仅根据 CPU 核数机械设置。
五、组合分区:先按业务边界,再按分布方式拆分
Range、List 和 Hash 可以组合使用。例如先按月份划分,再在每个月内部按客户 ID Hash:
CREATE TABLE monthly_orders (
order_id NUMBER NOT NULL,
order_date DATE NOT NULL,
customer_id NUMBER NOT NULL,
amount NUMBER(18, 2) NOT NULL
)
PARTITION BY RANGE (order_date)
SUBPARTITION BY HASH (customer_id)
SUBPARTITIONS 4
(
PARTITION p2024_01 VALUES LESS THAN (DATE '2024-02-01'),
PARTITION p2024_02 VALUES LESS THAN (DATE '2024-03-01'),
PARTITION p2024_03 VALUES LESS THAN (DATE '2024-04-01'),
PARTITION pmax VALUES LESS THAN (MAXVALUE)
);
这形成两层组织:
一级分区:按 order_date 选择月份
二级分区:在该月份内部按 customer_id 分散
例如:
WHERE order_date >= DATE '2024-02-01'
AND order_date < DATE '2024-03-01'
AND customer_id = :id
优化器可能先裁剪到 p2024_02,再在其子分区中定位 Hash 分区。
组合分区增加了维护复杂度:
- 每个一级分区都包含多个子分区;
- 本地索引可能需要保持相同的分区层级;
- 统计信息和分区级操作更加复杂;
- 分区数量是一级分区数乘以子分区数。
组合分区只有在两个维度都具有明确收益时才值得采用。
六、分区裁剪:优化器为什么能够跳过分区
6.1 裁剪的形式化条件
设分区表被划分为:
查询谓词定义了候选数据集合 。如果能够证明:
则分区 不可能产生结果,可以跳过。
对于 Range 分区:
P1: [2024-01-01, 2024-02-01)
P2: [2024-02-01, 2024-03-01)
P3: [2024-03-01, 2024-04-01)
查询:
WHERE order_date >= DATE '2024-02-10'
AND order_date < DATE '2024-02-20'
候选集合为:
[2024-02-10, 2024-02-20)
与 P1、P3 的交集为空,因此只需访问 P2。
这就是分区裁剪的逻辑基础,而不是“分区表自动更快”。
6.2 静态裁剪与动态裁剪
静态裁剪在生成执行计划时就能确定分区。例如:
SELECT COUNT(*)
FROM orders
WHERE order_date >= DATE '2024-04-01'
AND order_date < DATE '2024-07-01';
边界是常量,优化器可以在计划阶段确定目标分区。
动态裁剪在执行过程中才能确定分区。例如:
SELECT o.order_id
FROM orders o
JOIN customer_filter f
ON f.customer_id = o.customer_id
WHERE o.order_date >= :start_date
AND o.order_date < :end_date;
目标分区可能依赖绑定变量或连接输入。执行计划中可能出现动态分区访问边界,具体表现依赖优化器版本、谓词形式和统计信息。
6.3 查看裁剪结果
先收集统计信息:
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(
ownname => USER,
tabname => 'ORDERS',
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
method_opt => 'FOR ALL COLUMNS SIZE AUTO'
);
END;
/
然后查看计划:
EXPLAIN PLAN FOR
SELECT COUNT(*)
FROM orders
WHERE order_date >= DATE '2024-04-01'
AND order_date < DATE '2024-07-01';
SELECT *
FROM TABLE(DBMS_XPLAN.DISPLAY);
重点观察计划中的分区信息,例如:
Pstart Pstop
------ -----
2 2
或者:
Pstart Pstop
------ -----
KEY KEY
一般可以这样理解:
Pstart = Pstop = 2:只访问一个分区;Pstart = 2, Pstop = 4:访问一个连续分区范围;KEY:分区边界在执行期间根据值决定;1, 1048575或类似全范围信息:可能表示访问所有分区,具体显示取决于版本和计划格式。
仅看到索引扫描并不代表发生了有效裁剪,必须同时检查 PSTART 和 PSTOP。
6.4 使裁剪失效或变弱的写法
下面的写法可能妨碍基于 order_date 的裁剪:
WHERE TRUNC(order_date) = DATE '2024-04-01'
更适合改写为:
WHERE order_date >= DATE '2024-04-01'
AND order_date < DATE '2024-04-02'
另一个常见问题是隐式类型转换:
WHERE order_date = '2024-04-01'
这依赖会话的 NLS_DATE_FORMAT,可能产生错误或隐式函数转换。应使用日期字面量或显式转换:
WHERE order_date = DATE '2024-04-01'
或者:
WHERE order_date = TO_DATE(
:date_text,
'YYYY-MM-DD',
'NLS_DATE_LANGUAGE=American'
)
但即使使用了函数,如果函数作用在绑定变量而不是分区键列上,通常更容易保持可裁剪形式。关键原则是:不要无必要地把分区键包在表达式中。
6.5 OR、绑定变量与裁剪
下面的查询:
WHERE order_date < DATE '2024-04-01'
OR order_date >= DATE '2024-10-01'
逻辑上只需要访问早期分区和晚期分区,但最终计划是否能精确表达这种裁剪,取决于优化器改写、统计信息和谓词复杂度。
绑定变量本身并不必然阻止裁剪。Oracle 可以在执行阶段使用绑定值完成动态裁剪,但如果应用使用了复杂表达式、隐式转换,或者计划被固定为不适合当前数据分布的形式,裁剪效果可能下降。
因此诊断时应查看真实执行计划,而不是只看 SQL 文本。
七、分区与索引:本地索引、全局索引和可用性
7.1 本地索引
本地索引(local index)按表分区建立索引分区:
CREATE INDEX orders_lix_customer
ON orders (customer_id)
LOCAL;
每个表分区都有对应的索引分区。其重要特征是索引分区与表分区具有明确的对应关系。
优点:
- 删除或截断一个表分区时,相关索引维护边界清晰;
- 索引分区通常容易与表分区一起管理;
- 分区生命周期管理更自然;
- 适合按时间删除历史数据的表。
缺点:
- 不能直接把所有分区中的索引键组织成一个全局有序结构;
- 某些跨分区排序、唯一性和访问模式不一定适合本地索引;
- 本地唯一索引通常要求包含分区键,否则无法在单个索引分区内保证全表唯一性。
例如:
CREATE UNIQUE INDEX orders_lux_order_id
ON orders (order_id)
LOCAL;
是否可行,需要结合分区键和 Oracle 对本地唯一索引的约束规则验证。常见做法是:
CREATE UNIQUE INDEX orders_lux_order_id_date
ON orders (order_id, order_date)
LOCAL;
但这改变了索引键的唯一性语义:它保证的是 (order_id, order_date) 的唯一,而不一定是 order_id 单列全局唯一。
7.2 全局索引
全局索引(global index)可以跨越多个表分区:
CREATE INDEX orders_gix_customer
ON orders (customer_id);
全局索引适合:
- 频繁按非分区键全局查找;
- 需要一个跨分区的统一索引结构;
- 某些无法或不适合使用本地索引的唯一性约束。
但分区维护时,全局索引是重要风险点。比如:
ALTER TABLE orders
TRUNCATE PARTITION p2024q1;
如果直接移除分区数据,相关全局索引条目可能失效或需要维护,具体行为取决于操作、索引类型和使用的子句。生产操作应明确选择并验证 UPDATE GLOBAL INDEXES 等维护选项:
ALTER TABLE orders
TRUNCATE PARTITION p2024q1
UPDATE GLOBAL INDEXES;
代价是截断操作可能需要更多索引维护工作。另一种策略是允许全局索引暂时不可用,再重建:
ALTER INDEX orders_gix_customer REBUILD;
但这会引入一个索引不可用窗口,并可能需要额外空间和较长 I/O 时间。
7.3 分区表不等于自动使用索引
下面两个问题必须分开:
- 查询是否裁剪了分区;
- 每个目标分区内部是否使用了索引。
一个查询可以:
只访问一个分区,但在该分区全表扫描;
访问多个分区,但每个分区使用索引;
访问所有分区,并在每个分区使用索引。
是否高效取决于:
- 过滤选择性;
- 分区大小;
- 索引聚簇因子;
- 统计信息;
- 访问列是否覆盖;
- 并行度;
- 实际数据分布。
因此不能用“看到分区表”或“看到索引扫描”替代执行计划分析。
八、分区交换:用元数据操作完成大批量迁移
8.1 交换的核心语义
分区交换(partition exchange)将一个分区与一张普通表交换数据段和相关元数据的归属。
先创建结构相容的中间表:
CREATE TABLE orders_stage
AS
SELECT *
FROM orders
WHERE 1 = 0;
然后装载数据:
INSERT INTO orders_stage (
order_id,
order_date,
customer_id,
region,
amount
)
SELECT
order_id,
order_date,
customer_id,
region,
amount
FROM source_orders
WHERE order_date >= DATE '2024-04-01'
AND order_date < DATE '2024-07-01';
COMMIT;
执行交换:
ALTER TABLE orders
EXCHANGE PARTITION p2024q2
WITH TABLE orders_stage
INCLUDING INDEXES
WITH VALIDATION;
交换后:
orders.p2024q2 得到原 orders_stage 中的数据
orders_stage 得到原 p2024q2 中的数据
orders_stage 原本为空,因此交换后它会包含原来 p2024q2 的数据。此时可以:
- 对
orders_stage做校验; - 导出或归档;
- 删除或重新利用;
- 对外部数据执行后续处理。
交换的价值在于:数据不必通过 INSERT INTO orders SELECT ... 再复制一次。对于满足结构和约束条件的场景,交换主要改变数据段和元数据归属,通常比逐行迁移更适合大批量装载和归档。
8.2 WITH VALIDATION 与 WITHOUT VALIDATION
WITH VALIDATION 会验证中间表中的数据是否属于目标分区。
例如目标分区是:
2024-04-01 <= order_date < 2024-07-01
如果 orders_stage 中存在:
order_date = 2024-08-01
验证就应失败,因为这行不属于 p2024q2。
验证的优点是保护分区约束,缺点是可能需要扫描中间表,在大批量数据上产生明显开销。
WITHOUT VALIDATION 则跳过这一步:
ALTER TABLE orders
EXCHANGE PARTITION p2024q2
WITH TABLE orders_stage
INCLUDING INDEXES
WITHOUT VALIDATION;
它不是“更快且等价”的验证方式,而是把正确性责任交给操作者。只有在已经通过独立方法证明以下条件时才安全:
SELECT COUNT(*)
FROM orders_stage
WHERE order_date < DATE '2024-04-01'
OR order_date >= DATE '2024-07-01';
结果必须为:
0
还应检查空值:
SELECT COUNT(*)
FROM orders_stage
WHERE order_date IS NULL;
如果分区键不允许为空,则应为 0。
如果错误地使用 WITHOUT VALIDATION:
错误数据进入错误分区
↓
后续查询按分区裁剪
↓
本应返回的行被跳过
↓
结果错误,而且错误可能很难从普通 SQL 中发现
因此交换验证是数据正确性问题,不只是性能问题。
8.3 交换的结构前提
中间表通常需要与目标表具有相容结构,包括:
- 列数和列顺序;
- 数据类型和长度;
- 相关虚拟列、隐藏列等;
- 分区键对应列;
- 约束语义;
- 索引结构,尤其是使用
INCLUDING INDEXES时; - 压缩、LOB、分区和存储属性等。
不能仅凭:
CREATE TABLE orders_stage AS SELECT * FROM orders WHERE 1 = 0;
就假设所有生产属性已经完整复制。CTAS 常常不会复制所有索引、约束、触发器、权限和存储属性。交换前应通过数据字典比对:
SELECT table_name, column_id, column_name, data_type,
data_length, nullable
FROM user_tab_columns
WHERE table_name IN ('ORDERS', 'ORDERS_STAGE')
ORDER BY table_name, column_id;
索引状态也要检查:
SELECT index_name, table_name, status, locality
FROM user_indexes
WHERE table_name IN ('ORDERS', 'ORDERS_STAGE');
如果中间表的索引、约束或统计信息不符合预期,交换可能失败,或交换成功后产生维护与性能问题。
8.4 交换与索引
INCLUDING INDEXES 表示同时处理相关索引结构,但它并不意味着所有索引都可以无条件交换。索引必须满足 Oracle 对结构、分区方式和状态的要求。
交换前应检查:
- 中间表是否建立了需要交换的索引;
- 索引列定义是否相容;
- 本地索引分区是否可对应;
- 全局索引是否会被标记不可用;
- 是否需要使用全局索引维护选项;
- 交换后两张表的索引状态是否符合预期。
一个稳妥的验证动作是:
SELECT index_name, status
FROM user_indexes
WHERE table_name = 'ORDERS';
SELECT index_name, partition_name, status
FROM user_ind_partitions
WHERE index_name IN (
SELECT index_name
FROM user_indexes
WHERE table_name = 'ORDERS'
);
实际列名和可查询视图需要根据目标 Oracle 版本、索引类型和对象权限确认。
8.5 交换不是普通事务内的可回滚更新
交换是 DDL。Oracle 执行 DDL 时会发生隐式提交:
提交当前事务
执行 DDL
DDL 完成后再次提交
因此不能这样理解:
BEGIN
INSERT INTO orders_stage ...;
ALTER TABLE orders EXCHANGE PARTITION ...;
ROLLBACK;
END;
/
ROLLBACK 不能把已经完成的 DDL 当作普通 DML 一起回滚。装载阶段可以独立提交,交换阶段需要按照可恢复发布设计执行。
如果交换后发现数据错误,恢复通常不是对原 DDL 执行 ROLLBACK,而是:
- 确认目标分区和中间表当前内容;
- 修复或重建中间表;
- 在正确的锁和校验条件下执行反向交换,或重新装载;
- 必要时从备份、闪回或其他恢复机制处理。
8.6 交换期间的并发和锁
交换虽然避免了逐行复制,但不是没有锁风险。它需要修改表和分区的字典元数据,并可能与以下操作冲突:
- 对目标表的 DML;
- 长事务;
- 查询持有的相关锁;
- 索引维护;
- 统计信息收集;
- 并发分区维护操作。
生产执行前应设置明确的锁等待策略,例如:
ALTER SESSION SET DDL_LOCK_TIMEOUT = 60;
这表示 DDL 等待相关 DDL 锁最多约 60 秒,具体等待行为仍取决于锁类型和会话状态。
验证锁和长事务可以从数据字典、动态性能视图或企业监控系统入手。出现等待时,需要区分:
DDL 正在等待业务事务结束
业务事务正在等待 DDL
DDL 已完成但后续索引维护仍在运行
不要把“交换是元数据操作”误解为“不会阻塞任何业务”。
九、分区级维护:删除、截断、拆分、合并和移动
9.1 TRUNCATE PARTITION 与 DROP PARTITION
删除历史数据时常见两种操作。
截断分区:
ALTER TABLE orders
TRUNCATE PARTITION p2024q1
UPDATE GLOBAL INDEXES;
特点:
- 删除分区中的数据;
- 保留分区定义;
- 适合分区仍将继续使用的场景;
- 不是普通 DML,不能按普通事务回滚;
- 全局索引需要单独评估。
删除分区:
ALTER TABLE orders
DROP PARTITION p2024q1
UPDATE GLOBAL INDEXES;
特点:
- 删除数据;
- 同时删除该分区定义;
- 适合永久移除历史范围;
- 可能影响分区边界连续性和后续装载策略。
如果删除的是包含唯一性约束或关联关系的重要分区,还要检查:
- 外键约束;
- 全局索引;
- 本地索引;
- 物化视图;
- 统计信息;
- 备份和归档要求。
9.2 SPLIT PARTITION
List 的 DEFAULT 分区或 Range 的宽分区都可能需要拆分。
Range 示例:
ALTER TABLE orders
SPLIT PARTITION pmax
AT (DATE '2025-04-01')
INTO (
PARTITION p2025q1,
PARTITION pmax
);
原来的 pmax 被拆为:
p2025q1: order_date < 2025-04-01 且属于原 pmax 范围
pmax: order_date >= 2025-04-01
拆分通常需要处理现有数据、索引和统计信息,不应简单等同于修改一个边界数字。
9.3 MERGE PARTITIONS
如果两个相邻 Range 分区不再需要独立维护,可以合并:
ALTER TABLE orders
MERGE PARTITIONS p2024q3, p2024q4
INTO PARTITION p2024_h2;
合并可能需要重组数据和索引,执行时间取决于数据量、索引和存储条件。它适合减少过度细分的分区,不适合作为高频业务操作。
9.4 MOVE PARTITION
分区移动常用于:
- 迁移到其他表空间;
- 改变压缩属性;
- 进行存储重组;
- 处理空间碎片。
示意:
ALTER TABLE orders
MOVE PARTITION p2024q2
TABLESPACE users;
移动会影响相关索引状态。执行后必须检查:
SELECT index_name, status
FROM user_indexes
WHERE table_name = 'ORDERS';
以及索引分区状态。某些版本和具体操作支持在线移动或在线维护能力,但是否可用、对哪些索引和对象生效,必须以目标版本官方文档和实际测试为准,不能把 ONLINE 当成所有场景都适用的无锁保证。
十、用分区交换治理大表迁移
10.1 为什么不直接 INSERT ... SELECT
如果将数亿行数据直接插入目标大表:
INSERT INTO orders
SELECT ...
FROM source_orders;
可能带来:
- 大量重做和撤销;
- 长事务;
- 长时间持有业务资源;
- 索引逐行维护;
- 失败后重试成本高;
- 归档、校验和切换边界难以控制。
分区交换适合将“批量准备”和“短时间切换”分开:
阶段 1:在中间表准备数据
阶段 2:独立校验数据
阶段 3:短暂执行交换
阶段 4:验证目标分区和索引状态
阶段 5:清理或归档旧数据
这与 Expand-Contract 的思想一致:先扩展出可并行准备的结构,再进行受控切换,最后收缩旧路径。
10.2 一个完整的迁移示例
假设目标是将 2024 年第二季度数据导入 orders.p2024q2。
第一步:确认目标边界
SELECT partition_name, high_value
FROM user_tab_partitions
WHERE table_name = 'ORDERS'
ORDER BY partition_position;
需要确认:
p2024q2 的边界确实是:
2024-04-01 <= order_date < 2024-07-01
HIGH_VALUE 在数据字典中通常以表达式形式保存,不能只依赖字符串比较,必要时应结合 DDL 和人工复核。
第二步:建立中间表
CREATE TABLE orders_stage
AS
SELECT *
FROM orders
WHERE 1 = 0;
然后补齐需要的约束和索引。比如:
CREATE INDEX orders_stage_customer_ix
ON orders_stage (customer_id);
如果使用 INCLUDING INDEXES,中间表索引结构必须与交换要求相容。
第三步:装载并提交
INSERT /*+ APPEND */ INTO orders_stage (
order_id,
order_date,
customer_id,
region,
amount
)
SELECT
order_id,
order_date,
customer_id,
region,
amount
FROM source_orders
WHERE order_date >= DATE '2024-04-01'
AND order_date < DATE '2024-07-01';
COMMIT;
APPEND 可能使用直接路径插入,减少部分传统插入开销,但它会影响并发、空间使用和事务行为。是否使用应根据装载窗口、日志模式、约束和触发器条件验证。
第四步:校验分区边界
SELECT COUNT(*) AS invalid_rows
FROM orders_stage
WHERE order_date < DATE '2024-04-01'
OR order_date >= DATE '2024-07-01';
预期:
INVALID_ROWS = 0
检查空值:
SELECT COUNT(*) AS null_key_rows
FROM orders_stage
WHERE order_date IS NULL;
如果目标表的分区键为 NOT NULL,预期同样为 0。
检查业务唯一性:
SELECT order_id, COUNT(*)
FROM orders_stage
GROUP BY order_id
HAVING COUNT(*) > 1;
如果目标表要求 order_id 全局唯一,还必须与目标表中其他分区的数据一起校验,而不能只检查中间表。
第五步:收集中间表统计信息
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(
ownname => USER,
tabname => 'ORDERS_STAGE',
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
method_opt => 'FOR ALL COLUMNS SIZE AUTO'
);
END;
/
交换后的统计信息是否自动继承、是否需要重新收集,取决于操作和对象配置。切换后应检查实际统计信息状态,不能假设装载前收集一次就永久正确。
第六步:执行交换
ALTER SESSION SET DDL_LOCK_TIMEOUT = 60;
ALTER TABLE orders
EXCHANGE PARTITION p2024q2
WITH TABLE orders_stage
INCLUDING INDEXES
WITH VALIDATION;
如果数据已经通过可靠的独立校验,并且锁窗口极短,可以评估 WITHOUT VALIDATION。但这必须是有证据的取舍,而不是默认配置。
第七步:验证切换结果
先检查行数:
SELECT COUNT(*)
FROM orders PARTITION (p2024q2);
SELECT COUNT(*)
FROM orders_stage;
其中:
orders.p2024q2 的行数
应等于交换前 orders_stage 的行数,而:
orders_stage 的行数
应等于交换前 orders.p2024q2 的行数。
再检查分区边界数据:
SELECT COUNT(*)
FROM orders PARTITION (p2024q2)
WHERE order_date < DATE '2024-04-01'
OR order_date >= DATE '2024-07-01';
预期为 0。
最后检查索引:
SELECT index_name, status
FROM user_indexes
WHERE table_name = 'ORDERS';
如果存在分区索引,还要查询索引分区状态。任何 UNUSABLE 都必须在允许的维护窗口内处理,并确认应用查询是否依赖该索引。
十一、分区裁剪与统计信息、执行计划的关系
分区边界提供了“可能访问哪些分区”的结构信息,但优化器仍需要决定:
- 是否使用分区裁剪;
- 在目标分区中使用索引还是全表扫描;
- 是否并行;
- 是否进行分区级连接;
- 是否使用全局索引;
- 是否采用连接顺序和连接算法。
统计信息错误时,可能出现这种计划:
实际只返回几十行
优化器估计返回数百万行
于是选择全表扫描或错误的连接顺序
或者:
谓词可以裁剪到一个分区
但计划选择了全局索引
导致跨多个分区访问索引和表
诊断时应同时查看:
SELECT *
FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(
NULL,
NULL,
'ALLSTATS LAST +PARTITION +PREDICATE'
));
关注:
PSTART、PSTOP:实际或计划访问的分区范围;E-Rows:估算行数;A-Rows:实际行数;Predicate Information:谓词是否作用于分区键;- 是否出现全分区访问;
- 是否存在估算与实际的数量级偏差。
EXPLAIN PLAN 只表示优化器预生成的计划,不一定等于真实执行计划。绑定变量、动态采样、自适应行为和运行时信息都可能造成差异。
分区统计信息还涉及两个层次:
分区级统计信息:各分区内部的数据分布
全局统计信息:整张表的总体分布
只更新某一个新分区的统计信息,并不自动意味着全局统计信息已经准确。对于持续增加的时间分区,应根据数据变化和目标版本能力评估分区级、全局级以及增量统计信息策略。
十二、常见误解和失败表现
12.1 “分区表一定比普通表快”
错误。
没有分区键谓词的查询可能访问所有分区:
SELECT SUM(amount)
FROM orders;
这类全表聚合通常需要处理所有分区。分区可能有利于并行和分区级访问,但不会凭空减少需要读取的数据。
12.2 “有分区键条件就一定发生裁剪”
也错误。
下面的查询虽然逻辑上有日期条件,但表达式可能使优化器难以识别边界:
WHERE TO_CHAR(order_date, 'YYYY-MM-DD') = :day
应尽量改成原始列的范围条件:
WHERE order_date >= TO_DATE(:day, 'YYYY-MM-DD')
AND order_date < TO_DATE(:day, 'YYYY-MM-DD') + 1;
同时需要检查实际计划,而不是仅凭 SQL 形式判断。
12.3 “Hash 分区可以优化所有范围查询”
不能。
Hash 破坏了业务值的顺序关系。user_id 为 100 和 101,不意味着它们会相邻存放。Hash 更适合等值定位和均匀分布。
12.4 “交换一定是瞬时且无阻塞”
不准确。
交换避免了大批量逐行复制,但仍需:
- 获取 DDL 相关锁;
- 检查结构;
- 可能验证数据;
- 处理本地和全局索引;
- 产生数据字典变更;
- 遵守 DDL 隐式提交语义。
WITH VALIDATION 尤其可能扫描中间表。
12.5 “WITHOUT VALIDATION 只是跳过一个多余检查”
这是危险误解。
WITHOUT VALIDATION 可能让不属于目标范围的行进入分区。分区裁剪会依据分区边界跳过该分区,从而产生逻辑错误结果。
它只能建立在外部校验已经证明分区约束成立的前提上。
12.6 “删除分区后索引自然没问题”
需要分情况:
- 本地索引通常更容易随分区维护;
- 全局索引可能失效或需要维护;
- 分区索引本身也可能处于
UNUSABLE; - 唯一性和外键约束可能阻止操作或改变风险。
每次分区维护后都应检查索引状态和关键查询计划。
十三、生产治理中的边界与取舍
13.1 分区键应服务于主要维护动作
如果业务每月归档一次,按月或按日进行 Range 分区通常有清晰边界:
新数据进入当前分区
历史数据进入已关闭分区
归档通过交换或分区级操作完成
过期数据通过 DROP/TRUNCATE PARTITION 清理
如果业务没有自然的时间生命周期,却频繁按用户 ID 等值访问,Hash 或普通索引可能更符合需求。
13.2 分区数量需要受控
分区越多,管理对象越多。需要考虑:
- 字典视图查询;
- 统计信息收集;
- 本地索引分区数量;
- 分区级备份和恢复;
- DDL 编排;
- 监控和告警;
- SQL 计划复杂度。
按天分区并不天然比按月分区好。只有当按天维护、裁剪或归档确实需要更细粒度时,才应承担额外成本。
13.3 维护操作必须可验证
一次分区治理操作至少需要验证:
结构:分区边界、表属性、索引属性
数据:行数、边界、空值、重复值
计划:PSTART/PSTOP、估算与实际行数
并发:DDL 锁、长事务、业务延迟
恢复:失败后如何恢复或反向交换
特别是以下操作不能仅通过“命令执行成功”判断成功:
EXCHANGE PARTITIONSPLIT PARTITIONMERGE PARTITIONSMOVE PARTITIONDROP/TRUNCATE PARTITION
命令成功只代表 DDL 被 Oracle 接受,不代表业务数据、索引状态和查询计划都符合预期。
十四、Oracle 与 SQLite 的边界
Oracle 原生提供表分区、分区索引、分区交换和分区裁剪等能力。
SQLite 没有 Oracle 意义上的原生分区表体系。常见模拟方式是:
- 按月份建立多张普通表;
- 使用
VIEW合并查询; - 使用触发器将写入路由到不同表;
- 由应用或迁移脚本维护表集合。
但这种模拟不会自动获得 Oracle 的完整语义:
没有统一的原生分区元数据
没有 Oracle 式 EXCHANGE PARTITION
没有同等的优化器分区裁剪机制
没有相同的本地/全局索引模型
DDL、事务和锁行为也不同
因此,从 Oracle 分区表迁移到 SQLite 时,不能直接把 PARTITION BY RANGE/LIST/HASH 语句机械翻译过去。需要重新设计表、视图、触发器、迁移流程和查询路由,并明确 SQLite 的单库事务和锁边界。
分区治理的核心不是记住几条 ALTER TABLE 语句,而是建立一条可验证的数据组织链路:
分区键定义边界
↓
行被路由到唯一分区
↓
谓词决定可排除的分区
↓
索引和统计信息决定分区内部访问路径
↓
交换和分区级 DDL 完成批量迁移与生命周期管理
↓
通过数据、计划、锁和恢复验证治理结果
只有当这些环节同时成立时,分区才真正成为大表治理工具,而不只是表定义中的一个选项。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:Oracle 锁与隔离:行锁、ITL、读一致性、死锁和诊断
- 下一篇:Oracle 物化视图与查询重写:刷新、日志、一致性和性能
- 延伸:Oracle 索引与优化器:统计信息、执行计划、Hint 和 SQL 调优
- 延伸:数据库迁移与在线变更:Expand-Contract、锁风险和可回滚发布
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论