☰
分区表实战:范围/列表/hash 分区的适用场景和运维操作
2026/10/15 17:23:20 网站建设 项目流程

十五年数据库相关经验,做过 DBA、架构师、技术顾问。不求"颠覆",只求"靠谱"。


一张表到了几千万甚至上亿行,即使有索引,查询也开始变慢。插入变慢、更新变慢、DDL 操作几乎不可能。

很多人第一反应是"分库分表"。但分库分表不是第一步。在分库分表之前,有一个更轻量的方案:分区表。

分区表不需要中间件,不需要改应用代码,数据库原生支持。一张 5 亿行的表,按月分区后,每个分区只有几千万行,查询只扫目标分区,性能大幅提升。DDL 操作(比如删除过期数据)变成 drop 一个分区,秒级完成。

今天把分区表的完整实战讲清楚。什么时候该分区、范围/列表/hash 分区怎么选、怎么建、怎么维护、踩了什么坑。


01 什么时候该用分区表

不是所有大表都需要分区。分区表有明确的适用场景。

适合分区的场景

场景一:数据按时间持续增长,查询通常带时间条件。

日志表、订单表、流水表——数据按天/月增长,查询通常是"查某个月的数据"。这种表按时间范围分区,查询自动只扫目标分区(分区裁剪),性能提升明显。

场景二:需要定期清理历史数据。

保留近 12 个月数据,更早的删除。如果没有分区,DELETE 几千万行数据要很久,还会产生大量 WAL 日志和碎片。有了分区,直接 DROP 一个分区,秒级完成。

场景三:大表 DDL 操作困难。

给一张 3 亿行的表加索引,可能要跑几个小时,期间锁表。分区后,每个分区单独建索引,可以并行操作,不影响其他分区。

不适合分区的场景

场景一:数据量不大。几百万行的表,分区只会增加复杂度,不会提升性能。

场景二:查询条件不包含分区键。如果大部分查询都不带分区键(比如按时间分区的表,查询却按用户 ID),分区裁剪无法生效,反而多了一层管理开销。

场景三:跨分区关联查询多。两个分区表 JOIN,如果分区键不一致,会产生跨分区笛卡尔积,性能反而下降。


02 三种分区策略:怎么选

分区策略的核心是"数据怎么分"。分错了,性能不升反降。

范围分区(Range Partitioning)

按某个列的值的范围分配数据。最常见的是按时间范围分区。

-- 按月份范围分区CREATETABLEorders(idBIGINT,user_idINT,amountDECIMAL(10,2),order_dateDATE,statusVARCHAR(20))PARTITIONBYRANGE(order_date);-- 创建分区CREATETABLEorders_2025_01PARTITIONOFordersFORVALUESFROM('2025-01-01')TO('2025-02-01');CREATETABLEorders_2025_02PARTITIONOFordersFORVALUESFROM('2025-02-01')TO('2025-03-01');CREATETABLEorders_2025_03PARTITIONOFordersFORVALUESFROM('2025-03-01')TO('2025-04-01');

查询带order_date条件时,优化器自动只扫目标分区:

-- 只扫 orders_2025_02 一个分区EXPLAINSELECT*FROMordersWHEREorder_date='2025-02-15';

适用场景:时间序列数据、日志、订单、流水——查询条件通常带时间范围。

列表分区(List Partitioning)

按某个列的离散值分配数据。

-- 按地区列表分区CREATETABLEusers(idBIGINT,nameVARCHAR(100),regionVARCHAR(20),created_atTIMESTAMP)PARTITIONBYLIST(region);CREATETABLEusers_eastPARTITIONOFusersFORVALUESIN('北京','上海','广州','深圳');CREATETABLEusers_centralPARTITIONOFusersFORVALUESIN('武汉','长沙','郑州','合肥');CREATETABLEusers_westPARTITIONOFusersFORVALUESIN('成都','重庆','西安','昆明');

