クエリチューニングレシピ
実践的なプレイブック: 症状 → 根本原因 → 実証済みの修正。
プロファイルを開いて問題のあるメトリックを見つけたが、「次に何をすべきか?」という問いに答える必要があるときに使用します。
1 · 迅速な診断ワークフロー
-
実行概要をざっと見る
QueryPeakMemoryUsagePerNode > 80 %
またはQuerySpillBytes > 1 GB
の場合、メモリとスピルのレシピに直接進みます。 -
最も遅いパイプライン / オペレーターを見つける
⟶ Query Profile UI で Sort by OperatorTotalTime % をクリックします。
最も負荷の高いオペレーターが次に読むべきレシピブロックを教えてくれます(Scan, Join, Aggregate など)。 -
ボトルネックのサブタイプを確認する
各レシピはそのシグネチャメトリックパターンから始まります。修正を試みる前にそれらを一致させます。
2 · オペレーター別レシピ
2.1 OLAP / コネクタースキャン [metrics]
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
を増やします。
フィルタープッシュダウンの欠如 – PushdownPredicates
が 0 に近いままで ExprFilterRows
が高い場合、述語がストレージ層に到達していません。単純な比較として書き直す(%LIKE%
や広範な OR
チェーンを避ける)か、ゾーンマップ/Bloom インデックスやマテリアライズドビューを追加してプッシュダウンできるようにします。
スレッドプールの枯渇 – 高い IOTaskWaitTime
と低い PeakIOTasks
は、I/O スレッドプールが飽和していることを示します。BE の max_io_threads
を増やすか、キャッシュを拡大してより多くのタスクを同時に実行できるようにします。
タブレット間のデータスキュー – 最大と最小の OperatorTotalTime
の間に大きなギャップがある場合、一部のタブレットが他よりも多くの作業をしています。高いカーディナリティのキーで再バケット化するか、バケット数を増やして負荷を分散します。
Rowset/segment の断片化 – RowsetsReadCount
/SegmentsReadCount
が急増し、長い SegmentInitTime
がある場合、多くの小さな rowset が存在します。手動でのコンパクションをトリガーし、小さなロードをバッチ処理してセグメントを事前にマージします。
累積されたソフトデリート – 大きな DeleteFilterRows
は、ソフトデリートの使用が多いことを示します。BE コンパクションを実行してトゥームストーンを削除し、削除ビットマップを統合します。
2.2 集計 [metrics]
Aggregate Operator は、集計関数、GROUP BY
、および DISTINCT
の実行を担当します。
集計アルゴリズムの多様な形式
形式 | プランナーが選択する条件 | 内部データ構造 | 特徴 / 注意点 |
---|---|---|---|
ハッシュ集計 | キーがメモリに収まる; カーディナリティが極端でない | SIMD プロービングを備えたコンパクトなハッシュテーブル | デフォルトパス、適度なキー数に最適 |
ソート集計 | 入力が GROUP BY キーで既に順序付けされている | 単純な行比較 + 実行状態 | ハッシュテーブルコストがゼロ、プロービングが重いスキューで通常 2-3 倍速い |
スピル可能な集計 (3.2+) | ハッシュテーブルがメモリ制限を超える | ディスクスピルパーティションを持つハイブリッドハッシュ/マージ | OOM を防ぎ、パイプラインの並行性を維持 |
多段階分散集計
StarRocks では、集計は分散方式で実装されており、クエリパターンとオプティマイザの決定に応じて多段階になることがあります。
┌─────────┐ ┌──────────┐ ┌────────────┐ ┌────────────┐
│ Stage 0 │ local │ Stage 1 │ shard/ │ Stage 2 │ gather/│ Stage 3 │ final
│ Partial │───► │ Update │ hash │ Merge │ shard │ Finalize │ output
└─────────┘ └──────────┘ └────────────┘ └────────────┘
ステージ | 使用される条件 | 実行内容 |
---|---|---|
ワンステージ | DISTRIBUTED BY が GROUP BY のサブセットで、パーティションがコロケートされている | 部分集計が即座に最終結果になります。 |
ツーステージ (ローカル + グローバル) | 一般的な分散 GROUP BY | 各 BE 内の Stage 0 が重複を適応的に圧縮し、Stage 1 が GROUP BY に基づいてデータをシャッフルしてからグローバル集計を実行します。 |
スリーステージ (ローカル + シャッフル + ファイナル) | 重い DISTINCT と高カーディナリティの GROUP BY | 上記のように Stage 0; Stage 1 が GROUP BY によってシャッフルし、GROUP BY と DISTINCT で集計; Stage 2 が部分状態を GROUP BY としてマージします。 |
フォーステージ (ローカル + 部分 + 中間 + ファイナル) | 重い DISTINCT と低カーディナリティの GROUP BY | 単一ポイントのボトルネックを避けるために GROUP BY と DISTINCT によってシャッフルする追加のステージを導入します。 |
一般的なパフォーマンスのボトルネック
高カーディナリティの GROUP BY – HashTableSize
または HashTableMemoryUsage
がメモリ制限に向かって膨らむ場合、グループ化キーが広すぎるか、非常に異なるものです。ソートされたストリーミング集計 (enable_streaming_preaggregation = true
) を有効にし、ロールアップマテリアライズドビューを作成するか、広い文字列キーを INT
にキャストします。
シャッフルスキュー – フラグメント間で HashTableSize
または InputRowCount
に大きな差がある場合、不均衡なシャッフルを示します。キーにソルト列を追加するか、DISTINCT [skew]
ヒントを使用して行を均等に分配します。
状態が重い集計関数 – AggregateFunctions
が実行時間を支配し、関数に HLL_
、BITMAP_
、または COUNT(DISTINCT)
が含まれる場合、巨大な状態オブジェクトが移動されています。取り込み中に HLL/ビットマップスケッチを事前に計算するか、近似バリアントに切り替えます。
部分集計の劣化 – 大きな InputRowCount
と控えめな AggComputeTime
、および上流の EXCHANGE での大規模な BytesSent
は、事前集計がバイパスされたことを意味します。SET streaming_preaggregation_mode = "force_preaggregation"
で強制的にオンに戻します。
高価なキー式 – ExprComputeTime
が AggComputeTime
に匹敵する場合、GROUP BY キーが行ごとに計算されています。これらの式をサブクエリで実体化するか、生成列に昇格させます。
2.3 ジョイン [metrics]
Join Operator は、明示的なジョインまたは暗黙のジョインを実装する責任があります。
実行中、ジョインオペレーターはパイプラインエンジン内で並行して実行される Build (ハッシュテーブル構築) フェーズと Probe フェーズに分割されます。ベクトルチャンク (最大 4096 行) は SIMD でバッチハッシュされ、消費されたキーは実行時フィルター (Bloom または IN フィルター) を生成し、プローブ入力を早期に削減するために上流のスキャンにプッシュバックされます。
ジョイン戦略
StarRocks は、ベクトル化され、パイプラインに適したハッシュジョインコアに依存しており、コストベースオプティマイザがプラン時に評価する 4 つの物理戦略に組み込むことができます。
戦略 | オプティマイザが選択する条件 | 高速化の要因 |
---|---|---|
Colocate Join | 両方のテーブルが同じコロケーショングループに属している (同一のバケットキー、バケット数、レプリカレイアウト)。  | ネットワークシャッフルなし: 各 BE はローカルバケットのみをジョインします。 |
Bucket-Shuffle Join | ジョインテーブルの一方がジョインキーと同じバケットキーを持っている | ジョインテーブルの一方のみをシャッフルする必要があり、ネットワークコストを削減できます。 |
Broadcast Join | ビルド側が非常に小さい (行/バイトのしきい値または明示的なヒント)。  | 小さなテーブルがすべてのプローブノードに複製され、大きなテーブルのシャッフルを回避します。 |
Shuffle (Hash) Join | 一般的なケース、キーが一致しない。 | 各行をジョインキーでハッシュ分割し、プローブを BE 間で均等に分散します。 |
一般的なパフォーマンスのボトルネック
ビルド側のサイズ超過 – BuildHashTableTime
と HashTableMemoryUsage
のスパイクは、ビルド側がメモリを超えていることを示します。プローブ/ビルドテーブルを入れ替え、ビルドテーブルを事前フィルタリングするか、ハッシュスピリングを有効にします。
キャッシュに優しくないプローブ – SearchHashTableTime
が支配的な場合、プローブ側がキャッシュ効率が悪いです。プローブ行をジョインキーでソートし、実行時フィルターを有効にします。
シャッフルスキュー – 単一のフラグメントの ProbeRows
が他のすべてを圧倒する場合、データがスキューしています。より高いカーディナリティのキーに切り替えるか、key || mod(id, 16)
のようなソルトを追加します。
意図しないブロードキャスト – ジョインタイプ BROADCAST で巨大な BytesSent
は、小さいと思っていたテーブルが実際にはそうでないことを示します。broadcast_row_limit
を下げるか、SHUFFLE
ヒントでシャッフルを強制します。
実行時フィルターの欠如 – 小さな JoinRuntimeFilterEvaluate
とフルテーブルスキャンは、実行時フィルターが伝播しなかったことを示します。ジョインを純粋な等式として書き直し、列タイプが一致していることを確認します。
非等式フォールバック – オペレータータイプが CROSS
または NESTLOOP
の場合、不等式または関数がハッシュジョインを妨げています。真の等式述語を追加するか、より大きなテーブルを事前にフィルタリングします。
2.4 Exchange (ネットワーク) [metrics]
サイズ超過のシャッフルまたはブロードキャスト – NetworkTime
が 30 % を超え、BytesSent
が大きい場合、クエリは過剰なデータを送信しています。ジョイン戦略を再評価するか、Exchange Compaction (pipeline_enable_exchange_compaction
) を有効にします。
レシーバーのバックログ – 送信者キューが常に満杯である場合、シンクでの高い WaitTime
はレシーバーが追いつけないことを示します。レシーバースレッドプール (brpc_num_threads
) を増やし、NIC の帯域幅と QoS 設定を確認します。
2.5 ソート / マージ / ウィンドウ
さまざまなメトリックを理解しやすくするために、マージは以下の状態メカニズムとして表現できます。
┌────────── PENDING ◄──────────┐
│ │
│ │
├──────────────◄───────────────┤
│ │
▼ │
INIT ──► PREPARE ──► SPLIT_CHUNK ──► FETCH_CHUNK ──► FINISHED
▲
|
| one traverse from leaf to root
|
▼
PROCESS
ソートスピリング – MaxBufferedBytes
が約 2 GB を超えるか SpillBytes
がゼロでない場合、ソートフェーズがディスクにスピルしています。LIMIT
を追加し、上流で事前集計するか、マシンに十分なメモリがある場合は sort_spill_threshold
を上げます。
マージの飢餓状態 – 高い PendingStageTime
は、マージが上流のチャンクを待っていることを示します。最初にプロデューサーオペレーターを最適化するか、パイプラインバッファを拡大します。
広いウィンドウパーティション – ウィンドウオペレーター内の巨大な PeakBufferedRows
は非常に広いパーティションまたはフレーム制限のない ORDER BY を示します。より細かくパーティション化し、RANGE BETWEEN
境界を追加するか、中間集計を実体化します。
3 · メモリ & スピル チートシート
閾値 | 監視対象 | 実践的なアクション |
---|---|---|
80 % の BE メモリ | QueryPeakMemoryUsagePerNode | セッションの exec_mem_limit を下げるか、BE RAM を追加します。 |
スピル検出 (SpillBytes > 0) | QuerySpillBytes , オペレーターごとの SpillBlocks | メモリ制限を増やす; SR 3.2+ にアップグレードしてハイブリッドハッシュ/マージスピルを使用します。 |
4 · ポストモーテムのテンプレート
1. 症状
– 遅いステージ: 集計 (OperatorTotalTime 68 %)
– 警告: HashTableMemoryUsage 9 GB (> exec_mem_limit)
2. 根本原因
– GROUP BY 高カーディナリティ UUID
3. 適用された修正
– ソートされたストリーミング集計 + ロールアップ MV を追加
4. 結果
– クエリ実行時間が 95 秒から 8 秒に短縮; メモリピーク 0.7 GB