查询调优方法
一个实用的手册:症状 → 根本原因 → 验证过的修复方法。
当您打开一个 Profile 并发现一个危险指标,但仍然需要回答“接下来怎么办?”时,请使用它。
1 · 快速诊断工作流程
-
浏览执行概览
如果QueryPeakMemoryUsagePerNode > 80 %
或QuerySpillBytes > 1 GB
,请直接跳转到内存和溢出秘籍。 -
找到最慢的 Pipeline / Operator
⟶ 在查询 Profile UI 中,点击按 OperatorTotalTime % 排序。
最热的 operator 告诉您接下来要阅读哪个秘籍块(Scan, Join, Aggregate, …)。 -
确认瓶颈子类型
每个秘籍都以其签名指标模式开始。在尝试修复之前,请匹配这些模式。
2 · 按 Operator 分类秘籍
2.1 OLAP / Connector Scan [指标]
为了更好地理解 Scan Operator 中的各种指标,以下图表展示了这些指标与存储结构之间的关联。
为了从磁盘检索数据并应用谓词,存储引擎利用了几种技术
- 数据存储:编码和压缩的数据以段的形式存储在磁盘上,并附带各种索引。
- 索引过滤:引擎利用 BitmapIndex、BloomfilterIndex、ZonemapIndex、ShortKeyIndex 和 NGramIndex 等索引来跳过不必要的数据。
- 下推谓词:简单的谓词,如
a > 1
,被下推到特定列上进行评估。 - 延迟物化:仅从磁盘检索所需的列和过滤后的行。
- 非下推谓词:无法下推的谓词会被评估。
- 投影表达式:计算表达式,例如
SELECT a + 1
。
Scan Operator 使用额外的线程池来执行 IO 任务。因此,此节点的各个时间指标之间的关系如下图所示
常见的性能瓶颈
冷存储或慢速存储 – 当 BytesRead
、ScanTime
或 IOTaskExecTime
占主导地位并且磁盘 I/O 徘徊在 80‑100 % 左右时,扫描正在命中冷存储或未充分配置的存储。将热数据移动到 NVMe/SSD,启用存储缓存,或者——如果您正在扫描 S3/HDFS——提高 remote_cache_capacity
。
缺少 Filter push‑down – 如果 PushdownPredicates
保持在 0 附近,而 ExprFilterRows
很高,则谓词无法到达存储层。将它们重写为简单的比较(避免 %LIKE%
和宽 OR
链)或添加 zonemap/Bloom 索引或物化视图,以便可以将其下推。
线程池饥饿 – 高 IOTaskWaitTime
以及低 PeakIOTasks
表明 I/O 线程池已饱和。增加 BE 上的 max_io_threads
或扩大缓存以允许更多任务并发运行。
平板之间的数据倾斜 – 最大 OperatorTotalTime
和最小 OperatorTotalTime
之间的差距很大意味着某些平板比其他平板做更多的工作。在更高基数的键上重新分桶或增加桶计数以分散负载。
Rowset/segment 碎片 – 爆炸性的 RowsetsReadCount
/SegmentsReadCount
加上长时间的 SegmentInitTime
表明存在许多微小的 rowset。触发手动 compaction 并批量加载小负载,以便段可以预先合并。
累积的软删除 – 大量的 DeleteFilterRows
意味着大量使用软删除。运行 BE compaction 以清除墓碑并整合删除位图。
2.2 Aggregate [指标]
Aggregate Operator 负责执行聚合函数、
GROUP BY
和 DISTINCT
。
聚合算法的多种形式
形式 | 规划器何时选择它 | 内部数据结构 | 亮点 / 注意事项 |
---|---|---|---|
Hash 聚合 | 键适合内存;基数不极端 | 带有 SIMD 探测的紧凑哈希表 | 默认路径,非常适合适度的键计数 |
排序聚合 | 输入已按 GROUP BY 键排序 | 简单的行比较 + 运行状态 | 零哈希表成本,通常在探测重度倾斜时快 2-3 倍 |
可溢出聚合 (3.2+) | 哈希表超出内存限制 | 带有磁盘溢出分区的混合哈希/合并 | 防止 OOM,保留 pipeline 并行性 |
多阶段分布式聚合
在 StarRocks 中,聚合以分布式方式实现,这可以是多阶段的,具体取决于查询模式和优化器决策。
┌─────────┐ ┌──────────┐ ┌────────────┐ ┌────────────┐
│ Stage 0 │ local │ Stage 1 │ shard/ │ Stage 2 │ gather/│ Stage 3 │ final
│ Partial │───► │ Update │ hash │ Merge │ shard │ Finalize │ output
└─────────┘ └──────────┘ └────────────┘ └────────────┘
阶段 | 何时使用 | 发生了什么 |
---|---|---|
单阶段 | DISTRIBUTED BY 是 GROUP BY 的子集,分区是 colocated 的 | 部分聚合立即成为最终结果。 |
两阶段(本地 + 全局) | 典型的分布式 GROUP BY | 每个 BE 内的阶段 0 自适应地折叠重复项;阶段 1 基于 GROUP BY 洗牌数据,然后执行全局聚合 |
三阶段(本地 + 洗牌 + 最终) | 重度 DISTINCT 和高基数 GROUP BY | 阶段 0 如上;阶段 1 按 GROUP BY 洗牌,然后按 GROUP BY 和 DISTINCT 聚合;阶段 2 将部分状态合并为 GROUP BY |
四阶段(本地 + 部分 + 中间 + 最终) | 重度 DISTINCT 和低基数 GROUP BY | 引入一个额外的阶段,按 GROUP BY 和 DISTINCT 洗牌,以避免单点瓶颈 |
常见的性能瓶颈
高基数 GROUP BY – 当 HashTableSize
或 HashTableMemoryUsage
膨胀到内存限制时,分组键太宽或太离散。启用排序流式聚合 (enable_streaming_preaggregation = true
),创建 roll‑up 物化视图,或将宽字符串键转换为 INT
。
Shuffle 倾斜 – 片段之间 HashTableSize
或 InputRowCount
的巨大差异揭示了不平衡的 shuffle。向键添加 salt 列或使用 DISTINCT [skew]
提示,以便行均匀分布。
状态繁重的聚合函数 – 如果 AggregateFunctions
支配运行时,并且函数包括 HLL_
、BITMAP_
或 COUNT(DISTINCT)
,则巨大的状态对象正在移动。在摄取期间预先计算 HLL/bitmap 草图或切换到近似变体。
部分聚合降级 – 巨大的 InputRowCount
和适度的 AggComputeTime
,加上上游 EXCHANGE 中大量的 BytesSent
,意味着跳过了预聚合。使用 SET streaming_preaggregation_mode = "force_preaggregation"
强制其重新启用。
昂贵的键表达式 – 当 ExprComputeTime
与 AggComputeTime
匹敌时,GROUP BY 键按行计算。在子查询中物化这些表达式或将它们提升为生成的列。
2.3 Join [指标]
Join Operator 负责实现显式 join 或隐式 join。
在执行期间,join operator 分为 Build(哈希表构建)和 Probe 阶段,这些阶段在 pipeline 引擎内并行运行。向量块(最多 4096 行)使用 SIMD 进行批量哈希;消耗的键生成运行时过滤器——Bloom 或 IN 过滤器——被推回到上游扫描以尽早削减 probe 输入。
Join 策略
StarRocks 依赖于矢量化、pipeline 友好的哈希连接核心,该核心可以连接到四种物理策略,基于成本的优化器会在计划时权衡这些策略
策略 | 优化器何时选择它 | 什么使其快速 |
---|---|---|
Colocate Join | 两个表都属于同一个 colocation 组(相同的 bucket 键、bucket 计数和副本布局)。 | 没有网络 shuffle:每个 BE 仅连接其本地 bucket。 |
Bucket-Shuffle Join | 其中一个 join 表具有与 join 键相同的 buckket 键 | 只需要 shuffle 一个 join 表,这可以降低网络成本 |
Broadcast Join | Build 侧非常小(行/字节阈值或显式提示)。 | 小表被复制到每个 probe 节点;避免 shuffle 大表。 |
Shuffle (Hash) Join | 一般情况,键不对齐。 | 在 join 键上哈希分区每一行,以便 probe 在 BE 之间平衡。 |
常见的性能瓶颈
过大的 build 侧 – BuildHashTableTime
和 HashTableMemoryUsage
的峰值显示 build 侧已超出内存。交换 probe/build 表,预先过滤 build 表,或启用哈希溢出。
缓存不友好的 probe – 当 SearchHashTableTime
占主导地位时,probe 侧的缓存效率不高。按 join 键对 probe 行进行排序并启用运行时过滤器。
Shuffle 倾斜 – 如果单个片段的 ProbeRows
比所有其他片段都多,则数据倾斜。切换到更高基数的键或附加 salt,例如 key || mod(id, 16)
。
意外的广播 – 带有巨大 BytesSent
的 Join 类型 BROADCAST 意味着您认为很小的表不是。降低 broadcast_row_limit
或使用 SHUFFLE
提示强制 shuffle。
缺少运行时过滤器 – 微小的 JoinRuntimeFilterEvaluate
以及全表扫描表明运行时过滤器从未传播。将 join 重写为纯等式,并确保列类型对齐。
非等值回退 – 当 operator 类型为 CROSS
或 NESTLOOP
时,不等式或函数会阻止哈希连接。添加一个真正的等式谓词或预先过滤较大的表。
2.4 Exchange (Network) [指标]
过大的 shuffle 或广播 – 如果 NetworkTime
超过 30 % 并且 BytesSent
很大,则查询正在传输过多的数据。重新评估 join 策略或启用 exchange compaction (pipeline_enable_exchange_compaction
)。
接收器积压 – sink 中较高的 WaitTime
和保持满状态的发送者队列表明接收器无法跟上。增加接收器线程池 (brpc_num_threads
) 并确认 NIC 带宽和 QoS 设置。
2.5 Sort / Merge / Window
为了便于理解各种指标,Merge 可以表示为以下状态机制
┌────────── PENDING ◄──────────┐
│ │
│ │
├──────────────◄───────────────┤
│ │
▼ │
INIT ──► PREPARE ──► SPLIT_CHUNK ──► FETCH_CHUNK ──► FINISHED
▲
|
| one traverse from leaf to root
|
▼
PROCESS
排序溢出 – 当 MaxBufferedBytes
升至大约 2 GB 以上或 SpillBytes
非零时,排序阶段将溢出到磁盘。添加 LIMIT
,预先聚合上游,或者如果机器有足够的内存,则提高 sort_spill_threshold
。
合并饥饿 – 高 PendingStageTime
告诉您合并正在等待上游块。首先优化生产者 operator 或扩大 pipeline 缓冲区。
宽窗口分区 – 窗口 operator 内巨大的 PeakBufferedRows
指向非常宽的分区或缺少帧限制的 ORDER BY。更精细地进行分区,添加 RANGE BETWEEN
边界,或物化中间聚合。
3 · 内存和溢出备忘单
阈值 | 要观察什么 | 实际行动 |
---|---|---|
80 % 的 BE 内存 | QueryPeakMemoryUsagePerNode | 降低会话 exec_mem_limit 或添加 BE RAM |
检测到溢出 (SpillBytes > 0) | QuerySpillBytes ,每个 operator 的 SpillBlocks | 增加内存限制;升级到 SR 3.2+ 以获得混合哈希/合并溢出 |
4 · 您的事后剖析模板
1. Symptom
– Slow stage: Aggregate (OperatorTotalTime 68 %)
– Red-flag: HashTableMemoryUsage 9 GB (> exec_mem_limit)
2. Root cause
– GROUP BY high-cardinality UUID
3. Fix applied
– Added sorted streaming agg + roll-up MV
4. Outcome
– Query runtime ↓ from 95 s ➜ 8 s; memory peak 0.7 GB```