文章目录
环境
系统平台:N/A
版本:9.0,4.5
文档用途
本文介绍通过使用分区表提升SQL查询性能。
详细信息
1. 场景
- 分区裁剪
使用分区键查分区表,访问数据量下降,提升性能。 - 批量删除
删除分区操作代替DELETE操作,增强可维护性。
2. 分区建议
几种分区观点
- 大于1000万行或表大小超过100G
- 历史数据清理频繁,DELETE产生大量死元组,VACUUM跟不上,DROP分区代替DELETE
3. 选择合适的分区类型
| 分区类型 | 适用场景 | 数据分布方式 |
|---|---|---|
| RANGE | 时间序列、数值区间 | 按范围映射,无重叠 |
| LIST | 地理区域、业务分类 | 按离散值映射 |
| HASH | 均匀打散,无天然分区键 | 根据哈希值分配 |
3.1 RANGE 分区 --- 最常用
适合时间序列数据(日志、订单、交易流水)。
sql
-- 按年月分区
CREATE TABLE orders (
id BIGSERIAL,
order_date DATE NOT NULL,
amount NUMERIC(10,2),
user_id INT
) PARTITION BY RANGE (order_date);
-- 创建分区
CREATE TABLE orders_2025_01 PARTITION OF orders
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE orders_2025_02 PARTITION OF orders
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
CREATE TABLE orders_2025_03 PARTITION OF orders
FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');
注意:RANGE 分区必须连续且无重叠。插入超出范围的数据会报错;预留"默认"分区兜底。
3.2 LIST 分区 --- 离散值
适合按区域、状态、类型分类的数据。
sql
-- 按区域分区
CREATE TABLE users (
id BIGSERIAL,
name TEXT,
region TEXT NOT NULL
) PARTITION BY LIST (region);
CREATE TABLE users_asia PARTITION OF users
FOR VALUES IN ('CN', 'JP', 'KR', 'SG');
CREATE TABLE users_eu PARTITION OF users
FOR VALUES IN ('DE', 'FR', 'UK', 'IT');
CREATE TABLE users_others PARTITION OF users
DEFAULT;
DEFAULT 分区兜底所有未匹配的值。如果有大量数据落入 DEFAULT,说明分区策略需要调整。
3.3 HASH 分区 --- 均匀打散
适合没有合适分区键但需提升性能的场景,数据均匀分布,但分区裁剪效果最弱(只有等值查询才能裁剪)。
sql
-- 按 user_id 哈希分为 4 个分区
CREATE TABLE sessions (
id BIGSERIAL,
user_id INT NOT NULL,
login_at TIMESTAMPTZ
) PARTITION BY HASH (user_id);
CREATE TABLE sessions_p0 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_p1 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE sessions_p2 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE sessions_p3 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
HASH 分区无法做范围裁剪,查询 WHERE user_id = 42 才会只扫一个分区。范围查询 WHERE user_id > 100 会扫所有分区。
4. 主键或唯一约束必须包含分区键
sql
highgo=# CREATE TABLE measurement (
city_id int,
logdate date not null,
peaktemp int,
unitsales int,
PRIMARY KEY (city_id)
) PARTITION BY RANGE (logdate);
ERROR: unique constraint on partitioned table must include all partitioning columns
DETAIL: PRIMARY KEY constraint on table "measurement" lacks column "logdate" which is part of the partition key.
highgo=# CREATE TABLE measurement (
city_id int,
logdate date not null,
peaktemp int,
unitsales int,
PRIMARY KEY (city_id, logdate)
) PARTITION BY RANGE (logdate);
CREATE TABLE
5. 限制分区个数
虽然没有对分区数量设置硬性上限,但实际使用时必须限制分区个数,核心原因在于查询规划(Query Planning)时间会随分区数量线性增长。优化器在生成执行计划时,需要为每个分区分别生成最优的访问路径,再将这些路径并联起来作为整张分区表的执行计划。当分区数过多时,即使查询最终只访问少数几个分区,规划阶段的耗时也可能高达数秒,相比普通表的毫秒级规划时间相差数百倍。
此外,每个分区都需要独立的统计信息和元数据缓存,分区过多会显著增加内存消耗,在长连接场景下甚至可能触发OOM; 同时,当SQL未携带分区键条件需要全分区扫描时,一些高效的执行路径(如 HashAgg、HashJoin、Merge Join 等)也可能无法被支持。
因此,在设计分区策略时要合理选择分区数量,在数据局部性收益与规划开销之间取得平衡。
6. 查询条件必带分区键
6.1 不带分区键等于全分区扫描
不带分区键等于全分区扫描,比普通表要慢。
sql
-- 错误:不带分区键,扫描所有分区
SELECT * FROM orders WHERE user_id = 42;
-- 正确:带上分区键,只扫描 1/12 的分区
SELECT * FROM orders
WHERE user_id = 42
AND order_date >= '2025-01-01'
AND order_date < '2025-02-01';
6.2 分区裁剪的两种模式
| 模式 | 触发时机 | 条件 |
|---|---|---|
| 静态裁剪(Static Pruning) | 查询规划阶段 | 分区键使用常量 |
| 运行时裁剪(Runtime Pruning) | 查询执行阶段 | 分区键使用参数(PREPARE / 子查询结果) |
静态裁剪
sql
-- 规划阶段就能确定目标分区
EXPLAIN SELECT * FROM orders
WHERE order_date = '2025-01-15';
-- 输出:
-- Seq Scan on orders_2025_01 orders (cost=0.00..35.50 rows=10 width=24)
-- Filter: (order_date = '2025-01-15'::date)
-- 只扫描 orders_2025_01
-- 范围查询也能裁剪
EXPLAIN SELECT * FROM orders
WHERE order_date >= '2025-01-01' AND order_date < '2025-03-01';
-- 只扫描 1 月和 2 月的分区
运行时裁剪
sql
-- 使用 PREPARE,参数值在执行时才确定
PREPARE find_orders (date) AS
SELECT * FROM orders WHERE order_date = $1;
EXPLAIN EXECUTE find_orders('2025-02-10');
-- 运行时裁剪:只扫描目标分区
-- 子查询也能触发运行时裁剪
EXPLAIN SELECT * FROM orders
WHERE order_date = (SELECT max(order_date) FROM recent_updates);
6.3 裁剪失效
分区键被函数包裹
sql
-- 裁剪失效:函数包裹分区键
SELECT * FROM orders WHERE date_trunc('month', order_date) = '2025-01-01';
-- 改为范围查询
SELECT * FROM orders
WHERE order_date >= '2025-01-01' AND order_date < '2025-02-01';
隐式类型转换
sql
-- 裁剪可能失效:字符串与 date 比较
SELECT * FROM orders WHERE order_date = '2025-01-15';
-- 显式类型转换,确保裁剪生效
SELECT * FROM orders WHERE order_date = '2025-01-15'::date;
总结
分区类型:时序用 RANGE,离散值用 LIST,均匀打散用 HASH,超大用复合
查询必带分区键:否则分区无意义,甚至更慢;注意函数包裹和类型转换陷阱
日常监控:定期检查分区数、分区大小、裁剪是否生效