『Software Design 2026年4月号』
はじめに
本稿では、PostgreSQLがサポートする高速化処理について紹介します。これから紹介するパラレルクエリやJITの効果を見るため、高速化処理を無効にしたパターンのクエリと実行計画を図1に提示します。1,000万レコードの最大値を取るサンプルクエリは全体のコストが288,935かかり、全体の実行時間が2427.
test=> SET jit=off; ← JITを無効化 SET test=> SET max_parallel_workers_per_gather=0; ← パラレルクエリを無効化 SET test=> EXPLAIN (ANALYZE ON) SELECT MAX(bid) FROM pgbench_accounts; QUERY PLAN -------------------------------------------------------------------------------------------- Aggregate (cost=288935.00..288935.01 rows=1 width=4) (actual time=2427.136..2427.138ュ rows=1.00 loops=1) Buffers: shared hit=163935 -> Seq Scan on pgbench_accounts (cost=0.00..263935.00 rows=10000000 width=4) (actualュ time=0.028..1024.361 rows=10000000.00 loops=1) Buffers: shared hit=163935 Planning Time: 0.212 ms Execution Time: 2427.187 ms (6 rows) test=>
パラレルクエリ
パラレルクエリとは
パラレルクエリは1つのクエリをPostgreSQL内部で並列に処理して高速化するしくみです。リーダープロセス
パラレルクエリの動作条件
パラレルクエリは実行計画でクエリ内の特定箇所を並列処理にすることで高速化が見込まれる場合に動作します。ワーカープロセスの生成数は、次の3つのパラメータに基づき決定されます。
- max_
worker_ processes:バックグラウンドプロセスの最大値 - max_
parallel_ workers_ per_ gather:1つのクエリで実行できるワーカープロセスの最大値 - max_
parallel_ workers:クラスタ全体で同時に実行できるワーカープロセスの最大値
パラレル化の対象となるのは、
パラレルクエリの実行計画
パラレルクエリが動作しているかは実行計画を見ることで判断できます。図3は全体のコストが196,185かかり、全体の実行時間が665.
test=> SET jit=off; ← JITを無効化 SET test=> SET max_parallel_workers_per_gather=4; ← パラレルクエリを4多重で有効化 SET test=> EXPLAIN (ANALYZE ON) SELECT MAX(bid) FROM pgbench_accounts; QUERY PLAN -------------------------------------------------------------------------------------------- Finalize Aggregate (cost=196185.42..196185.43 rows=1 width=4) (actual time=636.842..664.ュ 943 rows=1.00 loops=1) Buffers: shared hit=163935 -> Gather (cost=196185.00..196185.41 rows=4 width=4) (actual time=636.620..664.927ュ rows=5.00 loops=1) Workers Planned: 4 Workers Launched: 4 ← 4多重でプロセスが動作している Buffers: shared hit=163935 -> Partial Aggregate (cost=195185.00..195185.01 rows=1 width=4) (actualュ time=625.573..625.574 rows=1.00 loops=5) Buffers: shared hit=163935 -> Parallel Seq Scan on pgbench_accounts (cost=0.00..188935.00ュ rows=2500000 width=4) (actual time=0.034..270.140 rows=2000000.00 loops=5) ↑ 1プロセスは200万レコード処理している Buffers: shared hit=163935 Planning Time: 0.233 ms Execution Time: 665.080 ms (12 rows) test=>
パラレルクエリの実行計画で登場するノードは表1を参照してください。Gather以下が並列処理部分です。図3では
| ノード | 実行計画上の表記 | 説明 |
|---|---|---|
| パラレルシーケンシャルスキャン | Parallel Seq Scan | テーブルの処理するブロック範囲を設定し、各プロセスでそれぞれ逐次処理する。 |
| パラレルビットマップヒープスキャン | Parallel Bitmap Heap Scan | リーダープロセスで処理対象テーブルのインデックスをスキャンし、アクセスするブロックを示すビットマップを作成する。各プロセスが作成したビットマップを参照してテーブルを処理する。インデックススキャン部分は直列処理。 |
| パラレルインデックススキャン | Parallel Index Scan | インデックスの処理するブロック範囲を設定し、各プロセスでそれぞれ逐次処理する。インデックスのみで処理が完結する場合はParallel Index Only Scanとなる。 |
| ネステッドループ結合 | Nested Loop | 外側のテーブルをループで読み出し、内側のテーブルもループで該当行を探索する結合。内側のテーブルにインデックスがあると高速化される。 |
| マージ結合 | Merge Join | それぞれのテーブルをソートした後に結合キーを探索する結合。各プロセスで同一処理のソートを行うため、計算効率的に非効率な結合となる。 |
| ハッシュ結合 | Hash Join | 一方のテーブルに対するハッシュテーブルを作成し、もう片方のテーブルデータをハッシュテーブルに参照して結合する。各プロセス共通のハッシュテーブルを作成するが、ハッシュテーブルが大きい場合、メモリ不足時に一時ファイルを使用するため遅くなる可能性がある。ハッシュテーブル作成も並列処理する場合はParallel Hash Joinとなる。 |
| 集約 | Partial Aggregate | 各プロセスで処理された最終結果をまとめる処理。この処理結果がリーダープロセスへ転送される。 |
| 転送 | Gather | 各プロセスで処理した結果をリーダープロセスに転送する。ソート済みデータを転送するGather Mergeも存在する。 |
| 集約確定 | Finalize Aggregate | Gatherで転送された処理結果を再集約して結果を確定させる。AggregateはGroupAggregate やHashAggregateが存在する。 |
| パラレルアペンド | Parallel Append | UNIONやパーティションテーブルを並列処理する場合に使用される。各プロセスは別々のテーブルを並列で処理し、処理結果をまとめる。 |
また、
JIT(実行時)コンパイル
JITとは
PostgreSQLはLLVMと呼ばれるコンパイル基盤を用いて、クエリの一部分を直接CPUで動作する形に変換
JITが有効な場合はOptimizerが実行計画を最適化する際、推定コスト閾値を超える場合にJITコンパイルを検討します。JITを使用することになると該当式をネイティブマシンコードにコンパイルし、直接CPUで実行します[1]。
インタプリタ実行では最新のCPUアーキテクチャを十分活用できない可能性がありますが、コンパイルされたネイティブマシンコードはCPUアーキテクチャを十分に活用できる形に最適化されます。Executorはコンパイルしたネイティブマシンコードの関数をロードし、実行計画をもとに処理を行います
JITの動作条件
前述のとおり、JITはPlannerでJITを使用し、高速化が見込まれた場合に動作します。高速化の対象は、WHERE句やターゲットリスト、集約関数などといった式の評価や、ディスク上のタプル
JITの使用可否はパラメータjitです。jitの値をoffにすることでJITは動作しなくなります。デフォルトでJITは有効化されています。
JITが動作する場合、クエリの実行とは別にコンパイルを実行する時間が別に追加されることに留意してください。クエリの実行は高速化されてもコンパイル時間をプラスすると、ほとんど変わらなかったり逆に時間がかかったりする場合があります。そのため、コストの判定として次の3つのパラメータを設定します。
- jit_
above_ cost:JITコンパイルを使用するコストの閾値 - jit_
inline_ above_ cost:関数のインライン化を使用するコストの閾値 - jit_
optimize_ above_ cost:積極的な最適化を使用するコストの閾値
JITの実行計画
図5は実行計画内に
test=> SET jit=on; ← JITを有効化 SET test=> SET max_parallel_workers_per_gather=0; ← パラレルクエリを無効化 SET test=> EXPLAIN (ANALYZE ON) SELECT MAX(bid) FROM pgbench_accounts; QUERY PLAN -------------------------------------------------------------------------------------------- Aggregate (cost=288935.00..288935.01 rows=1 width=4) (actual time=1959.029..1959.031ュ rows=1.00 loops=1) Buffers: shared hit=163935 -> Seq Scan on pgbench_accounts (cost=0.00..263935.00 rows=10000000 width=4) (actualュ time=0.033..838.801 rows=10000000.00 loops=1) Buffers: shared hit=163935 Planning Time: 0.213 ms JIT: ← 以降のインデントが下がっている行はJITの詳細情報 Functions: 3 Options: Inlining false, Optimization false, Expressions true, Deforming true Timing: Generation 0.599 ms (Deform 0.194 ms), Inlining 0.000 ms, Optimization 0.727 ms,ュ Emission 16.291 ms, Total 17.618 ms Execution Time: 2040.375 ms (10 rows) test=>
「Functions」
| 項目 | 説明 |
|---|---|
| Inlining | インライン化 |
| Optimization | 最適化 |
| Expressions | 式の評価 |
| Deforming | タプル変形 |
| 項目 | 説明 |
|---|---|
| Generation | JITコンパイルの所要時間 |
| Inlining | インライン化所要時間 |
| Optimization | 最適化所要時間 |
| Emission | JITコード出力所要時間 |
パラレルクエリとJITの使い分け
パラレルクエリは処理を行うプロセスを増やして高速化を行い、JITはクエリ自体を最適化して高速化するというように、それぞれ異なる高速化技術となります。パラレルクエリとJITは共存でき、パラレルクエリ内でJITが動くという複合処理も可能です。
複合処理時の実行時間は623.
test=> SET jit=on; ← JITを有効化 SET test=> SET max_parallel_workers_per_gather=4; ← パラレルクエリを4多重で有効化 SET test=> EXPLAIN (ANALYZE ON, COSTS OFF) SELECT MAX(bid) FROM pgbench_accounts; QUERY PLAN -------------------------------------------------------------------------------------------- Finalize Aggregate (cost=196185.42..196185.43 rows=1 width=4) (actual time=605.071..622ュ .974 rows=1.00 loops=1) Buffers: shared hit=163935 -> Gather (cost=196185.00..196185.41 rows=4 width=4) (actual time=604.855..622.946ュ rows=5.00 loops=1) Workers Planned: 4 Workers Launched: 4 Buffers: shared hit=163935 -> Partial Aggregate (cost=195185.00..195185.01 rows=1 width=4) (actual time=ュ 582.718..582.720 rows=1.00 loops=5) Buffers: shared hit=163935 -> Parallel Seq Scan on pgbench_accounts (cost=0.00..188935.00 rows=ュ 2500000 width=4) (actual time=0.024..254.786 rows=2000000.00 loops=5) Buffers: shared hit=163935 Planning Time: 0.142 ms JIT: Functions: 17 Options: Inlining false, Optimization false, Expressions true, Deforming true Timing: Generation 1.851 ms (Deform 0.403 ms), Inlining 0.000 ms, Optimization 1.636ュ ms, Emission 24.366 ms, Total 27.853 ms Execution Time: 623.507 ms (16 rows) test=>
パラレルクエリとJITのメリット/
| 高速化技術 | メリット | デメリット |
|---|---|---|
| パラレルクエリ | 大規模テーブルアクセスなどを並列処理にて高速化できる。 | プロセス生成・ |
| JIT | 式の評価などをCPUで動作させることで高速化できる。 | JITコンパイルによるオーバーヘッドあり。 |
パラレルVACUUM
VACUUMとは
PostgreSQLが採用しているアーキテクチャは
パラレルVACUUM
パラレルVACUUMはテーブルに設定された複数のインデックスに対して、並列処理を実行します。並列処理でないVACUUM処理の場合は複数のインデックスも直列で実行するため時間がかかりますが、パラレルVACUUMは同時に複数のインデックスを処理するため高速に処理が完了します。パラレルVACUUMはインデックスのみに対して動作し、通常レコードは並列処理されないことに留意してください。