2. 聚合查询性能模型:理解聚合算子(HashAgg、SortAgg)的执行模型与代价模型

聚合查询是数据库中最常见的性能瓶颈场景之一。在尾延迟压测中,聚合算子的选择直接决定了查询的响应时间分布。本章深入剖析两种核心聚合算子——HashAgg 与 SortAgg 的执行模型与代价模型,为后续调优提供理论基础。

2.1 聚合算子执行模型对比

HashAgg 与 SortAgg 在内存使用、计算路径和适用场景上存在本质差异。下表从关键维度进行对比:

对比维度 HashAgg SortAgg
核心数据结构 哈希表(Hash Table) 排序缓冲区 + 顺序扫描
分组方式 哈希计算 + 冲突解决 排序后相邻行合并
内存需求 与分组基数成正比 与输入数据量成正比
是否依赖有序输入 否 是(需排序或索引有序)
适用分组基数 低~中等(内存可容纳) 高基数(数据量大)
溢出处理 磁盘溢出(性能急剧下降) 外部归并排序(可控降级)
尾延迟稳定性 较差(哈希冲突 + 溢出抖动) 较好(排序行为可预测)
压测启示: 在尾延迟压测场景中,HashAgg 的哈希冲突和磁盘溢出会导致明显的长尾效应;而 SortAgg 由于排序过程的可预测性,尾延迟分布更集中。选择聚合算子时,需结合分组基数和内存限制综合评估。

2.2 HashAgg 执行模型与代价分析

HashAgg 的执行分为三个阶段:构建哈希表 → 聚合计算 → 结果输出。其代价模型主要由以下因素决定:

  • 哈希计算代价: 每个输入行计算分组键的哈希值,复杂度 O(1) 但存在 CPU 开销。
  • 冲突解决代价: 哈希冲突导致链表遍历或开放寻址,冲突率随负载因子上升。
  • 内存分配代价: 哈希表初始化和扩容操作,涉及内存分配与数据重哈希。
  • 磁盘溢出代价: 当内存不足时,部分哈希表写入磁盘,后续读取产生 I/O 抖动。

HashAgg 的总代价可近似表示为:

Cost_HashAgg = N * (C_hash + C_conflict) + M * C_mem + O_disk * C_io

其中:N 为输入行数,C_hash 为单行哈希计算代价,C_conflict 为平均冲突处理代价,M 为哈希表大小,O_disk 为溢出数据量。

尾延迟陷阱: 当分组基数超过内存阈值时,HashAgg 的溢出行为会导致延迟从微秒级跳变到毫秒级,形成明显的尾延迟拐点。压测中应监控 work_mem 与哈希表大小的比例。

2.3 SortAgg 执行模型与代价分析

SortAgg 的执行分为两个阶段:排序 → 顺序聚合。其代价模型更依赖于排序操作的效率:

  • 排序代价: 基于输入数据量 N 和排序算法复杂度 O(N log N)。
  • 内存排序代价: 当数据量小于 work_mem 时,完全在内存中完成快速排序。
  • 外部排序代价: 数据量超过内存时,触发归并排序,产生多次磁盘读写。
  • 顺序聚合代价: 排序完成后,仅需一次线性扫描即可完成分组聚合,代价 O(N)。

SortAgg 的总代价可近似表示为:

Cost_SortAgg = N * log(N) * C_sort + N * C_agg + O_merge * C_io

其中:C_sort 为单次比较交换代价,C_agg 为单行聚合计算代价,O_merge 为外部归并的 I/O 次数。

关键洞察: SortAgg 的排序代价虽然在前端较高,但排序后的聚合过程完全线性且无冲突,尾延迟分布更平滑。在压测中,SortAgg 的 P99 延迟通常比 HashAgg 更稳定,尤其在高分组基数场景下。

2.4 聚合算子选择决策流程

下图展示了数据库优化器在选择聚合算子时的典型决策路径:

聚合算子选择决策流程 输入查询计划 输入是否已按分组键有序? (索引有序 / 上游排序) 是 否 选择 SortAgg 分组基数是否在内存可容纳范围? (估算 distinct 值数量) 是 否 选择 HashAgg 选择 SortAgg(高基数) 生成最终执行计划(可结合代价估算微调) 说明:实际优化器还会考虑 CPU 代价、I/O 代价、并行度等因素,此处为简化决策模型

2.5 代价模型关键参数与压测指标

在尾延迟压测中,以下参数直接影响聚合算子的性能表现:

参数 影响算子 压测关注点 典型调优方向
work_mem HashAgg / SortAgg 内存溢出阈值 增大内存或改用磁盘友好算子
分组基数(NDV) HashAgg 哈希冲突率 优化哈希函数或切换 SortAgg
输入数据量 SortAgg 排序时间 利用索引预排序或增加排序内存
数据倾斜度 HashAgg 热点桶冲突 使用两阶段聚合或倾斜优化
并行度 两者 数据分片与合并开销 调整并行 worker 数量
压测实践建议: 在尾延迟压测中,建议同时测试 HashAgg 和 SortAgg 两种算子在同一数据集上的 P50、P99、P99.9 延迟。若 HashAgg 的 P99.9 延迟超过 P50 的 10 倍以上,说明哈希冲突或溢出已严重影响尾延迟,应考虑强制使用 SortAgg 或优化分组键设计。

2.6 执行模型对尾延迟的直接影响

从执行模型角度,两种算子在尾延迟上的差异可归纳为:

  • HashAgg 的尾延迟特征: 正常情况延迟较低,但遇到哈希冲突链过长或磁盘溢出时,延迟出现突发尖峰,形成“长尾”。压测中表现为 P99 突然跳变。
  • SortAgg 的尾延迟特征: 排序阶段延迟相对均匀,外部归并排序的 I/O 行为可预测,尾延迟分布更集中,P99 与 P50 的比值通常较小。
  • 混合场景: 部分数据库支持 Hybrid Agg,先使用 HashAgg 在内存中聚合部分数据,溢出后转为 SortAgg,但切换过程本身可能引入额外延迟抖动。
调优口诀: 低基数用 Hash,高基数用 Sort;内存充足用 Hash,内存紧张用 Sort;尾延迟敏感用 Sort,吞吐优先用 Hash。