SQL编程高级数据库工程师(PostgreSQL)

哪种架构约束阻止 **PostgreSQL** 的查询规划器在分区键通过 **STABLE** 函数过滤时进行分区修剪,尽管该函数在事务中返回常量?

用 Hintsage AI 助手通过面试

问题的回答。

PostgreSQL 在第 10 版中引入了声明式分区,以取代繁琐的基于继承的分区模型。查询规划器在规划阶段通过将查询谓词与分区边界进行比较来执行静态分区修剪,但它要求表达式在计划时可评估为常量,以确定可以消除哪些分区。

核心架构约束是 STABLE 函数(例如 now()current_timestamp)在规划时不会被评估,因为它们的结果可能在规划与执行之间或甚至在查询执行期间有所不同。因此,规划器将涉及这些函数的谓词视为黑盒,无法证明某些分区无法包含匹配的行,这迫使其扫描所有分区。

解决方案是将谓词重写为使用 IMMUTABLE 函数或字面常量,或依赖于 PostgreSQL 11 及更高版本中的运行时分区修剪。通过将 enable_partition_pruning 设置为 on,执行器在执行时根据分区边界评估 STABLE 函数结果,动态跳过初始规划阶段后不相关的分区。

生活中的情况

一家金融分析公司根据 execution_time 列使用每日范围对交易表按 TIMESTAMPTZ 进行分区,以管理数TB的刻度数据。分析师经常查询最近的活动,使用 WHERE execution_time >= now() - interval '1 hour',但观察到这些查询遭遇了灾难性的性能下降,扫描了所有 365 个每日分区,而不仅仅是最新的一个。

考虑的第一种方法是让应用层计算时间戳边界并将其作为字面常量注入。这使得立即静态修剪成为可能,并将查询时间从 45 秒减少到 80 毫秒。然而,它破坏了嵌入第三方 BI 工具中的现有 SQL 查询,这些工具无法轻松修改。

第二种方法涉及创建一个自定义的不可变函数,该函数返回基于当前日期的固定时间戳。这被拒绝了,因为如果数据库事务在午夜边界打开,可能会产生不正确的结果,违反了在事务中 now() 提供的 STABLE 合同。这种违规可能导致在规划器缓存过时的分区范围时,静默地出现不正确的查询结果。

所选解决方案利用了 PostgreSQL 12 的运行时分区修剪能力。数据库管理员确保启用了 enable_partition_pruning,并重构应用程序以使用预处理语句,将时间边界作为参数传递,而不是连接到 SQL 字符串中。这使得执行器能够在执行时间动态修剪分区,使用实际参数值实现近乎最佳的性能,而无需更改遗留报告工具中的 SQL 文本生成。

候选人常常忽视的内容

继承表的约束排除机制与本机分区修剪有何不同,为什么前者需要显式 GUC 配置,而后者则不需要?

约束排除是声明式分区之前使用的分区方法,依靠子表上的 CHECK 约束来证明表中不能包含相关行。因为在规划期间对每个继承表评估这些约束在存在数百个表时是昂贵的,所以它由 constraint_exclusion 参数控制,该参数默认为 partition(仅在通过继承查询时进行检查)。本机分区修剪使用了规划器中的专用数据结构,直接理解分区层级,使其更快并始终启用,无需对 GUC 进行调整以确保功能正确。

为什么在通过修改分区键将行移动到分区之间时,PostgreSQL 内部执行 DELETEINSERT 而不是就地更新,这带来了什么触发器影响?

因为每个分区都是一个独立的堆关系,具有单独的物理存储,因此更改分区键需要将元组从一个文件移动到另一个文件。PostgreSQL 通过从源分区删除行并将其插入到目标分区来实现这个转换。这意味着行级 BEFORE DELETEAFTER DELETEBEFORE INSERTAFTER INSERT 触发器在看似单个 UPDATE 操作期间都被触发。此外,逻辑复制将其作为两个单独的 WAL 条目(删除和插入)流,这可能在复制身份配置不当的情况下导致订户冲突。

当向具有 DEFAULT 分区的表附加新分区时,会发生哪些特定的锁定和验证开销,以及如何避免对默认分区的完全扫描?

在将新分区附加到包含 DEFAULT 分区的范围或列表分区表时,PostgreSQL 必须扫描整个 DEFAULT 分区以验证是否存在应该属于新的、更具体分区的行。此验证扫描在分区表上获取 ACCESS EXCLUSIVE 锁,对大型默认分区可能需要几个小时。为了避免这种情况,应在附加新分区之前分离 DEFAULT 分区,然后在确保没有冲突行存在后才重新附加 DEFAULT 分区,或者使用 CREATE TABLE ... PARTITION OF 创建一个新的空分区,并使用 INSERT ... SELECT 进行数据迁移,同时使用 WHERE 子句过滤特定分区范围,从而绕过对默认分区内容的验证扫描。