当前位置: 首页 > news >正文

【PostgreSQL内核学习:WindowAgg 节点对 Tuplestore 的复用机制】

PostgreSQL内核学习:WindowAgg 节点对 Tuplestore 的复用机制

  • 背景
  • 补丁概述
    • 提交信息
    • 提交描述
  • 优化目的
    • 举例说明
      • 测试用例
      • 优化分析:优化前后对比
        • 优化前
        • 优化后
  • 源码解读
    • begin_partition 函数
      • 获取外部计划状态和窗口函数数量
      • 重置状态标志和计数器
      • 清空元组槽
      • 处理第一个分区
      • 创建或复用 tuplestore
      • 重置 next_partition 标志
      • 重置聚合函数状态
      • 重置窗口函数状态
      • 存储第一个元组
    • prepare_tuplestore 函数
      • 获取计划节点、框架选项和窗口函数数量
      • 断言 tuplestore 未创建
      • 创建新的 tuplestore
      • 初始化当前读指针
      • 为聚合函数创建读指针
      • 为窗口函数创建读指针
      • 为 RANGE 或 GROUPS 模式创建框架指针
      • 为排除子句创建组尾部指针
    • 执行层改动
      • ExecInitWindowAgg:
      • ExecWindowAgg:
      • ExecEndWindowAgg:
  • 性能测试
    • 统计汇总
      • 平均执行时间:
      • 平均性能提升:
      • 范围:
      • 总结

声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 postgresql-18 beta2 的开源代码和《PostgresSQL数据库内核分析》一书

背景

  在 PostgreSQL 中,WindowAgg 节点是用于处理 SQL 窗口函数的核心执行节点。窗口函数(如 ROW_NUMBER(), RANK(), SUM() OVER() 等)需要在数据分区内对行进行排序、存储和访问,因此对性能敏感,尤其是当分区数量多或分区内行数少时,初始化和清理的开销可能显著影响查询性能。
  补丁(908a968612f9ed61911d8ca0a185b262b82f1269)针对 PostgreSQLWindowAgg 节点优化了其对 tuplestore(元组存储)组件的使用方式,通过减少重复的初始化和销毁操作显著提升了性能,特别是在分区数量极多但每个分区行数较少的情况下。本分析将详细探讨该补丁的优化内容、实现方式及预期效果。

补丁概述

提交信息

  下面为本次优化的提交信息,hash值为:908a968612f9ed61911d8ca0a185b262b82f1269
在这里插入图片描述

提交描述

  该补丁优化了 WindowAgg 节点在处理分区切换时的 tuplestore 管理逻辑。原始实现中,每当 WindowAgg 完成一个分区的处理后,会调用 tuplestore_end() 销毁整个 tuplestore,并在处理下一个分区时重新调用 tuplestore_begin_heap() 创建新的 tuplestore,同时重新设置所有读指针(read pointers)。这种方式在分区数量多但每个分区行数少的情况下会导致显著的性能开销,因为频繁的内存分配(malloc)和释放(free)操作带来了不必要的 overhead
  补丁的主要改进是将 tuplestore创建读指针的初始化操作移到查询开始时执行一次,而不是在每个分区切换时重复执行。具体做法包括:

  1. 引入 prepare_tuplestore 函数:将 tuplestore 的创建和读指针初始化逻辑抽取到一个独立的 pg_noinline 函数中,仅在第一次需要时调用。
  2. 使用 tuplestore_clear:在分区切换时,不再销毁 tuplestore,而是调用 tuplestore_clear() 清空存储的元组并重置读指针到起始位置,从而复用已分配的内存。
  3. 添加 next_partition 标志:在 WindowAggState 结构体中新增一个布尔字段 next_partition,用于标记是否需要调用 begin_partition 来处理新分区。
  4. 结构体字段重排:为避免因新增 next_partition 字段导致结构体大小增加,补丁对 WindowAggState 结构体的字段顺序进行了调整,将与 runcondition 相关的字段重新排列,利用现有的填充字节(padding)容纳新字段,从而避免性能回归。

优化目的

  补丁的主要目标是减少 WindowAgg处理多个分区时的性能开销,特别是针对以下场景:

  • 高分区数、低行数:当查询涉及大量分区(例如,PARTITION BY 列值变化频繁),但每个分区包含的行数较少时,原始实现中频繁的 tuplestore 创建和销毁会导致显著的性能瓶颈
  • 内存管理效率:通过复用 tuplestore 和读指针,减少 mallocfree 的调用,从而降低 CPU 和内存管理的开销。
  • 避免结构体膨胀:通过字段重排,确保新增的 next_partition 字段不会导致 WindowAggState 结构体大小增加,从而避免因内存对齐或缓存命中率下降带来的性能回归。

