PostgreSQL分区表的演进与选型
PostgreSQL从10版本引入声明式分区(Declarative Partitioning),12版本完成功能成熟,支持范围分区、列表分区、哈希分区三种策略。对于单表数据量超过千万行的场景,分区表是提升查询性能和运维效率的核心手段——分区裁剪(Partition Pruning)让查询只扫描相关分区,VACUUM操作可以按分区独立执行,历史数据归档只需DETACH分区而非DELETE。
三种分区策略的适用场景:范围分区(RANGE)适合时间序列数据,如按月/季度分区;列表分区(LIST)适合枚举值分布,如按地区、状态分区;哈希分区(HASH)适合无明显规律的均匀分布数据。选择错误策略会导致数据倾斜或分区裁剪失效。
范围分区的完整实现
以订单表为例,按月范围分区:
-- 创建分区主表
CREATE TABLE orders (
id BIGSERIAL,
user_id BIGINT NOT NULL,
order_no VARCHAR(32) NOT NULL,
amount NUMERIC(12,2),
status SMALLINT DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);
-- 创建月分区
CREATE TABLE orders_2026_01 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_2026_02 PARTITION OF orders
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
-- 默认分区兜底
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
-- 在主表上创建索引(自动传播到所有分区)
CREATE INDEX idx_orders_user_id ON orders (user_id);
CREATE INDEX idx_orders_order_no ON orders (order_no);
范围分区的设计要点:分区键必须包含在唯一约束中,所以分区表的主键必须是(PARTITION KEY, ID)组合;默认分区(DEFAULT)必须有,否则插入超出范围的数据会报错;分区边界是左闭右开区间,即[2026-01-01, 2026-02-01)。
自动创建分区的实践方案
手动创建分区不可持续,需要自动化机制。PostgreSQL 14+的pg_partman扩展是最成熟的方案:
-- 安装pg_partman
CREATE EXTENSION pg_partman;
-- 配置自动分区
SELECT partman.create_parent(
p_parent_table := 'public.orders',
p_control := 'created_at',
p_type := 'range',
p_interval := '1 month',
p_premake := 3, -- 预创建未来3个月分区
p_retention := '24 months', -- 保留24个月
p_retention_schema := 'archive' -- 归档到archive schema
);
-- 启用后台自动维护(需pg_partman后台worker)
UPDATE partman.part_config
SET
infinite_time_partitions = true,
automatic_maintenance = 'on'
WHERE parent_table = 'public.orders';
pg_partman会定期检查是否需要创建新分区、归档过期分区。p_premake=3表示预创建未来3个月分区,避免数据插入时找不到对应分区。p_retention配置保留策略,过期分区自动DETACH并移动到归档schema。
分区裁剪与查询优化
分区裁剪是分区表性能优化的核心。EXPLAIN中看到Partition Removed表示裁剪生效:
-- 带分区键的查询:触发裁剪
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE created_at >= '2026-07-01'
AND created_at < '2026-08-01'
AND user_id = 10086;
-- 执行计划输出:
-- Append
-- -> Seq Scan on orders_2026_07
-- Filter: (created_at >= '2026-07-01' AND ...)
-- Rows Removed by Filter: 0 (只扫描了7月分区)
-- 不带分区键的查询:全分区扫描
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 10086;
-- 执行计划输出:
-- Append
-- -> Seq Scan on orders_2026_01 ...
-- -> Seq Scan on orders_2026_02 ...
-- ... (扫描所有分区)
关键优化规则:查询条件必须包含分区键才能触发裁剪;分区键上的条件应使用简单比较运算符,避免函数包装(如WHERE date_trunc(‘month’, created_at) = …不会触发裁剪);多列分区键需按顺序提供条件。
哈希分区的适用场景与配置
当数据没有自然的范围或列表分区键时,哈希分区是唯一选择:
CREATE TABLE user_events (
id BIGSERIAL,
user_id BIGINT NOT NULL,
event_type VARCHAR(32),
payload JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
) PARTITION BY HASH (user_id);
-- 创建8个哈希分区
CREATE TABLE user_events_p0 PARTITION OF user_events
FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE user_events_p1 PARTITION OF user_events
FOR VALUES WITH (MODULUS 8, REMAINDER 1);
-- ... p2-p7
哈希分区的分区数建议取2的幂次(4/8/16/32),方便后续扩容时分裂分区。MODULUS和REMAINDER的数学关系保证数据均匀分布。哈希分区不支持范围裁剪,但等值查询(WHERE user_id = ?)能精确路由到单个分区。
分区表运维方面:ALTER TABLE在主表上的操作会传播到所有分区,大表加列需要注意锁表时间;DETACH PARTITION CONCURRENTLY在PG14+可用,不阻塞查询;跨分区UPDATE会先DELETE再INSERT,可能触发意外行为,设计分区键时避免对分区键列做UPDATE操作。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/postgresql-fen-qu-biao-ce-lyue-xuan-ze-yu-hai-liang-shu-ju/