导读:本期聚焦于缅甸程序员创作的《PostgreSQL数据倾斜如何处理?数据分布优化有哪些实用方案》,敬请观看详情。数据倾斜不只是分布式数据库的专属问题,单机PostgreSQL在索引、并行查询和频繁更新的场景中同样会遇到类似的困扰。当某个字段的取值高度集中时,对应的索引页会变得异常繁忙,查询计划可能错误地高估或低估行数,并行worker之间也会出现负载不均。这篇文章从实际现象出发,先讲清楚倾斜产生的几个典型来源,包括低基数列上的B树索引、不准确的统计信息以及并行扫描时的数据划分方式。然后给出可以落地的排查思路,比如借助pg_stat_user_tables和pg_statistic查看数据分布,用自定义SQL统计高频值占比。最后分别介绍改写查询、调整分布策略、优化索引设计以及通过ANALYZE和扩展统计信息来缓解倾斜的具体方法。文章包含可执行的SQL示例,帮助读者针对不同场景选择合适的处理手段。

PostgreSQL里提到数据倾斜,很多人的第一反应是分布式架构下的事情,比如Greenplum或者Citus里分布键选得不好导致节点负载不均衡。但在单机PostgreSQL中,倾斜同样实实在在地影响查询性能和写入吞吐,只是表现方式不同。它可能藏在一个频繁更新的状态字段上,也可能藏在一张几千万行表里某个取值占了九成的列上。把这些隐藏的倾斜找出来并做出调整,往往比盲目加内存、上更贵的磁盘要有效得多。

PostgreSQL数据倾斜如何处理?数据分布优化有哪些实用方案

先理解倾斜在PostgreSQL中的具体表现

单机PostgreSQL没有分布式节点,数据倾斜主要体现在页面访问热点和查询计划估计偏差上。假设一张订单表有一个状态列,取值只有待支付、已支付、已取消三种,其中已取消占全表的百分之九十。如果在这个状态列上建了B树索引,查询已取消的记录时,索引扫描会命中大量索引项,而这些索引项很可能集中在少数几个叶子页里。并发查询一多,这些叶子页上的锁竞争和缓存争用就会变得非常明显。反过来,查询待支付这样的小众值时,优化器又可能因为统计信息不够精细而错误选择顺序扫描。

另一个典型的倾斜场景是主键使用UUID或者雪花ID。这类键值本身分布比较均匀,但如果写入集中在某个时间窗口,比如日志表按时间递增的序列生成主键,B树索引的右侧叶子页就会成为写入热点。PostgreSQL的B树索引在插入递增键时,新值总是追加到最右侧的叶子页,这本身不是坏事,但当删除和更新混合发生时,页面分裂和空间回收的代价会放大。对于UUID主键,如果使用v4随机版本,索引插入的位置是随机的,虽然写入热点分散了,但缓存局部性变差,B树维护成本也更高。

还有一种容易被忽略的倾斜发生在并行查询中。PostgreSQL从9.6开始支持并行顺序扫描,从14开始支持并行B树索引扫描。并行扫描的核心是让多个worker进程各负责一段数据,但如果数据在物理存储上分布不均,或者过滤条件天然过滤掉了大部分页面,就可能导致某些worker很快做完,而另一些worker还在忙碌。这种隐性的负载不均使得并行加速比不理想,通过EXPLAIN (ANALYZE, VERBOSE)查看每个worker的实际执行时间就能发现端倪。

如何定位数据倾斜和统计信息失真

排查倾斜的第一步不是急着改表结构,而是先确认问题到底出在哪里。PostgreSQL的pg_statistic系统表保存了每个表每一列的统计信息,其中stanullfrac表示空值比例,stavalues和stahistogram则记录了高频值和直方图边界。直接用SQL查这些字段不太直观,更简单的办法是借助pg_stats视图。例如查看某列的高频值及其占比,可以执行:

SELECT attname, most_common_vals, most_common_freqs
FROM pg_stats
WHERE tablename = 'orders' AND attname = 'status';

如果某个值的出现频率超过0.5,说明这个列存在明显的倾斜。在此基础上查询该值的实际行数,与优化器的估计行数做对比,就能判断统计信息是否足够准确。有时候表数据刚发生了大规模更新,但pg_statistic里的信息还是旧的,这时优化器给出的执行计划就会跑偏。对于大表,ANALYZE默认采样30000行,如果倾斜值在采样中没有被充分捕获,估计误差会相当大。此时可以针对特定列调高统计目标:

ALTER TABLE orders ALTER COLUMN status SET STATISTICS 1000;
ANALYZE orders;

把统计目标从默认的100提高到1000,PostgreSQL会采样更多行来建立直方图和高频值列表。对于基数很低、重复值很多的列,建立扩展统计信息也是一种选择,例如使用CREATE STATISTICS收集多列之间的依赖关系。定位倾斜还可以从物理存储层面入手,pg_stat_user_tables视图中的seq_scan和idx_scan可以反映索引的使用情况,结合pg_statio_user_tables中的缓存命中率,能进一步确认是否存在索引页争用。

想要更直观地查看数据分布,可以自己写SQL统计每个取值的行数和占比:

SELECT status, count(*) AS cnt,
       round(count(*)::numeric / sum(count(*)) OVER (), 4) AS ratio