举例说明

  为了分析 PostgreSQLWindowAgg 优化补丁(Optimize-WindowAgg-s-use-of-tuplestores)的效果,我将提供一个完整的测试用例,包含创建表、插入数据、执行查询以及测量优化前后性能的步骤。测试用例基于补丁描述的场景:100万个分区,每个分区1。之后,我将分析优化前后的差异,并量化性能提升,重点关注补丁对 tuplestore 使用的改进。

测试用例

-- 创建表
CREATE TABLE data_table (id SERIAL PRIMARY KEY,category INTEGER NOT NULL,value INTEGER NOT NULL
);-- 插入数据
-- 测试需要100万个分区,每个分区1行。
-- 我们使用 generate_series 生成100万行,
-- 每行具有唯一的 category 值(从1到1,000,000),
-- value 列为简单起见设置为与 category 相同。
INSERT INTO data_table (category, value)
SELECT gs, gs
FROM generate_series(1, 1000000) gs;-- 查询
-- 按 category 分区,生成100万个分区(因为每个 category 值唯一)。
-- 在每个分区内根据 value 排序,为每行分配 ROW_NUMBER()。
-- WHERE 子句中的 generate_series(1, 1000000) 确保包含所有行,符合每个分区1行的测试场景。
EXPLAIN ANALYZE
SELECTid,ROW_NUMBER() OVER (PARTITION BY category ORDER BY value) AS row_num
FROM data_table
WHERE category IN (SELECT generate_series(1, 1000000));QUERY PLAN-------------------------------------------------------------------------------------------------------------------------------
----------WindowAgg  (cost=86973.94..96973.94 rows=500000 width=20) (actual time=4616.186..6083.283 rows=1000000 loops=1)->  Sort  (cost=86973.94..88223.94 rows=500000 width=12) (actual time=4616.174..4934.341 rows=1000000 loops=1)Sort Key: data_table.category, data_table.valueSort Method: external merge  Disk: 21616kB->  Hash Join  (cost=7504.52..31098.02 rows=500000 width=12) (actual time=2437.231..4149.162 rows=1000000 loops=1)Hash Cond: (data_table.category = (generate_series(1, 1000000)))->  Seq Scan on data_table  (cost=0.00..15406.00 rows=1000000 width=12) (actual time=0.066..158.038 rows=1000000loops=1)->  Hash  (cost=7502.02..7502.02 rows=200 width=4) (actual time=2437.048..2437.050 rows=1000000 loops=1)Buckets: 262144 (originally 1024)  Batches: 8 (originally 1)  Memory Usage: 6446kB->  HashAggregate  (cost=7500.02..7502.02 rows=200 width=4) (actual time=734.971..2057.769 rows=1000000 lo
ops=1)Group Key: generate_series(1, 1000000)Batches: 85  Memory Usage: 11073kB  Disk Usage: 23512kB->  ProjectSet  (cost=0.00..5000.02 rows=1000000 width=4) (actual time=0.004..142.272 rows=1000000 l
oops=1)->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=1)Planning Time: 0.323 msExecution Time: 6192.450 ms
(16 rows)

优化分析:优化前后对比

优化前
  • 行为:原始实现中,WindowAgg 在每个分区结束时调用 tuplestore_end() 销毁 tuplestore,并在下一个分区开始时调用 tuplestore_begin_heap() 创建新的 tuplestore,同时为窗口函数和聚合函数重新分配读指针
  • 开销
    • 内存分配100万个分区每次都触发 tuplestore_end()tuplestore_begin_heap(),涉及多次 mallocfree 调用。对于小分区(1行),内存管理开销占据主导。
    • 读指针设置:每个分区需要为 ROW_NUMBER() 等函数重新分配和初始化读指针,增加 CPU 开销。
    • 性能影响:补丁指出,对于100万个分区、每个分区1行的查询,malloc/free 和指针设置的开销显著,导致执行时间较长。
优化后
  • 行为:补丁通过以下方式优化 WindowAgg
    • 在查询开始时通过 prepare_tuplestore 函数一次性创建 tuplestore 和读指针。
    • 在分区切换时使用 tuplestore_clear() 重置 tuplestore,保留分配的内存和读指针。
    • 新增 next_partition 标志控制 begin_partition 的调用,避免重复初始化。
    • 重排 WindowAggState 结构体字段,避免因新增字段导致结构体大小增加,从而防止缓存相关的性能回归。
  • 优势:
    • 降低内存开销tuplestore_clear() 重置 tuplestore 而无需释放内存,仅在查询开始时分配一次,极大减少 malloc/free 调用。
    • 更快分区切换:通过 tuplestore_clear() 重置读指针比重新分配更快,因为它仅更新内部状态。
    • 无结构体大小增加:字段重排利用现有填充字节,确保结构体大小不变,避免缓存未命中或内存效率下降。

