分区表:大规模数据的高效管理
1.1 分区表概述
分区表是将逻辑上的大表 拆分为物理上的小表的技术。当表的数据量超过服务器内存时,分区表能解决以下痛点:
- 查询性能优化:仅扫描相关分区(比如查2023年数据时,跳过2020年的分区);
- 数据维护高效:删除旧数据只需 drop 分区(而非逐条删除);
- 存储分层:将冷数据(如5年前的记录)存到廉价存储,热数据存到高速存储。
PostgreSQL 支持三种分区方式:
- 范围分区(Range):按连续范围划分(如日期、ID);
- 列表分区(List):按离散值划分(如地区、状态);
- 哈希分区(Hash):按哈希值模运算划分(如将用户ID均匀分到10个分区)。
1.2 声明式分区的实现步骤
声明式分区是 PostgreSQL 10+ 推荐的方式(无需手动写触发器),以按日期分区的测量表为例:
步骤1:创建分区表(父表)
sql
-- 创建分区表,按logdate(日期)范围分区
CREATE TABLE measurement (
city_id int NOT NULL,
logdate date NOT NULL,
peaktemp int,
unitsales int
) PARTITION BY RANGE (logdate); -- 分区方法+分区键
步骤2:创建分区
每个分区对应一个时间范围,上界是排他的 (如FROM '2023-01-01' TO '2023-02-01'
包含1月但不包含2月1日):
sql
-- 创建2023年1月的分区
CREATE TABLE measurement_202301 PARTITION OF measurement
FOR VALUES FROM ('2023-01-01') TO ('2023-02-01');
-- 创建2023年2月的分区(指定高速表空间)
CREATE TABLE measurement_202302 PARTITION OF measurement
FOR VALUES FROM ('2023-02-01') TO ('2023-03-01')
TABLESPACE fast_tablespace;
步骤3:创建索引
在父表上创建索引会自动同步到所有分区:
sql
-- 对分区键logdate创建索引(加速查询与数据路由)
CREATE INDEX ON measurement (logdate);
步骤4:验证数据路由
插入数据时,PostgreSQL 会自动将行路由到对应分区:
sql
INSERT INTO measurement VALUES (1, '2023-01-15', 25, 100); -- 自动存入measurement_202301
INSERT INTO measurement VALUES (2, '2023-02-20', 28, 150); -- 自动存入measurement_202302
1.3 分区维护:添加、删除与 Detach
1.3.1 删除旧分区
直接 drop 分区即可快速删除大量数据(无需逐行删除):
sql
-- 删除2023年1月的分区(瞬间完成)
DROP TABLE measurement_202301;
1.3.2 detach 分区(保留数据但移出分区表)
如果需要保留旧数据但不再让它属于分区表,可以用 DETACH PARTITION
:
sql
-- 将2023年1月的分区移出measurement表
ALTER TABLE measurement DETACH PARTITION measurement_202301;
此时 measurement_202301
变成独立表,仍可查询,但不再参与分区表的逻辑。
1.3.3 添加新分区
每月需添加新分区以接收新数据:
sql
-- 添加2023年3月的分区
CREATE TABLE measurement_202303 PARTITION OF measurement
FOR VALUES FROM ('2023-03-01') TO ('2023-04-01');
1.4 分区剪枝:让查询跳过无用分区
**分区剪枝(Partition Pruning)**是分区表的核心优化:查询时,PostgreSQL 会自动跳过不包含目标数据的分区。
例子:查询2023年2月的数据
sql
-- 开启分区剪枝(默认开启)
SET enable_partition_pruning = on;
EXPLAIN ANALYZE
SELECT COUNT(*) FROM measurement WHERE logdate >= '2023-02-01';
剪枝前的执行计划(会扫描所有分区):
arduino
Aggregate (cost=188.76..188.77 rows=1 width=8)
-> Append (cost=0.00..181.05 rows=3085 width=0)
-> Seq Scan on measurement_202301 (cost=0.00..33.12 rows=617 width=0)
Filter: (logdate >= '2023-02-01'::date)
-> Seq Scan on measurement_202302 (cost=0.00..33.12 rows=617 width=0)
Filter: (logdate >= '2023-02-01'::date)
-> Seq Scan on measurement_202303 (cost=0.00..33.12 rows=617 width=0)
Filter: (logdate >= '2023-02-01'::date)
剪枝后的执行计划(仅扫描202302分区):
arduino
Aggregate (cost=37.75..37.76 rows=1 width=8)
-> Seq Scan on measurement_202302 (cost=0.00..33.12 rows=617 width=0)
Filter: (logdate >= '2023-02-01'::date)
物化视图:用空间换时间的查询优化
2.1 物化视图与普通视图的区别
特性 | 普通视图(View) | 物化视图(Materialized View) |
---|---|---|
数据存储 | 不存储数据,实时计算 | 存储查询结果(类似表) |
查询性能 | 每次查询重新计算,慢 | 直接读存储的结果,快 |
数据新鲜度 | 实时更新 | 需要手动刷新(REFRESH) |
索引支持 | 不支持(依赖基表索引) | 支持创建索引(进一步加速查询) |
2.2 物化视图的创建与刷新
以销售汇总物化视图为例:
步骤1:创建物化视图
sql
-- 创建销售汇总物化视图(按销售日期+销售员分组)
CREATE MATERIALIZED VIEW sales_summary AS
SELECT
seller_no,
invoice_date,
SUM(invoice_amt) AS total_sales
FROM invoice
WHERE invoice_date < CURRENT_DATE -- 排除今日未完成的订单
GROUP BY seller_no, invoice_date;
-- 添加唯一索引(用于并发刷新)
CREATE UNIQUE INDEX idx_sales_summary ON sales_summary (seller_no, invoice_date);
步骤2:刷新物化视图
物化视图的结果不会自动更新,需手动刷新:
sql
-- 普通刷新(会锁表,期间无法查询)
REFRESH MATERIALIZED VIEW sales_summary;
-- 并发刷新(不锁表,但需要唯一索引)
REFRESH MATERIALIZED VIEW CONCURRENTLY sales_summary;
2.3 物化视图的性能优势案例
官网中的Foreign Data Wrapper(FDW)案例 很直观:
假设用 file_fdw
读取本地字典文件(47万行),直接查询需188ms,而物化视图加索引后仅需0.1ms!
sql
-- 1. 直接查询FDW表(慢)
SELECT COUNT(*) FROM words WHERE word = 'caterpiler'; -- 188ms
-- 2. 查询物化视图(快)
CREATE MATERIALIZED VIEW wrd AS SELECT * FROM words;
CREATE UNIQUE INDEX wrd_word ON wrd (word);
SELECT COUNT(*) FROM wrd WHERE word = 'caterpiler'; -- 0.1ms
并行查询:利用多CPU加速查询
3.1 并行查询的工作原理
并行查询将一个大任务拆分成多个子任务,由多个工作进程同时执行,最后合并结果。例如:
- 并行扫描:多个进程同时扫描一个大表的不同片段;
- 并行聚合:多个进程分别计算部分结果,最后汇总总和;
- 并行连接:两个大表连接时,多个进程同时处理不同的数据块。
3.2 并行查询的适用场景
并非所有查询都能并行,以下场景最有效:
- 大表全表扫描 (如
SELECT COUNT(*) FROM big_table
); - 大表聚合操作 (如
SUM
、AVG
、COUNT
); - 大表连接 (如
JOIN
两个千万行级别的表); - 分区表扫描(多个分区同时扫描)。
不支持并行的场景:
- 使用了
LIMIT
(无法拆分任务); - 使用了并行不安全的函数(如
random()
、current_timestamp
); - 事务中修改了数据库(如
INSERT
/UPDATE
/DELETE
)。
3.3 并行安全:函数与聚合的并行标签
函数/聚合的并行安全性决定了能否在并行查询中使用,分为三类:
- PARALLEL SAFE :完全安全(如
abs()
、concat()
); - PARALLEL RESTRICTED :仅能在 leader 进程执行(如
currval()
); - PARALLEL UNSAFE :不安全(如
setval()
、pg_sleep()
)。
查看函数的并行标签
sql
-- 查看concat函数的并行标签
SELECT proname, proparallel FROM pg_proc WHERE proname = 'concat';
-- 结果:proparallel = 's'(即SAFE)
修改函数的并行标签
如果自定义函数是安全的,可以手动标记:
sql
-- 将自定义函数标记为PARALLEL SAFE
ALTER FUNCTION my_safe_function() SET PARALLEL SAFE;
课后 Quiz
问题1:插入数据到声明式分区表时,提示"no partition of relation found for row",原因是什么?如何解决?
答案 :
原因:插入的数据没有匹配的分区(如插入2023-04-01的数据,但未创建4月的分区)。
解决:
- 提前创建对应分区(如
CREATE TABLE measurement_202304 PARTITION OF measurement FOR VALUES FROM ('2023-04-01') TO ('2023-05-01')
); - 创建默认分区(接收所有未匹配的数据):
CREATE TABLE measurement_default PARTITION OF measurement DEFAULT;
。
问题2:为什么并发刷新物化视图需要唯一索引?
答案 :
并发刷新(REFRESH CONCURRENTLY
)需要比较旧数据与新数据的差异,唯一索引用于快速定位修改的行,避免全表扫描。如果没有唯一索引,PostgreSQL 无法安全地并发更新物化视图。
问题3:并行查询没有生效,可能的原因是什么?
答案:
enable_parallel_query
参数关闭(默认开启,需检查postgresql.conf
);- 查询使用了并行不安全的函数(如
random()
); - 表的数据量太小(PostgreSQL 认为并行的 overhead 大于收益);
- 查询包含
LIMIT
或FOR UPDATE
(无法并行)。
常见报错解决方案
报错1:ERROR: no partition of relation "measurement" found for row
原因 :插入的数据没有匹配的分区。
解决:创建对应分区或默认分区(参考Quiz问题1)。
报错2:ERROR: cannot refresh materialized view "sales_summary" concurrently without a unique index
原因 :并发刷新需要唯一索引。
解决 :给物化视图添加唯一索引(如 CREATE UNIQUE INDEX idx_sales_summary ON sales_summary (seller_no, invoice_date)
)。
报错3:并行查询未生效(执行计划中无 Parallel Seq Scan
)
原因:
enable_parallel_query
= off;- 查询使用了并行不安全的函数;
- 表太小(小于
min_parallel_table_scan_size
参数,默认8MB)。
解决:
- 检查
postgresql.conf
:enable_parallel_query = on
; - 将函数标记为
PARALLEL SAFE
; - 确认表大小超过
min_parallel_table_scan_size
。
参考链接
- 分区表:www.postgresql.org/docs/17/ddl...
- 物化视图:www.postgresql.org/docs/17/rul...
- 并行查询:www.postgresql.org/docs/17/par...
往期文章归档
-
只给表子集建索引?用函数结果建索引?PostgreSQL这俩操作凭啥能省空间又加速? - cmdragon's Blog
-
想抓PostgreSQL里的慢SQL?pg_stat_statements基础黑匣子和pg_stat_monitor时间窗,谁能帮你更准揪出性能小偷? - cmdragon's Blog
-
PostgreSQL 查询慢?是不是忘了优化 GROUP BY、ORDER BY 和窗口函数? - cmdragon's Blog
-
PostgreSQL选Join策略有啥小九九?Nested Loop/Merge/Hash谁是它的菜? - cmdragon's Blog
-
PostgreSQL索引选B-Tree还是GiST?"瑞士军刀"和"多面手"的差别你居然还不知道? - cmdragon's Blog
-
PostgreSQL处理SQL居然像做蛋糕?解析到执行的4步里藏着多少查询优化的小心机? - cmdragon's Blog
-
PostgreSQL备份不是复制文件?物理vs逻辑咋选?误删还能精准恢复到1分钟前? - cmdragon's Blog
-
PostgreSQL里的PL/pgSQL到底是啥?能让SQL从"说目标"变"讲步骤"? - cmdragon's Blog
-
PostgreSQL UPDATE语句怎么玩?从改邮箱到批量更新的避坑技巧你都会吗? - cmdragon's Blog
-
PostgreSQL 17安装总翻车?Windows/macOS/Linux避坑指南帮你搞定? - cmdragon's Blog
-
能当关系型数据库还能玩对象特性,能拆复杂查询还能自动管库存,PostgreSQL凭什么这么香? - cmdragon's Blog
-
测试覆盖率不够高?这些技巧让你的FastAPI测试无懈可击! - cmdragon's Blog
免费好用的热门在线工具