趣动WillGo手机版
233.06MB · 2025-10-14
分区表是将逻辑上的大表拆分为物理上的小表的技术。想象你有一个放了10年日记的书架:如果不分类,找某一天的日记要翻遍整个书架;但如果按“年份→月份”分层,找2023年10月的日记只需翻对应层——这就是分区的核心逻辑:把数据按规则“归类”,让查询/操作只触达必要的数据。
PostgreSQL官网明确提到,分区的价值主要体现在4点:
DROP TABLE 旧分区
(比DELETE
快10倍以上),添加新数据只需CREATE TABLE 新分区
。PostgreSQL支持范围分区、列表分区、哈希分区三种内置方式,覆盖90%以上的业务场景。下面用“电商订单表”为例,逐一解释:
定义:按连续区间拆分数据,比如时间、数值范围。
适用场景:数据有自然的“顺序性”,且查询常按区间过滤(如“最近7天的订单”“金额100-500元的订单”)。
例子:订单表按create_time
(创建时间)分月存储:
-- 1. 创建分区表(指定分区方式为RANGE,分区键为create_time)
CREATE TABLE orders (
order_id bigserial PRIMARY KEY,
create_time timestamp NOT NULL,
user_id int NOT NULL,
amount decimal(10,2) NOT NULL
) PARTITION BY RANGE (create_time);
-- 2. 创建2023年10月的分区(区间:2023-10-01 ≤ create_time < 2023-11-01)
CREATE TABLE orders_202310 PARTITION OF orders
FOR VALUES FROM ('2023-10-01') TO ('2023-11-01');
-- 3. 创建2023年11月的分区(依此类推)
CREATE TABLE orders_202311 PARTITION OF orders
FOR VALUES FROM ('2023-11-01') TO ('2023-12-01');
关键细节:范围分区的区间是左闭右开(FROM
包含,TO
不包含),避免数据重叠(比如2023-11-01
会落到orders_202311
分区)。
定义:按枚举值拆分数据,比如地区、状态。
适用场景:数据有明确的“分类标签”,且查询常按标签过滤(如“华东地区的订单”“已完成的订单”)。
例子:订单表按region
(地区)拆分:
-- 1. 创建分区表(分区方式为LIST,分区键为region)
CREATE TABLE orders (
order_id bigserial PRIMARY KEY,
region text NOT NULL,
amount decimal(10,2) NOT NULL
) PARTITION BY LIST (region);
-- 2. 创建“华东”分区(包含值:'华东')
CREATE TABLE orders_east PARTITION OF orders
FOR VALUES IN ('华东');
-- 3. 创建“华南”分区(包含值:'华南')
CREATE TABLE orders_south PARTITION OF orders
FOR VALUES IN ('华南');
关键细节:列表分区的IN
子句必须覆盖所有可能的取值(否则插入未定义的值会报错),或添加默认分区(但默认分区会影响剪枝性能,谨慎使用)。
定义:按哈希算法拆分数据,将分区键的哈希值对modulus
取余,分配到不同分区。
适用场景:数据无明显顺序/分类,但需要均匀分布(比如用户数据、设备日志),避免单分区过大。
例子:用户数据表按user_id
拆分到8个分区:
-- 1. 创建分区表(分区方式为HASH,分区键为user_id)
CREATE TABLE user_data (
user_id int NOT NULL,
data text NOT NULL
) PARTITION BY HASH (user_id);
-- 2. 创建8个分区(modulus=8,余数0-7)
CREATE TABLE user_data_0 PARTITION OF user_data
FOR VALUES WITH (modulus 8, remainder 0);
CREATE TABLE user_data_1 PARTITION OF user_data
FOR VALUES WITH (modulus 8, remainder 1);
...
CREATE TABLE user_data_7 PARTITION OF user_data
FOR VALUES WITH (modulus 8, remainder 7);
关键细节:modulus
(模数)决定分区数量,建议选2的幂(如8、16、32),保证数据分布更均匀。
PostgreSQL的声明式分区(Declarative Partitioning)是推荐的方式(比传统继承分区更高效),核心步骤如下(以“measurement”表为例,官网经典案例):
首先定义父表(虚拟表,无实际数据),指定分区方式和分区键:
CREATE TABLE measurement (
city_id int NOT NULL,
logdate date NOT NULL,
peaktemp int,
unitsales int
) PARTITION BY RANGE (logdate); -- 按logdate(日期)范围分区
为每个区间创建子表(实际存储数据的物理表),指定区间边界:
-- 2006年2月的分区:logdate ∈ [2006-02-01, 2006-03-01)
CREATE TABLE measurement_y2006m02 PARTITION OF measurement
FOR VALUES FROM ('2006-02-01') TO ('2006-03-01');
-- 2006年3月的分区:logdate ∈ [2006-03-01, 2006-04-01)
CREATE TABLE measurement_y2006m03 PARTITION OF measurement
FOR VALUES FROM ('2006-03-01') TO ('2006-04-01');
在父表上创建索引,PostgreSQL会自动同步到所有分区(包括未来新增的分区):
-- 在logdate列创建索引(加速按日期的查询)
CREATE INDEX ON measurement (logdate);
这等价于在每个分区上创建logdate
索引,无需手动操作。
分区剪枝(Partition Pruning)是分区表的“灵魂”——让查询只扫描必要的分区。验证方法:用EXPLAIN
查看执行计划。
例子:查询2008年1月的记录:
-- 开启分区剪枝(默认开启)
SET enable_partition_pruning = on;
-- 查看执行计划
EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= '2008-01-01';
预期输出(只扫描2008年1月的分区):
Aggregate (cost=37.75..37.76 rows=1 width=8)
-> Seq Scan on measurement_y2008m01 (cost=0.00..33.12 rows=617 width=0)
Filter: (logdate >= '2008-01-01'::date)
如果关闭分区剪枝(SET enable_partition_pruning = off
),执行计划会显示扫描所有分区,性能差异巨大。
分区表的价值,很大程度体现在快速维护上。下面介绍常见操作:
当有新数据写入时(比如下个月的订单),需要提前创建分区,避免插入失败。有两种方式:
-- 创建2024年1月的订单分区
CREATE TABLE orders_202401 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
如果需要先导入数据再关联到父表(比如批量导入历史数据),可以用ATTACH PARTITION
:
-- 1. 创建独立表(复制父表结构)
CREATE TABLE orders_202401 (LIKE orders INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
-- 2. 添加CHECK约束(确保数据符合分区边界)
ALTER TABLE orders_202401 ADD CONSTRAINT orders_202401_check
CHECK (create_time >= '2024-01-01' AND create_time < '2024-02-01');
-- 3. 导入数据(比如从CSV文件)
copy orders_202401 FROM 'orders_202401.csv' CSV HEADER;
-- 4. 关联到父表(成为分区)
ALTER TABLE orders ATTACH PARTITION orders_202401
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
关键:步骤2的CHECK
约束会让PostgreSQL信任数据符合边界,避免扫描整个表验证(大幅提升效率)。
删除历史数据只需DROP分区表,比DELETE
快几个数量级(无需VACUUM清理):
-- 删除2006年2月的measurement分区(直接物理删除)
DROP TABLE measurement_y2006m02;
如果需要保留数据但从父表分离,可以用DETACH PARTITION
:
-- 从父表分离分区(仍可单独查询)
ALTER TABLE measurement DETACH PARTITION measurement_y2006m02;
如果单个分区过大(比如“2023年的订单”分区有1000万行),可以子分区(比如按user_id
哈希再拆分成8个分区):
-- 1. 创建父分区(2023年订单)
CREATE TABLE orders_2023 PARTITION OF orders
FOR VALUES FROM ('2023-01-01') TO ('2024-01-01')
PARTITION BY HASH (user_id); -- 子分区方式:按user_id哈希
-- 2. 创建子分区(8个)
CREATE TABLE orders_2023_0 PARTITION OF orders_2023
FOR VALUES WITH (modulus 8, remainder 0);
CREATE TABLE orders_2023_1 PARTITION OF orders_2023
FOR VALUES WITH (modulus 8, remainder 1);
...
注意:子分区会增加规划时间,建议最多嵌套2层(比如“年→月→用户哈希”就过了)。
分区剪枝(Partition Pruning)是PostgreSQL的自动优化:当查询包含分区键的过滤条件时,数据库会跳过不需要的分区,只扫描必要的分区。
比如查询“2023年10月的订单”,数据库会自动跳过orders_202309
(9月)、orders_202311
(11月)等分区,只扫描orders_202310
。
用EXPLAIN
命令查看执行计划中的“Append”节点:
Append
节点只包含需要的分区(比如orders_202310
)。Append
节点包含所有分区(比如orders_202309
、orders_202310
、orders_202311
)。PostgreSQL不仅在规划阶段剪枝,还会在执行阶段剪枝——比如处理PREPARE
语句(参数化查询)或嵌套循环 join 时,能根据 runtime 参数动态跳过分区。
比如:
-- 预处理查询(logdate为参数)
PREPARE get_measurement(date) AS
SELECT * FROM measurement WHERE logdate >= $1;
-- 执行时传入参数(2008-01-01),会剪枝到2008年1月的分区
EXECUTE get_measurement('2008-01-01');
总结了5条关键建议,避免踩坑:
核心原则:分区键必须是查询中最常出现的过滤条件(比如订单表的create_time
、用户表的user_id
)。
反例:如果查询很少按city_id
过滤,却用city_id
做分区键,会导致分区剪枝失效,查询变慢。
比如按“客户ID”列表分区,但如果未来客户数量从100增长到10000,分区数量会爆炸——此时哈希分区更合适(比如选32个分区,不管客户数量多少,数据都均匀分布)。
比如UPDATE measurement SET logdate = '2008-02-01' WHERE logdate = '2008-01-01'
——会将数据从measurement_y2008m01
迁移到measurement_y2008m02
,性能很差。建议:尽量避免修改分区键的值。
默认分区(FOR VALUES DEFAULT
)会接收所有未匹配的行,但会导致分区剪枝失效(因为数据库无法确定默认分区是否包含需要的数据)。仅在必要时使用(比如临时接收未知数据)。
假设你有一个“电商订单表”,需要按“订单状态”(status
,值为'pending'
(待支付)、'paid'
(已支付)、'cancelled'
(已取消))存储,且经常查询“已支付的订单”。应该选择哪种分区类型?请写出创建分区表的核心SQL。
答案:选择列表分区(LIST),因为“订单状态”是枚举值,符合列表分区的场景。
核心SQL:
CREATE TABLE orders (
order_id bigserial PRIMARY KEY,
status text NOT NULL,
amount decimal(10,2) NOT NULL
) PARTITION BY LIST (status);
CREATE TABLE orders_pending PARTITION OF orders FOR VALUES IN ('pending');
CREATE TABLE orders_paid PARTITION OF orders FOR VALUES IN ('paid');
CREATE TABLE orders_cancelled PARTITION OF orders FOR VALUES IN ('cancelled');
当插入数据到分区表时遇到ERROR: no partition of relation "orders" found for row
,可能的原因是什么?如何解决?
答案:
status = 'refunded'
(已退款),但未创建对应分区)。CREATE TABLE orders_refunded PARTITION OF orders FOR VALUES IN ('refunded')
);-PostgreSQL 查询慢?是不是忘了优化 GROUP BY、ORDER BY 和窗口函数? - cmdragon's Blog
233.06MB · 2025-10-14
126.01MB · 2025-10-14
238.09MB · 2025-10-14
有望推“相机”版本:某厂 8E5 超大杯工程机曝光,网友猜测为小米 17 Ultra
《海绵宝宝》主题动作游戏《海绵宝宝:潮汐巨神》推出试玩版:11 月 18 日正式发售,Steam 国区 122.4 元