源码解读

begin_partition 函数

  begin_partitionWindowAgg 节点的核心函数,负责为新的分区初始化状态并开始缓冲其行WindowAggState *winstate 是窗口聚合的状态结构体,包含所有与窗口函数执行相关的上下文信息。

/** begin_partition* Start buffering rows of the next partition.* 开始缓冲下一个分区的行。*/
static void
begin_partition(WindowAggState *winstate)
{// 获取外部计划的状态,用于读取输入元组PlanState  *outerPlan = outerPlanState(winstate);// 获取窗口函数的数量int			numfuncs = winstate->numfuncs;// 重置分区是否已全部存储的标志为 falsewinstate->partition_spooled = false;// 重置框架头部是否有效的标志为 falsewinstate->framehead_valid = false;// 重置框架尾部是否有效的标志为 falsewinstate->frametail_valid = false;// 重置组尾部是否有效的标志为 falsewinstate->grouptail_valid = false;// 重置已存储的行数计数器为 0winstate->spooled_rows = 0;// 重置当前行位置为 0winstate->currentpos = 0;// 重置框架头部位置为 0winstate->frameheadpos = 0;// 重置框架尾部位置为 0winstate->frametailpos = 0;// 重置当前组编号为 0winstate->currentgroup = 0;// 重置框架头部组编号为 0winstate->frameheadgroup = 0;// 重置框架尾部组编号为 0winstate->frametailgroup = 0;// 重置组头部位置为 0winstate->groupheadpos = 0;// 重置组尾部位置为 -1(将在 update_grouptailpos 中更新)winstate->grouptailpos = -1;	/* see update_grouptailpos */// 清空聚合行槽ExecClearTuple(winstate->agg_row_slot);// 如果框架头部槽存在,清空它if (winstate->framehead_slot)ExecClearTuple(winstate->framehead_slot);// 如果框架尾部槽存在,清空它if (winstate->frametail_slot)ExecClearTuple(winstate->frametail_slot);/** If this is the very first partition, we need to fetch the first input* row to store in first_part_slot.* 如果这是第一个分区,需要获取第一个输入行并存储到 first_part_slot 中。*/if (TupIsNull(winstate->first_part_slot)){// 从外部计划获取一个输入元组TupleTableSlot *outerslot = ExecProcNode(outerPlan);// 如果输入元组非空,将其复制到 first_part_slotif (!TupIsNull(outerslot))ExecCopySlot(winstate->first_part_slot, outerslot);else{/* outer plan is empty, so we have nothing to do *//* 外部计划为空,无需处理 */winstate->partition_spooled = true;winstate->more_partitions = false;return;}}/* Create new tuplestore if not done already. *//* 如果尚未创建 tuplestore,则创建新的 tuplestore */if (unlikely(winstate->buffer == NULL))prepare_tuplestore(winstate);// 重置 next_partition 标志为 false,表示已处理分区切换winstate->next_partition = false;// 如果存在聚合函数(numaggs > 0)if (winstate->numaggs > 0){// 获取聚合函数的窗口对象WindowObject agg_winobj = winstate->agg_winobj;/* reset mark and see positions for aggregate functions *//* 重置聚合函数的标记和查找位置 */agg_winobj->markpos = -1;agg_winobj->seekpos = -1;/* Also reset the row counters for aggregates *//* 重置聚合函数的行计数器 */winstate->aggregatedbase = 0;winstate->aggregatedupto = 0;}/* reset mark and seek positions for each real window function *//* 重置每个真实窗口函数的标记和查找位置 */for (int i = 0; i < numfuncs; i++){// 获取当前窗口函数的状态WindowStatePerFunc perfuncstate = &(winstate->perfunc[i]);// 如果不是普通聚合函数(即为窗口函数)if (!perfuncstate->plain_agg){// 获取窗口函数的窗口对象WindowObject winobj = perfuncstate->winobj;// 重置标记和查找位置winobj->markpos = -1;winobj->seekpos = -1;}}/** Store the first tuple into the tuplestore (it's always available now;* we either read it above, or saved it at the end of previous partition)* 将第一个元组存储到 tuplestore 中(现在总是可用,要么在上面读取,要么在上一个分区结束时保存)*/tuplestore_puttupleslot(winstate->buffer, winstate->first_part_slot);// 增加已存储的行数计数winstate->spooled_rows++;
}

获取外部计划状态和窗口函数数量

PlanState  *outerPlan = outerPlanState(winstate);
int			numfuncs = winstate->numfuncs;

