选错分区键,分区再完美也没用!PostgreSQL 分区性能翻车真相
分区常被视作一项性能开关。对大表开启分区后,缓慢的查询性能往往就能提速。多数情况下确实如此。但也存在另一种情形:完成全部工作,将超大表拆分为规整分区后,慢查询性能没有任何改善。表已经做了分区,性能却毫无提升。
出现这类问题时,分区键几乎都是问题根源。分区的底层实现逻辑相对简单,PostgreSQL 对此有着成熟的处理能力。而选择哪一列作为分区键才是重中之重,一旦数据表体量上涨,该决策基本无法撤回。本文将介绍如何科学选定分区键。
若希望先了解底层原理 —— 范围分区、列表分区、哈希分区的工作机制、分区裁剪跳过无关分区的原理、运维工作如何简化,可参阅《借助分区优化 PostgreSQL 性能》。
分区键决定分区成败
核心理念可以概括为一句话:分区裁剪—— 也就是分区能够提速的根本机制,仅当查询条件过滤字段为分区键时才会生效。选对分区键,PostgreSQL 只需扫描一小块分区,而非整张数据表;选错分区键,PostgreSQL 仍然需要读取全部数据,只不过此时数据分散存放在大量子表当中,查询速度甚至会慢于原始单张大表。
因此真正的问题从来不是「我是否应该对这张表做分区」,而是「业务核心查询会依据哪些字段进行过滤,我能否基于该字段设置分区」。分区键必须与业务负载相匹配。只要选对分区键,其余工作往往水到渠成;一旦选错,后续任何优化手段都无法挽回性能损失。
在正式落地分区方案前,可采用一种快速自检方法:提取该表执行频率最高、资源开销最大的十条查询语句,查看它们的 WHERE 查询条件。倘若绝大多数查询都基于同一字段或少数几个固定字段做过滤,那么该字段便可作为候选分区键。若查询的过滤条件分散在十个不同字段上,则分区仅能优化一部分查询,对其余查询不起任何作用,这一点需要在前期就做到心中有数。
倾斜字段使用哈希分区会适得其反
哈希分区常常会出现预期与实际效果相悖的问题,值得深入剖析。
哈希分区的设计初衷是实现数据均匀分布。选取一个高基数字段做哈希运算后,PostgreSQL 会将数据行分散到固定数量的分区内。从理论层面看,一个包含上百个不同取值的字段十分适合哈希分区。充足的取值可以将数据均衡分配,实现负载均分。
但字段的唯一值数量并不是唯一的判断标准,真正关键的是每个取值对应的数据行数分布情况。以多租户 SaaS 数据表的租户、工作空间字段为例:该字段可能存在数百个不同租户,表面上看数据分布良好。但真实业务的数据量分布往往并不均衡,少数头部租户产生绝大部分数据,大量长尾租户的数据量微乎其微。
哈希分区不会感知、也不会处理该倾斜现象,它按照字段取值分配数据,而非按照数据体量分配负载。最终少数高数据量租户的数据会被哈希运算分配至少量分区,造成绝大部分数据堆积,其余分区近乎空置。
此时数据表虽然完成分区拆分,但大部分热点压力依旧集中在少数分区中。分区操作已经完成,但原本希望消除的热点问题依旧存在。
用仓库分区举例:按照商品名称首字母划分仓库过道,平面图上布局十分规整。实际运营后却发现大部分商品名称以少数几个字母开头,人流依旧聚集在同一区域,仓库另一半空间闲置。物理隔断已经搭建完成,访问压力却没有实现分流。
当分区键本身数据分布均匀时,哈希分区是可靠的方案,例如 UUID 主键、无业务含义的代理 ID、数据量分布均衡的客户 ID。在倾斜字段上使用哈希分区,只能均匀分配字段取值,却无法均衡业务负载。关于哈希分区相较于范围分区的优劣适配场景,可参考技术文章《哈希分区何时优于范围分区》。
分区键匹配真实的数据行为特征
解决分区键数据倾斜问题,不能依靠优化哈希算法,而应当选择与数据表查询模式、数据增长规律相匹配的分区键。实际生产中,同一数据库内不同数据表适合使用不同的分区键,无需强求统一方案。每张表应当按照自身业务负载独立分区,不要对所有数据表强制使用同一套分区策略。
绝大多数业务场景可归为两类典型模式:
无天然排序需求、追求均匀分布:均匀字段使用哈希分区。数据表以 UUID、代理客户 ID 作为标识字段,字段取值天然分散时,哈希分区可以达到预期效果。数据行均匀分布,无热点分区;新增客户数据会根据 ID 自动分配至对应分区,无需人工干预。哈希分区适用于该类场景。
按时间维度查询、数据持续增长:时间戳字段使用范围分区。数据表的查询大多基于时间维度,例如近期订单、当月事件、上个季度业务数据,时间戳范围分区是最优方案。带日期过滤条件的查询可直接裁剪定位至目标分区;新增下月数据无需复杂操作,PostgreSQL 自动将新行写入对应分区;过期数据清理仅需删除分区,替代高 IO 开销的大批量 DELETE 语句。仅清理历史数据这一项收益,就足以让时序、事件类业务选择范围分区。
如果数据表同时存在两个天然的划分维度,例如时间 + 租户,PostgreSQL 支持复合分区同时覆盖两项条件。复合分区功能强大,但会带来较高的运维成本,仅适合单分区键无法满足需求的超大规模数据表。
分区属于架构决策,而非一项可调参数
分区键与普通性能调优参数有着本质区别。分区方案中部分配置后期修改成本很低,但分区键一旦选定几乎不可变更,部署之前需要分清二者差异。
新增范围分区操作简单。次月到来时,新建对应的分区即可,PostgreSQL 会自动路由新数据。还可以借助 pg_partman 配置定时任务自动创建分区,无需人工值守。
哈希分区的分区数量则完全相反。哈希取模、桶的总数决定了每一行数据的存放位置。一旦修改分区数量,所有存量数据都需要重新计算哈希值并迁移,等同于全表拷贝。该操作要么带来停机时间,要么需要设计复杂的在线迁移方案规避停机。对于已经达到分区体量的大表而言,该迁移成本高昂、风险较高,哈希分区的基数配置基本只能一次性选定。
即便是完全更换分区字段,例如从租户哈希分区切换为时间范围分区,本质等同于重建整张数据表。数据表体量较小时迁移成本很低;当数据表存储数十亿行数据后,迁移工作痛苦且困难,而此时恰恰最需要正确的分区策略。
综上,分区属于架构层面的长期决策,而非随手开启的性能开关。选错分区键带来的后果,不只是查询性能小幅下降,而是必须规划、调配人力、测试并申请运维窗口执行数据迁移。应当在数据表体量尚可控的时候选定分区键,一旦数据表膨胀到迁移困难的阈值,再调整分区键将会代价巨大。
分区部署前检查清单
执行建表语句前,对照以下清单完成校验。半天的核查工作,就可以规避后期繁重的数据迁移。
- 提取数据表排名前十的高频查询,确认查询过滤条件包含计划选定的分区键。查询条件不带分区键则无法触发分区裁剪,分区不会带来性能收益。
- 核查候选分区键真实的数据分布,不能仅参考字段唯一值数量。少数取值占据大部分数据行时,该字段做哈希分区一定会产生热点分区。
- 分区策略匹配访问模式:无天然顺序、分布均匀的字段选用哈希分区;基于时间、时效性访问的数据选用范围分区;取值为少量固定分类的数据选用列表分区。
- 在与生产环境配置、数据体量一致的副本库上,使用真实混合查询负载完成测试。一万行数据规模下的分区表现,无法代表十亿行数据时的运行效果。
- 合理控制分区总数。几十至几百个分区属于健康区间;创建成千上万的小分区,会带来查询优化器与运维层面的额外开销。
分区键的业务收益
对于处于业务增长阶段的 PostgreSQL 集群,按月持续写入数据的 SaaS 平台、数据永不停增的金融交易系统,分区键属于收益极高的架构决策。分区键选择正确,数据库可以获得数年的性能缓冲期:查询速度提升、历史数据归档成本降低、分区可独立运维;即便数据量持续上涨,数据库性能依旧保持稳定。分区键选择失误,则后期必须开展数据迁移工作。
好消息是分区键选择属于可落地的确定性决策,而非盲目猜测。业务负载特征指明分区键的方向;数据分布特征决定分区策略;在数据表体量尚小的时候使用真实数据完成测试,即可低成本验证分区方案的正确性。
常见问题解答
Q. 选定 PostgreSQL 分区键时,最核心的判断因素是什么?
核心取决于查询模式。只有查询条件包含分区键,分区裁剪才能跳过无关分区。因此分区键必须是核心查询实际用于过滤的字段。
Q. 哈希分区有时无法提升性能的原因?
哈希分区按照字段取值分配数据,并不会参考每个取值对应的数据行数。少数取值存储绝大部分数据时,哈希运算会将这些高负载数据分配至少量分区,依旧产生热点。哈希分区仅在分区键天然均匀分布时收益明显,例如 UUID、分布均衡的代理 ID。
Q. 数据表上线后还能够修改分区键吗?
可以修改,但改动成本很高。新增范围分区操作简便;修改哈希分区数量或者更换分区字段,所有存量数据都需要重新排布,等同于全表拷贝。大表迁移通常伴随停机风险,因此建议数据表扩容之前就选定正确的分区键。
Q. 所有大表都应当使用相同的分区键吗?
不建议,数据表之间分区键可以不同。每张数据表的查询访问模式各不相同,分区策略应当适配单表负载。按时间查询的数据表适合时间戳范围分区;基于均匀 UUID 访问的数据表适合哈希分区。强制所有数据表使用同一分区方案,部分数据表的分区策略将和真实查询模式脱节。
Q. 正式部署分区前,如何提前识别分区键存在数据倾斜?
按照候选分区键分组统计行数,查看数据分布情况。排名靠前的少数取值占据大量数据,则该字段存在倾斜,哈希分区会生成热点分区。该项校验需要在承载真实业务数据、与生产库配置一致的副本库上完成,不可仅凭唯一值数量下结论,提前排查能够以较低成本解决隐患。
作者:Umair Shahid

浙公网安备 33010602011771号