Richard Yen:索引扫描只是故事的一半
内容提要
客户将PostgreSQL迁移到Azure后,同一查询在冷缓存下从几十毫秒变慢至1-2秒。排查发现索引无膨胀或碎片、存储正常,真正原因是并行加载数据使相关行分散在更多堆页:某查询返回200行,慢环境占101个堆页,快环境仅5个。索引扫描只负责定位,堆页访问才是瓶颈。可用ctid统计不同堆页数进行诊断。VACUUM FULL无效,需按索引键重排堆(如CLUSTER或pg_repack),并注意后续写入会再次破坏局部性。
延伸解读
索引扫描的隐藏成本:堆页访问
文章指出,索引扫描仅负责定位匹配行,真正的开销在于访问堆页。同样返回200行,若分散在101个堆页,相比仅5个堆页,I/O工作量相差约20倍。执行计划都显示Index Scan,但实际工作量可能天差地别。因此,评估查询性能时不能只看扫描类型,还需关注缓冲区命中和读取次数,以及堆页的物理分布。
诊断堆页分散:用ctid统计不同页数
文章推荐使用ctid系统列提取块号,统计查询结果涉及的不同堆页数量。示例中,慢环境返回200行占用101个堆页,快环境仅5个。该方法直接暴露结果集的堆足迹,比全局相关性统计更精确。注意ctid会随更新或重写变化,仅作诊断用途;分区表需同时考虑tableoid。
并行加载导致行分散,VACUUM FULL无效
客户在Azure环境并行加载数据,提高了吞吐量,但并发插入使相关行交错分布,破坏了局部性。重建索引不会重排堆表,VACUUM FULL只回收死元组空间,不按索引键排序,因此无效。解决方案是使用CLUSTER或pg_repack按索引键重排堆表,但需注意锁和空间要求,且后续写入会再次破坏局部性。
迁移后性能调优:关注物理布局与缓存
文章强调,迁移相同行和索引不保证相同物理布局。冷缓存下性能差异暴露了堆页分散问题,而重复执行因缓存掩盖了成本。诊断时需在可比缓存条件下测试,且诊断查询本身会预热页面。重排后应刷新统计、重复页数统计并比较执行计划。长期需关注加载策略和持续写入对局部性的影响。
Q&A
PostgreSQL迁移到Azure后,同样的查询为什么冷缓存下变慢很多?
因为并行加载数据导致相关行分散在更多的堆页中。索引扫描虽然能定位行,但需要访问的堆页数量大幅增加,冷缓存时这些页需要从存储读取,从而变慢。
如何诊断PostgreSQL中索引扫描实际访问了多少堆页?
可以使用ctid系统列提取块号,统计不同堆页的数量。例如:SELECT count(*) AS rows_found, count(DISTINCT split_part(trim(both '()' FROM ctid::text), ',', 1)) AS distinct_heap_pages FROM table WHERE key = value; 对于分区表,需统计(tableoid, block_number)对。
pg_stats中的correlation字段能说明什么?
correlation表示列的逻辑顺序与表物理顺序的相关性,值接近1或-1表示强相关,接近0表示弱相关。它可用于估计索引扫描成本,但只是采样统计,不能精确反映特定查询涉及的堆页数。
VACUUM FULL能解决堆页分散导致的性能问题吗?
不能。VACUUM FULL重写表以回收死元组空间并压缩堆,但不会按索引键排序行,因此相关行仍可能分散。它可能减少页数(如果有膨胀),但无法恢复局部性。
有哪些方法可以重新组织堆表以提高局部性?
可以使用CLUSTER按索引顺序重写表,但会获取ACCESS EXCLUSIVE锁,需谨慎安排。也可以使用pg_repack的--order-by选项在线重组。操作后需运行ANALYZE更新统计,并注意后续写入可能再次破坏局部性。
并行加载数据一定会导致表堆分散吗?
不一定。并行加载本身不保证分散,结果取决于工作划分和插入如何到达堆。但在该客户案例中,并行加载方法解释了观察到的布局差异。