作用:

  • outerPlanState(winstate) 获取外部计划节点(例如子查询或扫描节点),用于读取输入元组。
  • numfuncs 获取查询中定义的窗口函数数量(例如 ROW_NUMBER()),用于后续循环处理。

意义:为读取输入元组和处理窗口函数做准备。

重置状态标志和计数器

    // 重置分区是否已全部存储的标志为 falsewinstate->partition_spooled = false;// 重置框架头部是否有效的标志为 falsewinstate->framehead_valid = false;// 重置框架尾部是否有效的标志为 falsewinstate->frametail_valid = false;// 重置组尾部是否有效的标志为 falsewinstate->grouptail_valid = false;// 重置已存储的行数计数器为 0winstate->spooled_rows = 0;// 重置当前行位置为 0winstate->currentpos = 0;// 重置框架头部位置为 0winstate->frameheadpos = 0;// 重置框架尾部位置为 0winstate->frametailpos = 0;// 重置当前组编号为 0winstate->currentgroup = 0;// 重置框架头部组编号为 0winstate->frameheadgroup = 0;// 重置框架尾部组编号为 0winstate->frametailgroup = 0;// 重置组头部位置为 0winstate->groupheadpos = 0;// 重置组尾部位置为 -1(将在 update_grouptailpos 中更新)winstate->grouptailpos = -1;	/* see update_grouptailpos */

作用

  • 重置分区相关的状态标志
    • partition_spooled:表示当前分区是否已完全存储到 tuplestore
    • framehead_validframetail_valid:表示框架(frame)头部和尾部的位置是否有效。
    • grouptail_valid:表示组尾部位置是否有效(用于 GROUPS 模式)。
  • 重置计数器
    • spooled_rows:已存储到 tuplestore 的行数。
    • currentpos:当前处理的行位置。
    • frameheadposframetailpos:框架头部和尾部的位置。
    • currentgroup, frameheadgroup, frametailgroup:当前组、框架头部和尾部的组编号(用于 GROUPS 模式)。
    • groupheadposgrouptailpos:组的头部和尾部位置。

意义:为新分区初始化状态,确保所有位置和计数器从零开始,避免旧分区的数据干扰

清空元组槽

ExecClearTuple(winstate->agg_row_slot);
if (winstate->framehead_slot)ExecClearTuple(winstate->framehead_slot);
if (winstate->frametail_slot)ExecClearTuple(winstate->frametail_slot);

作用

  • ExecClearTuple 清空指定的元组槽(agg_row_slot 用于聚合,framehead_slotframetail_slot 用于框架边界)。
  • 使用条件检查确保仅在槽存在时清空,防止无效操作。

意义:确保元组槽不保留旧分区的元组,避免数据混淆。

处理第一个分区

if (TupIsNull(winstate->first_part_slot))
{TupleTableSlot *outerslot = ExecProcNode(outerPlan);if (!TupIsNull(outerslot))ExecCopySlot(winstate->first_part_slot, outerslot);else{winstate->partition_spooled = true;winstate->more_partitions = false;return;}
}

作用

  • 检查 first_part_slot 是否为空(表示这是查询的第一个分区)。
  • 如果为空,从 outerPlan 获取第一个输入元组(outerslot)。
  • 如果 outerslot 非空,复制到 first_part_slot;否则,标记分区已完成(partition_spooled = true)且无更多分区(more_partitions = false),然后返回。

意义:为第一个分区初始化,确保有可用的元组用于后续存储。

创建或复用 tuplestore

if (unlikely(winstate->buffer == NULL))prepare_tuplestore(winstate);

作用

  • 检查是否需要创建新的 tuplestorebuffer == NULL 表示尚未创建)。
  • 如果需要,调用 prepare_tuplestore 创建 tuplestore 并初始化读指针。
  • unlikely 提示编译器此条件通常为假(优化分支预测,因为补丁确保 tuplestore 通常已创建)。

优化背景:补丁的关键改进是仅在查询开始时创建一次 tuplestore,后续分区通过 tuplestore_clear() 复用,避免重复分配

重置 next_partition 标志

winstate->next_partition = false;
  • 作用:将 next_partition 标志置为 false,表示已处理分区切换,准备处理当前分区。
  • 意义next_partition补丁新增的标志,用于控制 begin_partition 的调用逻辑,确保不重复初始化 tuplestore

重置聚合函数状态

if (winstate->numaggs > 0)
{WindowObject agg_winobj = winstate->agg_winobj;agg_winobj->markpos = -1;agg_winobj->seekpos = -1;winstate->aggregatedbase = 0;winstate->aggregatedupto = 0;
}