FROM orders
GROUP BY status
ORDER BY cnt DESC;

这个查询对普通规模的表很快,但如果表特别大,全表扫描会比较耗时。可以先利用pg_stats里的高频值信息做初步判断,再决定是否值得对整个表做精确统计。找到倾斜列之后,还要结合具体的查询模式来决定处理方案,不能一上来就想着重建表。

针对不同场景的倾斜处理方案

如果倾斜列是低基数列,并且查询总是过滤某个高频值,最简单的办法是避免在这个列上建立效率低下的索引。对于返回大量行的查询,索引扫描本身就没有意义,顺序扫描加并行执行反而更快。比如状态列已经取消了百分之九十的订单,查询已取消订单时就应该走顺序扫描。可以让优化器为不同取值生成不同计划,但PostgreSQL默认使用同一套统计信息,很难做到按值区分。这种情况下可以考虑使用部分索引,只对低频值建立索引:

CREATE INDEX idx_orders_status_active ON orders (status)
WHERE status IN ('待支付', '已支付');

部分索引的体量比全列索引小得多,维护成本低,查询低频值时优化器也更容易选择它。查询高频值时由于索引不包含这些行,优化器自然会选择顺序扫描。这种思路对状态字段、逻辑删除标记这类倾斜明显的列非常有效。

当倾斜体现在主键或唯一键的写入热点上时,调整键的生成方式比调整数据库参数更根本。对于日志表这类按时间递增写入的场景,可以考虑使用BRIN索引代替B树索引。BRIN索引按物理页面范围记录摘要信息,对于存储顺序与时间顺序一致的列极为高效,索引体积极小,维护开销低:

CREATE INDEX idx_events_created_at ON events USING brin (created_at);

如果表已经存在并且数据分布与物理存储不完全对应,可以先通过CLUSTER命令按时间列重排表,然后再建立BRIN索引。对于UUID主键的写入热点,可以考虑使用UUIDv7这种时间有序的版本,它在保留随机性的同时让B树插入更接近追加模式,减少随机跳转带来的页面分裂。PostgreSQL 18已经内置了UUIDv7生成函数,此前版本可以通过扩展或者应用层生成。

并行查询中的倾斜则需要从数据划分入手。PostgreSQL的并行顺序扫描按照数据块范围划分给不同worker,如果过滤条件导致大部分匹配行集中在某个数据块范围,负载就会不均匀。这种情况下,调整过滤条件或者提前对表进行物理排序都会有帮助。例如一张按用户ID分区的表,如果某个大客户的数据量远超其他客户,并行扫描这个大客户分区时worker之间的数据量差异就很大。可以考虑对这类热分区使用更细粒度的物理划分,或者在查询时人为拆分过滤条件,让多个并行任务各负责一部分数据。

从统计信息与查询改写入手优化

处理倾斜不能只盯着表结构,统计信息的维护和查询改写往往能起到四两拨千斤的效果。对于统计信息严重失真的列,除了提高STATISTICS目标外,还可以在查询中使用SET STATISTICS来临时影响规划器的估计。不过更实用的做法是定期执行ANALYZE,尤其是在大批量数据导入或者删除之后。对于频繁更新的小表,可以考虑开启autovacuum_analyze_threshold参数的自动调优,让统计信息更新的频率跟上数据变化。

查询改写是另一个容易被忽视的维度。假设有一张用户表,其中某个地区代码的取值占比极高,查询该地区用户时走索引反而慢。可以把这类倾斜值单独拎出来处理,比如用UNION ALL把高频值和低频值的查询分开:

SELECT * FROM users WHERE region_code = 'CN' AND status = 'active'
UNION ALL
SELECT * FROM users WHERE region_code != 'CN' AND status = 'active';

这样优化器可以对第一个分支选择顺序扫描,对第二个分支选择索引扫描。当然这种改写只适合倾斜值比较固定的情况,如果倾斜值经常变化,维护起来就比较麻烦。更通用的办法是使用CASE表达式配合表达式索引,将倾斜值映射到不同的索引键空间。

还有一种容易被忽视的倾斜发生在连接操作上。当两个表做JOIN时,如果连接键的分布严重不均,哈希连接中的某些哈希桶会变得特别大。PostgreSQL的哈希连接会把较大的哈希分区分片落盘,倾斜的键会导致单个分片过大,内存无法容纳,进而触发大量的磁盘I/O。针对这种情况,可以考虑在连接前先过滤掉倾斜键,或者使用enable_hashjoin参数临时关闭哈希连接,强制优化器选择合并连接或嵌套循环连接。当然,最根本的解法还是让连接键的分布尽可能均匀,例如在ETL过程中对倾斜键做加盐处理,虽然这会改变查询逻辑,但在数据仓库场景下效果显著。

综合来看,PostgreSQL中的数据倾斜处理是一个需要结合具体查询模式、数据特征和硬件资源来权衡的过程。没有一套方案能解决所有倾斜问题,但掌握了统计信息的查看方法、索引类型的选择逻辑以及查询改写的常见套路,就能够在遇到性能瓶颈时快速定位并给出可行的优化方向。

PostgreSQL数据倾斜数据分布优化并行查询修改时间:2026-09-18 02:21:18

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0918/58634.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。