适用场景:数据按地区、类型、状态等离散值分布,查询条件通常包含这些字段。

Hash 分区

对分区键做哈希运算,均匀分布到各个分区。

-- 按 user_id 做 hash 分区,分成 4 个分区CREATETABLEevents(idBIGINT,user_idINT,event_typeVARCHAR(50),created_atTIMESTAMP)PARTITIONBYHASH(user_id);CREATETABLEevents_0PARTITIONOFeventsFORVALUESWITH(MODULUS4,REMAINDER0);CREATETABLEevents_1PARTITIONOFeventsFORVALUESWITH(MODULUS4,REMAINDER1);CREATETABLEevents_2PARTITIONOFeventsFORVALUESWITH(MODULUS4,REMAINDER2);CREATETABLEevents_3PARTITIONOFeventsFORVALUESWITH(MODULUS4,REMAINDER3);

user_id = 100 会落到哪个分区,由100 % 4决定。数据均匀分布,没有热点。

适用场景:没有明显范围或分类特征的大表,目的是分散数据量,让每个分区大小可控。

踩坑提醒:不要为了"看起来高级"选 hash 分区。大多数业务场景更适合范围分区或列表分区,因为查询条件通常带范围或分类。hash 分区适合没有查询模式特征的场景。


03 分区维护:创建、删除、拆分

分区表建好了不是"一劳永逸"。分区需要持续维护。

新增分区

范围分区最常见的维护操作:提前创建未来时间的分区。

-- 提前创建下个月的分区CREATETABLEorders_2025_04PARTITIONOFordersFORVALUESFROM('2025-04-01')TO('2025-05-01');

建议在分区过期前至少提前一个月创建。比如 3 月底要创建 4 月的分区,不要等到 4 月 1 号才建。如果写入数据时发现分区不存在,会直接报错。

PostgreSQL 支持默认分区(DEFAULT partition),当数据不匹配任何已有分区时落入默认分区。但默认分区不是好的长期方案——它会逐渐变大,而且不能直接拆分成独立分区(需要重建)。所以还是提前建好。

删除过期分区

-- 删除 12 个月前的分区ALTERTABLEorders DETACHPARTITIONorders_2024_01;DROPTABLEorders_2024_01;

用 DETACH 先解绑,再 DROP。DETACH 是 DDL 操作,很快。DROP 删除实际数据,也很快(比 DELETE 快几个数量级)。

踩坑提醒:DROP 之前先确认分区里的数据不再需要。可以用 pg_dump 导出做归档,或者直接 COPY 到归档表。

分区拆分

如果一个分区数据量太大,可以拆分成更小的分区。但 PostgreSQL/KES 不支持直接 SPLIT 分区,需要手动操作:

-- 假设 orders_2025_02 数据量太大,想按周拆分-- 1. 创建新的周分区CREATETABLEorders_2025_02_w1PARTITIONOFordersFORVALUESFROM('2025-02-01')TO('2025-02-08');-- ... 创建 w2, w3, w4-- 2. 先把月分区从主表解绑ALTERTABLEorders DETACHPARTITIONorders_2025_02;-- 3. 把数据迁移到周分区INSERTINTOordersSELECT*FROMorders_2025_02;-- 4. 删除旧的月分区DROPTABLEorders_2025_02;

这个过程需要停机窗口,因为中间有一段时间数据不在分区结构里。如果数据量大,用 pg_dump + pg_restore 或者逻辑导出导入。


04 分区表的常见坑

坑一:分区键选择不当

选了分区键,但大部分查询条件不带分区键。分区裁剪无法生效,查询扫了所有分区,性能没有提升反而下降。

怎么避免:先分析查询模式。80% 以上的查询是否都包含分区键?如果不是,换分区策略或者不分。

坑二:全局索引缺失

分区表的主键和唯一约束必须包含分区键。如果你的业务需要按非分区键做唯一校验,必须单独处理。