作用

  • 如果存在聚合函数(numaggs > 0),获取聚合窗口对象(agg_winobj)。
  • 重置聚合函数的标记位置(markpos)和查找位置(seekpos)为 -1,表示未标记或定位。
  • 重置聚合的行计数器:aggregatedbase(起始行)和 aggregatedupto(已处理行)。

意义:为新分区的聚合函数初始化状态,确保聚合计算从头开始。

重置窗口函数状态

for (int i = 0; i < numfuncs; i++)
{WindowStatePerFunc perfuncstate = &(winstate->perfunc[i]);if (!perfuncstate->plain_agg){WindowObject winobj = perfuncstate->winobj;winobj->markpos = -1;winobj->seekpos = -1;}
}

作用

  • 遍历所有窗口函数(numfuncs 个)。
  • 对于非普通聚合函数(!plain_agg,即真正的窗口函数,如 ROW_NUMBER()),获取窗口对象(winobj)。
  • 重置窗口函数的标记位置(markpos)和查找位置(seekpos)为 -1

意义:确保每个窗口函数的读指针状态为初始值,准备处理新分区的数据。

存储第一个元组

tuplestore_puttupleslot(winstate->buffer, winstate->first_part_slot);
winstate->spooled_rows++;

作用

  • first_part_slot 中的元组存储到 tuplestore(buffer)中。
  • 增加 spooled_rows 计数,表示已存储的行数加1

意义:将分区的第一个元组存入 tuplestore,开始缓冲分区数据。first_part_slot 保证始终有可用元组(要么从 outerPlan 读取,要么从上一分区保存)。

prepare_tuplestore 函数

  prepare_tuplestore 初始化 tuplestore 以及窗口函数和聚合函数所需的读指针,基于 WindowAggStateframeOptions 配置。pg_noinline 确保函数代码不内联到调用者,减少调用函数(如 begin_partition)的代码膨胀,因为此函数仅调用一次。
  补丁引入此函数,将 tuplestore 创建逻辑集中化,只在查询开始时执行一次,避免每个分区重复创建

/** prepare_tuplestore* 准备 tuplestore 以及 WindowAggState 的 frameOptions 所需的所有读指针。** 注意:使用 pg_noinline 以避免调用函数因一次性代码而膨胀。*/
static pg_noinline void
prepare_tuplestore(WindowAggState *winstate)
{// 获取 WindowAgg 计划节点WindowAgg  *node = (WindowAgg *) winstate->ss.ps.plan;// 获取窗口框架选项int			frameOptions = winstate->frameOptions;// 获取窗口函数数量int			numfuncs = winstate->numfuncs;/* 如果 tuplestore 已创建,不应调用此函数 */Assert(winstate->buffer == NULL);/* 创建新的 tuplestore */winstate->buffer = tuplestore_begin_heap(false, false, work_mem);/** 为 tuplestore 设置读指针。当前指针不需要 BACKWARD 能力,但每个窗口函数的读指针需要,* 如果需要重启聚合,聚合指针也需要。*/winstate->current_ptr = 0;	/* 当前读指针 0 已预分配 *//* 重置当前指针的默认 REWIND 能力位 */tuplestore_set_eflags(winstate->buffer, 0);/* 如果需要,为聚合函数创建读指针 */if (winstate->numaggs > 0){/* 获取聚合函数的窗口对象 */WindowObject agg_winobj = winstate->agg_winobj;/* 初始化读指针标志 */int			readptr_flags = 0;/** 如果框架头部可能移动,或存在 EXCLUSION 子句,可能需要重启聚合...*/if (!(frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING) ||(frameOptions & FRAMEOPTION_EXCLUSION)){/* ...因此创建一个标记指针以跟踪框架头部 */agg_winobj->markptr = tuplestore_alloc_read_pointer(winstate->buffer, 0);/* 并且读指针需要 BACKWARD 能力 */readptr_flags |= EXEC_FLAG_BACKWARD;}/* 为聚合函数分配读指针 */agg_winobj->readptr = tuplestore_alloc_read_pointer(winstate->buffer,readptr_flags);}/* 为每个真实窗口函数创建标记和读指针 */for (int i = 0; i < numfuncs; i++){/* 获取当前窗口函数的状态 */WindowStatePerFunc perfuncstate = &(winstate->perfunc[i]);/* 如果不是普通聚合函数(即为窗口函数) */if (!perfuncstate->plain_agg){/* 获取窗口函数的窗口对象 */WindowObject winobj = perfuncstate->winobj;/* 为窗口函数分配标记指针 */winobj->markptr = tuplestore_alloc_read_pointer(winstate->buffer,0);/* 为窗口函数分配读指针,支持 BACKWARD 能力 */winobj->readptr = tuplestore_alloc_read_pointer(winstate->buffer,EXEC_FLAG_BACKWARD);}}/** 如果处于 RANGE 或 GROUPS 模式,确定框架边界需要物理访问框架端点行,* 除非在某些退化情况下。我们创建指向这些行的读指针,以简化访问并确保 tuplestore* 不会过早丢弃端点行。(必须在 update_frameheadpos 和 update_frametailpos* 需要指针的相同情况下创建指针。)*/winstate->framehead_ptr = winstate->frametail_ptr = -1; /* 初始化框架头部和尾部指针为 -1(未使用时) *//* 如果使用 RANGE 或 GROUPS 模式 */if (frameOptions & (FRAMEOPTION_RANGE | FRAMEOPTION_GROUPS)){/* 如果框架起点为当前行且有排序列,或指定了偏移 */if (((frameOptions & FRAMEOPTION_START_CURRENT_ROW) &&node->ordNumCols != 0) ||(frameOptions & FRAMEOPTION_START_OFFSET))winstate->framehead_ptr =tuplestore_alloc_read_pointer(winstate->buffer, 0);/* 如果框架终点为当前行且有排序列,或指定了偏移 */if (((frameOptions & FRAMEOPTION_END_CURRENT_ROW) &&node->ordNumCols != 0) ||(frameOptions & FRAMEOPTION_END_OFFSET))winstate->frametail_ptr =tuplestore_alloc_read_pointer(winstate->buffer, 0);}/** 如果存在需要知道当前行对等组边界的排除子句,我们创建一个读指针以跟踪对等组* 的尾部位置(即下一个对等组的第一行)。头部位置不需要自己的指针,因为我们* 在推进当前行时会维护它。*/winstate->grouptail_ptr = -1;/* 初始化组尾部指针为 -1 *//* 如果存在排除组或排除平局的子句,且有排序列 */if ((frameOptions & (FRAMEOPTION_EXCLUDE_GROUP |FRAMEOPTION_EXCLUDE_TIES)) &&node->ordNumCols != 0){/* 为组尾部分配读指针 */winstate->grouptail_ptr =tuplestore_alloc_read_pointer(winstate->buffer, 0);}
}

