PostgreSQL 18 preview - 优化分区表规划阶段性能
PostgreSQL 18 preview - 优化分区表规划阶段性能
这个补丁主要优化了 PostgreSQL 查询规划器在处理包含大量分区的分区表时的性能。它通过改变存储和查找代表子分区的“等价成员”(Equivalence Member)的方式,解决了因子分区数量增加而导致的规划(plan)时间急剧变慢(二次方级别)的问题,显著提升了涉及多分区的查询规划速度。
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=d69d45a5a956e930dc91b3ca09a0188bf9fe2176
Speedup child EquivalenceMember lookup in planner master github/master
author David Rowley <[email protected]>
Tue, 8 Apr 2025 06:09:57 +0000 (18:09 +1200)
committer David Rowley <[email protected]>
Tue, 8 Apr 2025 06:09:57 +0000 (18:09 +1200)
commit d69d45a5a956e930dc91b3ca09a0188bf9fe2176
tree 3d147a6daa5c0d8e1538c5d309f069aca4b71168 tree
parent 105b2cb336173d7c62e26ad104682175ddad4cff commit | diff
Speedup child EquivalenceMember lookup in planner When planning queries to partitioned tables, we clone all
EquivalenceMembers belonging to the partitioned table into em_is_child
EquivalenceMembers for each non-pruned partition. For partitioned tables
with large numbers of partitions, this meant the ec_members list could
become large and code searching that list would become slow. Effectively,
the more partitions which were present, the more searches needed to be
performed for operations such as find_ec_member_matching_expr() during
create_plan() and the more partitions present, the longer these searches
would take, i.e., a quadratic slowdown.
To fix this, here we adjust how we store EquivalenceMembers for
em_is_child members. Instead of storing these directly in ec_members,
these are now stored in a new array of Lists in the EquivalenceClass,
which is indexed by the relid. When we want to find EquivalenceMembers
belonging to a certain child relation, we can narrow the search to the
array element for that relation.
To make EquivalenceMember lookup easier and to reduce the amount of code
change, this commit provides a pair of functions to allow iteration over
the EquivalenceMembers of an EC which also handles finding the child
members, if required. Callers that never need to look at child members
can remain using the foreach loop over ec_members, which will now often
be faster due to only parent-level members being stored there.
The actual performance increases here are highly dependent on the number
of partitions and the query being planned. Performance increases can be
visible with as few as 8 partitions, but the speedup is marginal for
such low numbers of partitions. The speedups become much more visible
with a few dozen to hundreds of partitions. With some tested queries
using 56 partitions, the planner was around 3x faster than before. For
use cases with thousands of partitions, these are likely to become
significantly faster. Some testing has shown planner speedups of 60x or
more with 8192 partitions.
Author: Yuya Watari <[email protected]>
Co-authored-by: David Rowley <[email protected]>
Reviewed-by: David Rowley <[email protected]>
Reviewed-by: Tom Lane <[email protected]>
Reviewed-by: Andrey Lepikhov <[email protected]>
Reviewed-by: Alena Rybakina <[email protected]>
Reviewed-by: Dmitry Dolgov <[email protected]>
Reviewed-by: Amit Langote <[email protected]>
Reviewed-by: Ashutosh Bapat <[email protected]>
Tested-by: Thom Brown <[email protected]>
Tested-by: newtglobal postgresql_contributors <[email protected]>
Discussion: https://postgr.es/m/CAJ2pMkZNCgoUKSE%2B_5LthD%2BKbXKvq6h2hQN8Esxpxd%2Bcxmgomg%40mail.gmail.com
AI 解读
好的,我们来解读一下这个 PostgreSQL 的补丁(Patch)。
代码作用概述 (Patch Summary)
这个补丁主要优化了 PostgreSQL 查询规划器在处理包含大量分区的分区表时的性能。它通过改变存储和查找代表子分区的“等价成员”(Equivalence Member)的方式,解决了因子分区数量增加而导致的规划时间急剧变慢(二次方级别)的问题,显著提升了涉及多分区的查询规划速度。
详细解读 (Detailed Interpretation)
问题背景 (The Problem):
在 PostgreSQL 中,查询优化器使用“等价类”(Equivalence Class, EC)来表示查询中相互等价的表达式(例如, WHERE a = b AND b = 10,那么a、b和常量10就属于同一个等价类)。每个表达式在等价类中表示为一个“等价成员”(Equivalence Member, EM)。当查询涉及到分区表时,对于每个未被“分区剪枝”(Partition Pruning)优化掉的子分区,规划器会为该子分区克隆一份父表对应的等价成员,并标记为 em_is_child。关键问题在于旧的实现方式: 所有这些为子分区克隆出来的 em_is_child成员,都被直接添加到了父等价类(EC)的ec_members这个列表中。后果: 如果一个分区表有非常多的子分区(比如几百、几千个), ec_members列表就会变得异常庞大。当规划过程需要查找某个特定的等价成员时(例如,find_ec_member_matching_expr()函数在create_plan()阶段执行此操作),它需要在整个巨大的ec_members列表中进行搜索。性能瓶颈: 分区越多,需要创建的 em_is_child成员就越多(线性增加),同时每次搜索ec_members列表所需的时间也越长(因为列表变长了,也是线性增加)。这导致了整体规划时间的二次方级别(quadratic)的性能下降。分区越多,规划越慢,而且慢得不成比例。
解决方案 (The Solution):
这个补丁的核心改动是调整了 em_is_child等价成员的存储位置。新的存储方式: 这些子分区的等价成员不再直接放入 ec_members列表。取而代之的是,在等价类(EquivalenceClass)结构中增加了一个新的列表数组(an array of Lists)。索引机制: 这个新的数组使用子分区的关系 ID ( relid) 作为索引。也就是说,属于child_relid_A的所有em_is_child成员现在都存储在这个数组中索引为A的那个列表里;属于child_relid_B的则存储在索引为B的列表里,以此类推。优势: 当需要查找属于某个特定子分区(比如 relid为X)的等价成员时,规划器现在可以直接定位到新数组中索引为X的那个列表进行搜索。这个列表只包含该特定子分区的成员,规模远小于原来混合所有子分区的ec_members列表,极大地缩小了搜索范围,提高了查找效率。
实现细节与兼容性 (Implementation Details & Compatibility):
那些需要同时查看父成员和子成员的代码,需要修改为使用这对新的迭代函数。 那些从不需要关心子分区成员( em_is_child)的代码,可以保持不变,继续使用简单的foreach循环遍历ec_members列表。并且,由于ec_members现在只包含父级成员,这个列表变小了,这些旧代码的遍历反而会变得更快。
为了方便代码的修改和使用新的存储结构,补丁引入了一对新的辅助函数。 迭代函数: 这对函数允许代码迭代遍历一个等价类(EC)中的所有相关等价成员(EM)。关键在于,这些函数封装了细节,能够根据需要同时处理仍然存储在 ec_members中的父级成员和存储在新数组中的子级成员。对现有代码的影响:
性能提升 (Performance Gains):
补丁带来的实际性能提升高度依赖于具体查询涉及的分区数量以及查询本身的复杂度。 少量分区: 在分区数量很少(例如 8 个)的情况下,也能观察到性能提升,但提升幅度不大。 中等数量分区: 当分区数量达到几十个到几百个时,速度提升变得非常明显。根据测试,对于某些涉及 56 个分区的查询,规划器的速度比之前快了大约 3 倍。 大量分区: 对于拥有数千个分区的用例,规划速度预计会显著加快。一些测试表明,在有 8192 个分区的情况下,规划器的速度提升了 60 倍甚至更多。
总结:
这个补丁通过引入一种更智能的数据结构(按 relid 索引的列表数组)来分开存储父表和子分区的等价成员,有效解决了因子分区数量过多导致规划时间呈二次方增长的性能瓶颈。它不仅大幅提升了处理大规模分区表的查询规划效率,还通过辅助函数保持了代码的可维护性,并让不关心子分区的代码路径也从中受益(ec_members 列表变小)。对于使用大量分区的 PostgreSQL 用户来说,这是一个非常重要的性能优化。