-- 主键必须包含分区键CREATETABLEorders(idBIGINT,order_dateDATE,PRIMARYKEY(id,order_date)-- 必须包含 order_date)PARTITIONBYRANGE(order_date);

如果id本身需要全局唯一(不含分区键),分区表不支持。这是分区表的固有限制。

坑三:跨分区 JOIN 性能差

两个分区表 JOIN,如果分区键不一致,会产生跨分区笛卡尔积。

-- orders 按 order_date 分区,order_items 按 item_date 分区SELECT*FROMorders oJOINorder_items oiONo.id=oi.order_id;-- 如果 order_date 和 item_date 不一致,会扫所有分区组合

解法:关联的两个表用相同的分区键,或者把小表广播到所有分区(绑定集合策略)。

坑四:忘记创建未来分区

范围分区如果不提前创建未来分区,写入新数据时直接报错。

解法:用定时任务自动创建未来分区:

-- 每月 25 号创建下个月的分区SELECTcron.schedule('create_next_month_partition','0 0 25 * *',$$-- 动态创建下个月分区(逻辑需按实际表结构调整)$$);

或者用应用层脚本定期检查分区是否存在,不存在就创建。


对比:分区表 vs 分库分表 vs 单表优化

维度单表优化分区表分库分表
数据量上限几千万行几亿行理论无上限
查询性能依赖索引分区裁剪 + 索引路由到目标分片
写入性能单表写入单分区写入多分片并行写入
运维复杂度低中高
应用改造不需要不需要需要
DDL 操作慢(全表锁)快(分区级)分片级
历史数据清理DELETE(慢)DROP 分区(快)清理分片

决策框架:按数据量和查询模式选择方案

数据量查询模式推荐方案说明
< 1000 万行任意单表 + 索引分区只会增加复杂度
1000 万 - 1 亿行查询带范围条件范围分区分区裁剪效果好
1000 万 - 1 亿行查询带分类条件列表分区按分类路由
1000 万 - 1 亿行查询无固定模式Hash 分区均匀分散数据
1 亿 - 5 亿行查询带范围条件范围分区 + 二级索引分区裁剪 + 索引
> 5 亿行单表扛不住分库分表单机到达极限

深度分析:为什么分区表经常被误用

分区表好用,但误用的情况很常见。

第一,为了分区而分区。数据量不大的时候就上了分区,以为"早晚用得上"。结果分区增加了管理复杂度,性能没有明显提升。分区是"数据量大到单表性能下降"时的方案,不是预防性方案。

第二,分区键和业务查询不匹配。按时间分区,但大部分查询按用户 ID 查。分区裁剪不生效,查一次扫所有分区,比不分还慢。分区键的选择必须基于实际查询模式,不是基于"哪个字段数据量大"。

第三,建了分区就不管了。分区表需要持续维护——提前创建未来分区、定期删除过期分区、监控分区大小。建完就不管,过几个月分区满了写入报错,或者过期数据堆积占满磁盘。

分区表的核心原则是:查询模式决定分区策略,数据量决定分区时机,持续维护决定分区效果。


总结

分区表是单机大数据量的最优解——不需要中间件、不需要改应用代码、数据库原生支持。

范围分区适合时间序列数据,列表分区适合分类数据,hash 分区适合无固定查询模式的大表。分区键选择必须基于实际查询模式,不是凭感觉。

分区表不是分库分表的替代品,是分库分表的前一步。能分区解决的不用分库分表,单机扛不住了再走分库分表的路径。

维护分区是持续工作:提前创建未来分区、定期清理过期分区、监控分区大小。建完就不管,分区表会变成定时炸弹。

后续我会继续分享主备切换与故障转移、锁排查这些话题,跟着我一篇篇学,数据库这块就没问题了。

有问题评论区见。


十五年数据库领域老炮。关注我,一起把数据库这件事搞明白。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询