PostgreSQL的表分区功能可以将大表在物理存储层面拆分为多个子表,查询时通过分区裁剪减少扫描范围。对于日志、交易记录等持续增长的大表,分区是控制单表数据量、提升查询性能的有效手段。
声明式分区与分区策略
PostgreSQL 10开始支持声明式分区(Declarative Partitioning),相比传统的继承分区,语法更简洁,优化器支持也更完善。分区策略包括Range分区、List分区和Hash分区三种。
-- Range分区:按时间范围分区,最常用于日志类数据
CREATE TABLE access_log (
id BIGSERIAL,
user_id INTEGER NOT NULL,
request_url TEXT NOT NULL,
status_code INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);
-- 按月创建分区
CREATE TABLE access_log_202601
PARTITION OF access_log
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE access_log_202602
PARTITION OF access_log
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE access_log_202603
PARTITION OF access_log
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- 创建默认分区,捕获未匹配任何分区的数据
CREATE TABLE access_log_default
PARTITION OF access_log DEFAULT;
-- List分区:按离散值分区,适合按区域、类别拆分
CREATE TABLE order_data (
id BIGSERIAL,
order_no TEXT NOT NULL,
region TEXT NOT NULL,
amount NUMERIC(12,2),
created_at TIMESTAMP NOT NULL
) PARTITION BY LIST (region);
CREATE TABLE order_data_north
PARTITION OF order_data
FOR VALUES IN ('beijing', 'tianjin', 'hebei');
CREATE TABLE order_data_south
PARTITION OF order_data
FOR VALUES IN ('guangdong', 'fujian', 'hainan');
-- Hash分区:均匀分布数据,适合无法按范围或列表划分的场景
CREATE TABLE user_data (
id BIGSERIAL,
user_code TEXT NOT NULL,
user_name TEXT NOT NULL,
extra JSONB
) PARTITION BY HASH (user_code);
CREATE TABLE user_data_0 PARTITION OF user_data
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE user_data_1 PARTITION OF user_data
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE user_data_2 PARTITION OF user_data
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE user_data_3 PARTITION OF user_data
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
分区裁剪与查询计划分析
分区表的核心价值在于分区裁剪(Partition Pruning):查询条件中包含分区键时,优化器自动跳过不相关的分区,减少IO扫描量。通过EXPLAIN可以验证分区裁剪是否生效。
-- 查询特定月份的数据,触发分区裁剪
EXPLAIN ANALYZE SELECT * FROM access_log
WHERE created_at >= '2026-02-01' AND created_at < '2026-03-01';
-- 执行计划应只扫描 access_log_202602 分区
-- "Append" 节点下只有 access_log_202602
-- 如果出现所有分区都被扫描,说明分区裁剪未生效
-- 常见导致裁剪失效的原因:分区键上使用了函数
-- 错误:对分区键做函数转换,无法裁剪
SELECT * FROM access_log
WHERE DATE(created_at) = '2026-02-15';
-- 不会触发分区裁剪,会扫描全部分区
-- 正确:直接比较分区键值
SELECT * FROM access_log
WHERE created_at >= '2026-02-15' AND created_at < '2026-02-16';
-- 正确触发分区裁剪
-- 验证分区裁剪效果
EXPLAIN SELECT * FROM access_log
WHERE created_at >= '2026-02-15' AND created_at < '2026-02-16';
-- Subplans Removed: 2 (裁剪掉了2个分区)
自动化分区管理
时间范围分区需要定期创建新分区,手工维护容易遗漏导致数据写入默认分区。通过pg_partman扩展可以实现自动创建和删除分区。
-- 安装pg_partman扩展
CREATE EXTENSION pg_partman;
-- 创建分区配置:按月自动分区,预创建3个月的分区
SELECT partman.create_parent(
p_parent_table := 'public.access_log',
p_control := 'created_at',
p_type := 'native',
p_interval := 'monthly',
p_premake := 3 -- 预创建未来3个月的分区
);
-- 设置自动删除旧分区:保留12个月数据
UPDATE partman.part_config
SET retention = '12 months',
retention_keep_table = false -- true=仅解除关联,false=删除分区
WHERE parent_table = 'public.access_log';
-- 设置定时任务自动执行维护(使用pg_cron扩展)
CREATE EXTENSION pg_cron;
-- 每天凌晨1点执行分区维护
SELECT cron.schedule(
'partman-maintenance',
'0 1 * * *',
$$SELECT partman.run_maintenance_proc()$$
);
-- 查看已创建的分区配置
SELECT parent_table, control, partition_interval,
premake, retention
FROM partman.part_config;
分区表上的索引策略
分区表支持在父表上创建索引,PostgreSQL会自动在所有子分区上创建对应索引。也可以在单个分区上创建独立索引,针对特定分区的查询做定向优化。
-- 在父表上创建索引,自动传播到所有分区
CREATE INDEX idx_access_log_user_id
ON access_log (user_id);
CREATE INDEX idx_access_log_created_at
ON access_log (created_at DESC);
-- 复合索引:分区键放最前面,配合分区裁剪效果最佳
CREATE INDEX idx_access_log_user_time
ON access_log (created_at, user_id);
-- 单独为热分区创建针对性索引
CREATE INDEX idx_access_log_202603_status
ON access_log_202603 (status_code)
WHERE status_code >= 400; -- 部分索引,只索引异常请求
-- 查看分区索引状态
SELECT
schemaname, tablename, indexname, indexdef
FROM pg_indexes
WHERE tablename LIKE 'access_log%'
ORDER BY tablename, indexname;
-- 索引使用情况统计
SELECT
schemaname, relname, indexrelname,
idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes
WHERE relname LIKE 'access_log%'
ORDER BY idx_scan DESC;
数据迁移到分区表
已存在的普通大表迁移到分区表需要分步骤执行。直接建分区表再导入数据是最安全的方案,但停机时间较长。通过分批迁移的方式可以实现在线迁移。
-- 方案:使用CTE分批迁移旧表数据
-- 第1步:创建分区表(如上)
-- 第2步:分批迁移数据,避免长事务
DO $$
DECLARE
start_date DATE := '2026-01-01';
end_date DATE := '2026-04-01';
batch_start DATE;
batch_end DATE;
rows_moved INTEGER;
BEGIN
batch_start := start_date;
WHILE batch_start < end_date LOOP
batch_end := batch_start + INTERVAL '1 day';
-- 使用CTE先删后插,确保幂等
WITH moved AS (
DELETE FROM old_access_log
WHERE created_at >= batch_start
AND created_at < batch_end
RETURNING *
)
INSERT INTO access_log
SELECT * FROM moved;
GET DIAGNOSTICS rows_moved = ROW_COUNT;
RAISE NOTICE 'Migrated %: % rows', batch_start, rows_moved;
-- 批次间暂停,减少对在线业务影响
PERFORM pg_sleep(0.1);
batch_start := batch_end;
END LOOP;
END $$;
-- 第3步:验证数据一致性
SELECT 'old' AS source, COUNT(*) FROM old_access_log
UNION ALL
SELECT 'new', COUNT(*) FROM access_log;
-- 第4步:确认无误后删除旧表
DROP TABLE old_access_log;
数据备份恢复在分区表场景下有天然优势——可以按分区单独备份和恢复。对于月度分区表,每月只需要备份当前月和上月分区,历史分区在确认不再变更后可以跳过备份。SQL查询优化中,分区表的统计信息是独立收集的,每个分区的数据分布可能不同,执行计划可能因分区而异。NoSQL选型应用场景中,如果数据量增长可控且有明确的时间或分类维度,PostgreSQL分区表比迁移到MongoDB等文档数据库更经济实用。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/postgresql-fen-qu-biao-shi-zhan-da-biao-shui-ping-chai-fen/