范围分区把边界定义变成写入路由规则。PostgreSQL 按键值把行放到能够接受它的分区,无论这条数据准时到达,还是晚了几个月。任何查询获得裁剪收益之前,都需要先定义正确边界、使用匹配的时间类型,并安排缺失范围的处理流程。分区建表成功,只是这些条件中的一部分。
使用相邻的左闭右开范围
范围分区包含下界,不包含上界。九月可以从九月一日开始,一直到十月一日之前;十月正好从同一边界开始,双方不会重叠。不要靠猜测一个月最后可表示的时间戳来定义范围。相邻的左闭右开区间,不依赖时间字段究竟保留多少位小数,也能避免边界遗漏。
查询同样需要准确表达区间。大于等于下界、小于上界的条件,适合描述一个月的数据。BETWEEN 包含两个端点,因此,把下月第一瞬间写成其上界,可能把下个月的数据也查进来。分区路由只能执行数据布局规则,不能替一个请求了错误区间的条件修正业务含义。
分区键类型与时区也是定义的一部分。timestamptz 代表瞬间,不带时区的 timestamp 则代表没有对应时区解释的本地日期时间。示例为每个边界显式写出 UTC 偏移。如果业务月份按另一个时区划分,应根据那个时区的规则推导边界,而不是假定所有本地日期都对应相同的实际时长。
缺失范围是一种运维状态
没有分区接受新键值时,插入会报错,除非 DEFAULT 分区能够接住它。日历进入新月份,并不会让 PostgreSQL 自动创建下一张月表。应在写入需要之前创建范围,同时考虑迟到数据和旧日期修正。输入键不必单调增长,路由照样能够工作;关键是相应目标范围仍然存在。
DEFAULT 可以接收显式范围之外的值,减少立即发生的插入失败,但如果不检查默认表,也会掩盖分区管理遗漏。持续增长的默认分区可能混入多个互不相关的时间范围,让后续整理更困难。应让“有意保留的例外数据”与“忘记创建分区的日常数据”保持可辨认,不能把插入暂时不报错当成管理正常。
新增显式分区,会改变默认分区能够接受的值。如果新范围对应的行还留在 DEFAULT 中,PostgreSQL 会拒绝变更,而不会允许两处同时拥有这批数据。应先把这些行迁移到合适的暂存表或按规则处理,再挂载新分区。边界是被执行的数据规则,不是可以随意标错的说明标签。
坏掉的样子
下一月分区未创建时,部署可能恰好在跨月瞬间出现写入失败。补建分区又可能因为 DEFAULT 已经积累对应行而失败。这两种问题都属于路由和数据位置,不是规划器统计陈旧。应先检查被拒绝的键和当前边界,再决定操作,不能直接运行 ANALYZE 或改索引,希望它们修复缺失范围。
ATTACH 可能扫描候选表以验证边界,也可能检查默认分区中是否存在冲突数据。合适且已验证的约束能省去部分扫描,但操作仍需要锁,并要求表结构兼容。活跃表上迁移数据时,需要协调写入者和锁获取;一个隔离环境中的事务示例,不是完整的在线迁移操作方案。
是否允许空值,与范围选择是两回事。示例要求时间戳非空,避免缺失事件时间悄悄变成普通的例外数据。迁移后还应核对行数和标识;ATTACH 成功不能证明更早的复制步骤没有丢行或重复,数据核对不能省略。
快速验证
把下面内容保存为 partition-ranges.sql,在可丢弃的 PostgreSQL 18 数据库中执行 psql -X -v ON_ERROR_STOP=1 -f partition-ranges.sql。全部表都是临时表,整个实验最后回滚清除。
\set ON_ERROR_STOP on
BEGIN;
SET LOCAL timezone='UTC';
CREATE TEMP TABLE samples (at timestamptz NOT NULL,id integer) PARTITION BY RANGE(at);
CREATE TEMP TABLE samples_sep PARTITION OF samples
FOR VALUES FROM ('2026-09-01 00:00:00+00') TO ('2026-10-01 00:00:00+00');
INSERT INTO samples VALUES ('2026-09-01 00:00:00+00',1),('2026-09-30 23:59:59.999999+00',2);
DO $$ BEGIN
INSERT INTO samples VALUES ('2026-10-01 00:00:00+00',3);
RAISE EXCEPTION 'upper boundary entered September';
EXCEPTION WHEN check_violation THEN RAISE NOTICE 'upper boundary needs another partition';
END $$;
CREATE TEMP TABLE samples_default PARTITION OF samples DEFAULT;
INSERT INTO samples VALUES ('2026-10-01 00:00:00+00',3);
DO $$ BEGIN
CREATE TEMP TABLE samples_oct_bad PARTITION OF samples
FOR VALUES FROM ('2026-10-01 00:00:00+00') TO ('2026-11-01 00:00:00+00');
RAISE EXCEPTION 'new partition overlapped populated default';
EXCEPTION WHEN check_violation THEN RAISE NOTICE 'matching default rows must be moved first';
END $$;
CREATE TEMP TABLE samples_oct (LIKE samples INCLUDING ALL);
WITH moved AS (
DELETE FROM samples_default
WHERE at >= TIMESTAMPTZ '2026-10-01 00:00:00+00' AND at < TIMESTAMPTZ '2026-11-01 00:00:00+00'
RETURNING *
) INSERT INTO samples_oct SELECT * FROM moved;
ALTER TABLE samples ATTACH PARTITION samples_oct
FOR VALUES FROM ('2026-10-01 00:00:00+00') TO ('2026-11-01 00:00:00+00');
SELECT id,at,tableoid::regclass AS physical_table FROM samples ORDER BY id;
DO $$ BEGIN
IF (SELECT array_agg(id ORDER BY id) FROM samples_sep) IS DISTINCT FROM ARRAY[1,2] OR
(SELECT array_agg(id ORDER BY id) FROM samples_oct) IS DISTINCT FROM ARRAY[3] OR
(SELECT count(*) FROM samples_default) <> 0 THEN
RAISE EXCEPTION 'range migration lost or duplicated data';
END IF;
END $$;
ROLLBACK;
PostgreSQL 18.6 接受了九月的第一瞬间,以及测试中十月开始前的最后一个微秒。没有相应目标时,十月一日被拒绝;创建 DEFAULT 后,这一行进入默认分区。随后尝试新增十月分区,因为匹配行仍留在 DEFAULT 中而失败,说明系统确实检查数据归属,并没有仅仅更新元信息。
脚本用 DELETE RETURNING 把该行移到暂存表,再把暂存表挂载为十月分区,并通过 tableoid 展示每条记录的实际物理表。标识一和二仍在九月,标识三位于十月,DEFAULT 为空。断言检查最终成员关系和行数,同时覆盖边界包含规则与完整迁移路径。这个结果不能被解释为未来范围管理可以省略,只说明测试中的布局已经正确完成。
来源
- PostgreSQL 表分区,范围边界和路由行为,已核验 2026-09-10。
- PostgreSQL ALTER TABLE,挂载验证、默认分区与锁,已核验 2026-09-10。