获取计划节点、框架选项和窗口函数数量

WindowAgg  *node = (WindowAgg *) winstate->ss.ps.plan;
int			frameOptions = winstate->frameOptions;
int			numfuncs = winstate->numfuncs;

作用

  • node:获取 WindowAgg 计划节点,包含查询计划信息(如排序列 ordNumCols)。
  • frameOptions:获取窗口框架选项(如 RANGEROWSGROUPS排除子句等)。
  • numfuncs:获取窗口函数数量(如 ROW_NUMBER())。

意义:为后续配置 tuplestore 和读指针提供必要上下文。

断言 tuplestore 未创建

Assert(winstate->buffer == NULL);

作用:确保 tuplestore 尚未创建(buffer == NULL),防止重复初始化

意义:保证函数仅在第一次调用时执行,符合补丁的优化目标(一次性创建 tuplestore)。

创建新的 tuplestore

winstate->buffer = tuplestore_begin_heap(false, false, work_mem);

作用:调用 tuplestore_begin_heap 创建一个堆存储的 tuplestore,用于存储分区中的元组。参数 false, false 表示不使用随机访问临时文件work_mem 指定内存限制。

意义tuplestore 是窗口函数处理的核心数据结构,存储所有分区元组以支持窗口计算。

初始化当前读指针

winstate->current_ptr = 0;	/* read pointer 0 is pre-allocated */
tuplestore_set_eflags(winstate->buffer, 0);

作用

  • 设置 current_ptr0,表示使用预分配的读指针 0(默认指针)。
  • tuplestore_set_eflags(0) 重置默认指针的标志,禁用 REWIND 能力(当前指针无需回溯)。

意义:当前指针用于跟踪 tuplestore 中的当前位置,禁用 REWIND 减少不必要的功能开销。

为聚合函数创建读指针

if (winstate->numaggs > 0)
{WindowObject agg_winobj = winstate->agg_winobj;int			readptr_flags = 0;if (!(frameOptions & FRAMEOPTION_START_UNBOUNDED_PRECEDING) ||(frameOptions & FRAMEOPTION_EXCLUSION)){agg_winobj->markptr = tuplestore_alloc_read_pointer(winstate->buffer, 0);readptr_flags |= EXEC_FLAG_BACKWARD;}agg_winobj->readptr = tuplestore_alloc_read_pointer(winstate->buffer,readptr_flags);
}

