想让PostgreSQL查询快到飞起?分区表、物化视图、并行查询这三招灵不灵?

分区表:大规模数据的高效管理

1.1 分区表概述

分区表是将逻辑上的大表 拆分为物理上的小表的技术。当表的数据量超过服务器内存时,分区表能解决以下痛点:

  • 查询性能优化:仅扫描相关分区(比如查2023年数据时,跳过2020年的分区);
  • 数据维护高效:删除旧数据只需 drop 分区(而非逐条删除);
  • 存储分层:将冷数据(如5年前的记录)存到廉价存储,热数据存到高速存储。

PostgreSQL 支持三种分区方式:

  1. 范围分区(Range):按连续范围划分(如日期、ID);
  2. 列表分区(List):按离散值划分(如地区、状态);
  3. 哈希分区(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 并行查询的适用场景

并非所有查询都能并行,以下场景最有效:

  1. 大表全表扫描 (如 SELECT COUNT(*) FROM big_table);
  2. 大表聚合操作 (如 SUMAVGCOUNT);
  3. 大表连接 (如 JOIN 两个千万行级别的表);
  4. 分区表扫描(多个分区同时扫描)。

不支持并行的场景

  • 使用了 LIMIT(无法拆分任务);
  • 使用了并行不安全的函数(如 random()current_timestamp);
  • 事务中修改了数据库(如 INSERT/UPDATE/DELETE)。

3.3 并行安全:函数与聚合的并行标签

函数/聚合的并行安全性决定了能否在并行查询中使用,分为三类:

  1. PARALLEL SAFE :完全安全(如 abs()concat());
  2. PARALLEL RESTRICTED :仅能在 leader 进程执行(如 currval());
  3. 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月的分区)。

解决:

  1. 提前创建对应分区(如 CREATE TABLE measurement_202304 PARTITION OF measurement FOR VALUES FROM ('2023-04-01') TO ('2023-05-01'));
  2. 创建默认分区(接收所有未匹配的数据):CREATE TABLE measurement_default PARTITION OF measurement DEFAULT;

问题2:为什么并发刷新物化视图需要唯一索引?

答案

并发刷新(REFRESH CONCURRENTLY)需要比较旧数据与新数据的差异,唯一索引用于快速定位修改的行,避免全表扫描。如果没有唯一索引,PostgreSQL 无法安全地并发更新物化视图。

问题3:并行查询没有生效,可能的原因是什么?

答案

  1. enable_parallel_query 参数关闭(默认开启,需检查 postgresql.conf);
  2. 查询使用了并行不安全的函数(如 random());
  3. 表的数据量太小(PostgreSQL 认为并行的 overhead 大于收益);
  4. 查询包含 LIMITFOR 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)。
    解决
  1. 检查 postgresql.confenable_parallel_query = on
  2. 将函数标记为 PARALLEL SAFE
  3. 确认表大小超过 min_parallel_table_scan_size

参考链接

  1. 分区表:www.postgresql.org/docs/17/ddl...
  2. 物化视图:www.postgresql.org/docs/17/rul...
  3. 并行查询:www.postgresql.org/docs/17/par...

往期文章归档

相关推荐
小虎AI生活5 小时前
别光盯着Claude,CodeBuddy的Codex才是AI编程爱好者的下一个效率神器!
chatgpt·ai编程·codebuddy
Value_Think_Power5 小时前
每次请求时,后端先对比过期时间,如果过期就refresh
后端
用户68545375977695 小时前
🛡️ MyBatis的#{}和${}:安全 vs 危险!
后端
uhakadotcom5 小时前
ChatGPT Atlas的使用笔记
后端·面试·github
得物技术5 小时前
从一次启动失败深入剖析:Spring循环依赖的真相|得物技术
java·后端
程序猿DD5 小时前
Jackson 序列化的隐性成本
java·后端
用户68545375977695 小时前
⚡ Spring Boot自动配置:约定优于配置的魔法!
后端
aicoding_sh5 小时前
为 Claude Code CLI 提供美观且高度可定制的状态行,具有powerline support, themes, and more.
后端·github