作用

  • 检查是否存在聚合函数numaggs > 0)。
  • 获取聚合窗口对象(agg_winobj)。
  • 如果框架起点不是 UNBOUNDED PRECEDING 或存在 EXCLUSION 子句(如 EXCLUDE CURRENT ROW),为聚合函数分配一个标记指针(markptr)以跟踪框架头部,并启用 BACKWARD 能力(EXEC_FLAG_BACKWARD),支持重启聚合
  • 为聚合函数分配读指针(readptr),根据需要设置 BACKWARD 标志。

意义:聚合函数(如 SUM() OVER())可能需要回溯或重启计算,标记指针和 BACKWARD 能力支持动态框架边界。

为窗口函数创建读指针

for (int i = 0; i < numfuncs; i++)
{WindowStatePerFunc perfuncstate = &(winstate->perfunc[i]);if (!perfuncstate->plain_agg){WindowObject winobj = perfuncstate->winobj;winobj->markptr = tuplestore_alloc_read_pointer(winstate->buffer, 0);winobj->readptr = tuplestore_alloc_read_pointer(winstate->buffer,EXEC_FLAG_BACKWARD);}
}

作用

  • 遍历所有窗口函数(numfuncs 个)。
  • 对于非普通聚合函数(!plain_agg,即真正的窗口函数,如 ROW_NUMBER()),获取窗口对象(winobj)。
  • 为每个窗口函数分配标记指针markptr)和读指针readptr),读指针启用 BACKWARD 能力。

意义:窗口函数需要灵活访问 tuplestore 中的元组,标记指针用于保存位置,BACKWARD 能力支持回溯(如计算 LAG() 或动态框架)。

为 RANGE 或 GROUPS 模式创建框架指针

winstate->framehead_ptr = winstate->frametail_ptr = -1; /* if not used */
if (frameOptions & (FRAMEOPTION_RANGE | FRAMEOPTION_GROUPS))
{if (((frameOptions & FRAMEOPTION_START_CURRENT_ROW) &&node->ordNumCols != 0) ||(frameOptions & FRAMEOPTION_START_OFFSET))winstate->framehead_ptr =tuplestore_alloc_read_pointer(winstate->buffer, 0);if (((frameOptions & FRAMEOPTION_END_CURRENT_ROW) &&node->ordNumCols != 0) ||(frameOptions & FRAMEOPTION_END_OFFSET))winstate->frametail_ptr =tuplestore_alloc_read_pointer(winstate->buffer, 0);
}

作用

  • 初始化框架头部和尾部指针为 -1(未使用)。
  • 如果窗口框架是 RANGEGROUPS 模式:
    • 如果起点为 CURRENT ROW 且有排序列(ordNumCols != 0)或指定了偏移(START_OFFSET),分配框架头部指针(framehead_ptr)。
    • 如果终点为 CURRENT ROW 且有排序列或指定了偏移(END_OFFSET),分配框架尾部指针(frametail_ptr)。

意义RANGEGROUPS 模式需要物理访问框架端点行,指针确保 tuplestore 保留这些行,防止过早丢弃。

为排除子句创建组尾部指针

winstate->grouptail_ptr = -1;
if ((frameOptions & (FRAMEOPTION_EXCLUDE_GROUP |FRAMEOPTION_EXCLUDE_TIES)) &&node->ordNumCols != 0)
{winstate->grouptail_ptr =tuplestore_alloc_read_pointer(winstate->buffer, 0);
}

作用

  • 初始化组尾部指针为 -1
  • 如果存在 EXCLUDE GROUPEXCLUDE TIES 子句且有排序列,分配组尾部指针(grouptail_ptr)以跟踪对等组的尾部(下一组的第一行)。

意义:排除子句需要确定对等组边界,尾部指针支持访问下一组的起始行(头部位置由当前行维护)。

执行层改动

ExecInitWindowAgg:

  • 修改内容:初始化时新增 winstate->next_partition = true,确保第一个分区触发 begin_partition 调用。
  • 意义:引入 next_partition 标志,与补丁的优化逻辑(复用 tuplestore)保持一致,确保分区切换的正确性。

ExecWindowAgg:

  • 修改内容:将 if (winstate->buffer == NULL) 替换为 if (winstate->next_partition),使用 next_partition 标志控制 begin_partition 的调用。
  • 意义:配合补丁的 tuplestore 复用机制,仅在需要时(新分区)调用 begin_partition,避免重复初始化 tuplestore

ExecEndWindowAgg:

  • 修改内容
    • 在函数开始时添加代码,检查 node->buffer != NULL,调用 tuplestore_end(node->buffer) 销毁 tuplestore,并将 node->buffer 置为 NULL
    • 确保 release_partition 跳过 tuplestore_clear(),避免重复清理。
  • 意义:确保查询结束时正确释放 tuplestore 资源,防止内存泄漏,同时保持与补丁复用逻辑的一致性。

  这些修改配合 prepare_tuplestorenext_partition 标志,实现了 tuplestore单次创建和复用,减少了分区切换时的内存分配开销(如 malloc/free),显著提升了高分区数(如 100 万个分区、每分区 1 行)查询的性能。

性能测试

  还是以以下用例为例:

-- 创建表
CREATE TABLE data_table (id SERIAL PRIMARY KEY,category INTEGER NOT NULL,value INTEGER NOT NULL
);-- 插入数据
-- 测试需要100万个分区,每个分区1行。
-- 我们使用 generate_series 生成100万行,
-- 每行具有唯一的 category 值(从1到1,000,000),
-- value 列为简单起见设置为与 category 相同。
INSERT INTO data_table (category, value)
SELECT gs, gs
FROM generate_series(1, 1000000) gs;-- 查询
-- 按 category 分区,生成100万个分区(因为每个 category 值唯一)。
-- 在每个分区内根据 value 排序,为每行分配 ROW_NUMBER()。
-- WHERE 子句中的 generate_series(1, 1000000) 确保包含所有行,符合每个分区1行的测试场景。
EXPLAIN ANALYZE
SELECTid,ROW_NUMBER() OVER (PARTITION BY category ORDER BY value) AS row_num
FROM data_table
WHERE category IN (SELECT generate_series(1, 1000000));
执行次数优化前执行时间(ms)优化后执行时间(ms)
17107.9306261.855
27034.9506266.170
37057.0406245.449
47025.3486266.164
57081.2256247.730
67053.0916277.012
77050.5426256.242
87099.1456254.400
97128.2286262.103
107088.1376260.770

在这里插入图片描述

统计汇总

平均执行时间:

  • 优化前(7107.930 + 7034.950 + ... + 7088.137) / 10 = 7072.564 ms
  • 优化后(6261.855 + 6266.170 + ... + 6260.770) / 10 = 6259.786 ms

平均性能提升:

  • 时间减少7072.564 - 6259.786 = 812.778 ms
  • 百分比812.778 / 7072.564 × 100% = 11.49%

范围:

  • 优化前时间范围7025.348–7128.228 ms
  • 优化后时间范围6245.449–6277.012 ms
  • 性能提升百分比范围10.81%–12.15%

总结

  • 性能提升:平均提升 11.49%(约 812.78 ms),优于优化前的 7072.564 ms,验证了补丁通过复用 tuplestore 减少内存分配开销的效果。
  • 稳定性:优化后执行时间波动较小(6245.449–6277.012 ms),表明优化提高了性能稳定性。
  • 与预期差异:提升低于补丁声称的 40%,可能因测试场景(如分区数量、硬件)不同,建议进一步测试更大分区数(如 100 万分区)以接近补丁目标。
http://www.xdnf.cn/news/1333135.html

相关文章:

  • 超大型公共场所的智慧守护者——人脸动态识别与管理系统
  • Temu美国站大规模扫号封店:虚假本土店遭批量封禁,如何规避?
  • 心路历程-了解网络相关知识
  • 【C2000常见问题】当板子处于强电噪声环境下,或带重载时C2000芯片的PWM发波会出现异常,导致炸管。
  • Vue2封装Axios
  • 8.20网络编程——sqlite3数据库
  • LeetCode 刷题【47. 全排列 II】
  • 【Linux系统】匿名管道以及进程池的简单实现
  • 在 React 中,​父子组件之间的通信(传参和传方法)
  • 大语言模型研究进展
  • Elasticsearch 写入全链路:从单机到集群
  • sfc_os!SfcValidateCatalogs函数分析之syssetup.inf中的[ProductCatalogsToInstall]
  • ansible playbook 实战案例roles | 实现基于 IHS 的 AWStats 访问监控系统
  • 第R6周:LSTM实现糖尿病探索与预测
  • [特殊字符] 小豆包 API 聚合平台:让 AI 接入更简单、更高效
  • 【数据结构之二叉树】
  • 【0基础PS】图片格式
  • Java常用类:编程世界的瑞士军刀
  • Web网站的运行原理1
  • JAVA:MyBatis 核心组件详解的技术指南
  • Mac电脑 Pixelmator Pro 专业图像处理【媲美PS】
  • 基于FPGA的实时图像处理系统(2)——VGA显示彩条和图片
  • 简笔成画:让AI绘画变得简单而有趣
  • 国产化PDF处理控件Spire.PDF教程:如何使用 Python 添加水印到 PDF
  • Map 和 Set
  • 19.web api 10
  • docker 部署
  • Go协程:从汇编视角揭秘实现奥秘
  • day31 SQLITE
  • 【38页PPT】关于5G智慧园区整体解决方案(附下载方式)