1. はじめに
本記事は、PostgreSQL19 のプランナの改善を、PostgreSQL18 と比較しながら実測する記事です。
なお、PostgreSQL19 は本記事の執筆時点(2026年9月6日)ではベータ版(Beta3)で、正式リリース前の段階です。以降の実測値や挙動は、この Beta3 時点のものである点をあらかじめご了承ください。正式リリース版では変わる可能性があります。
以前、遅いSQLを追いかけてチューニングする記事で、「集計をJOINの前に持ってくる」といった手動でのSQL書き換えによる高速化を扱いました。PostgreSQL19 では、こうした書き換えの一部をプランナが自動で行う改善が入っています。そこで本記事では、前回と同じ環境・同じデータを使い、「手を加えずに同じSQLを実行したとき、PostgreSQL19 では何が変わるか」を確認します。
あわせて、この記事にはもう一つのテーマがあります。それは 「実行計画が変わること」と「実際に速くなること」は必ずしも一致しない という点です。今回検証した4つの改善のうち、劇的に速くなったものもあれば、計画は変わっても差がわずかだったものもありました。だからこそ、EXPLAIN ANALYZE で実際に確かめることが重要だと考えています。
本記事で検証する新機能は、前編にあたるPostgreSQLの新機能の調べ方の記事で紹介した方法(Commitfest・リリースノートのドラフトを追う)で見つけたものです。あわせて読んでいただくと、「どうやってこれらの改善を見つけたか」まで辿れます。
1.1 この記事で確認すること
- 検証(1) Eager Aggregation:集計をJOINの前に自動で実行する改善
- 検証(2) NOT IN のanti-join変換:NULL安全な条件下でのプランナの書き換え
- 検証(3) IS NOT DISTINCT FROM の単純化:非NULL列での最適化
- 検証(4) radix sort:大規模ソートの内部改善
1.2 検証環境
前回のチューニング記事と同じEC2環境に、PostgreSQL19 Beta3 を追加して比較しました。なお、本記事の実測値は、いずれもウォームアップ後3回実行したホット状態の3回目の値です。
| 項目 | 内容 |
|---|---|
| OS | AlmaLinux 10.2 |
| PostgreSQL | 18(ポート5418) / 19 Beta3(ポート5419) |
| インスタンスタイプ | m7i.large(2 vCPU、8GiB メモリ、x86_64) |
| データ | pgbench scale factor 100(pgbench_accounts 1000万行) |
| ベンチマークツール | pgbench |
本記事は PostgreSQL19 Beta3 時点での実測です。正式リリース版では挙動や既定値が変わる可能性があります。実測値も x86_64(m7i)環境でのものであり、他のアーキテクチャでは傾向が異なる場合があります。
1.3 検証環境へのPostgreSQL19 Betaの追加
PGDGリポジトリにはベータ版用のtestingリポジトリが含まれています。既定では無効化されているため、有効化してインストールします。
# testing系リポジトリの確認(正確なリポジトリ名を出力から確認する)
$ sudo dnf repolist all | grep -i testing
# pgdg19のtestingリポジトリを有効化
$ sudo dnf config-manager --set-enabled pgdg19-updates-testing
# インストール
$ sudo dnf install -y postgresql19-server postgresql19-contrib
# 初期化とポート分け(既存環境に合わせて5419を使用)
$ sudo /usr/pgsql-19/bin/postgresql-19-setup initdb
$ sudo sh -c "echo 'port = 5419' >> /var/lib/pgsql/19/data/postgresql.conf"
$ sudo systemctl enable --now postgresql-19
# バージョン確認
$ /usr/pgsql-19/bin/postgres --version
postgres (PostgreSQL) 19beta3
リポジトリ名は環境により異なる可能性があります。dnf repolist all の出力を正としてください。19系が見当たらない場合は sudo dnf update pgdg-redhat-repo でリポジトリ定義を更新すると現れることがあります。
データは両バージョンとも同じ条件にするため、pgbench -i -s 100 で投入します。検証①で使うインデックスも両方に作成しておきます。
# 両バージョンで実行(下記は5419の例)
$ sudo -u postgres /usr/pgsql-19/bin/pgbench -i -s 100 -p 5419 pgbench
$ sudo -u postgres psql -p 5419 -d pgbench -c "CREATE INDEX idx_accounts_bid ON pgbench_accounts (bid);"
$ sudo -u postgres psql -p 5419 -d pgbench -c "ANALYZE pgbench_accounts;"
2. 検証(1):Eager Aggregation ─ 集計をJOINの前に自動実行する
2.1 Eager Aggregation とは
通常、集計を伴うJOINクエリは「まず結合してから集計する」順で実行されます。しかし、集計によって行数が大きく減る場合は、「先に集計してから結合する」ほうが、結合が処理する行数を減らせて有利になることがあります。
前回のチューニング記事では、この「JOINの前に集計する」書き換えを手動で行い、高速化を確認しました。PostgreSQL19 では、この変換をプランナが自動で判断して行う Eager Aggregation が導入されています。
2.2 設定の確認
まず、この機能を制御する設定パラメータを両バージョンで確認します。
$ sudo -u postgres psql -p 5418 -c "SHOW enable_eager_aggregate;"
ERROR: unrecognized configuration parameter "enable_eager_aggregate"
$ sudo -u postgres psql -p 5419 -c "SHOW enable_eager_aggregate;"
enable_eager_aggregate
------------------------
on
PostgreSQL18 にはこの設定自体が存在せず、PostgreSQL19 では既定で on(有効)になっています。この機能は PostgreSQL19 で新たに追加され、既定で有効化されていることが確認できます。
2.3 検証クエリと実測
前回のチューニング記事で「書き換え前」として扱ったクエリ(集計を伴うJOIN)を、そのまま両バージョンで実行します。ウォームアップ後、3回実行したホット状態の値を採用します。
$ sudo -u postgres psql -p 5419 -d pgbench << 'EOF'
EXPLAIN (ANALYZE, BUFFERS)
SELECT
t.tid,
t.tbalance,
COUNT(a.aid) AS account_count,
SUM(a.abalance) AS total_balance
FROM pgbench_accounts a
JOIN pgbench_tellers t ON t.bid = a.bid
WHERE a.bid = 1
GROUP BY t.tid, t.tbalance
ORDER BY total_balance DESC;
EOF
実測結果は以下の通りです。
| バージョン | 集計の位置 | JOINが処理する行数 | Execution Time |
|---|---|---|---|
| PostgreSQL18 | JOINの後 | 約100万行 | 196.658 ms |
| PostgreSQL19 | JOINの前 | 10行 | 13.347 ms |
PostgreSQL19 は PostgreSQL18 のおよそ15分の1の時間で完了しました。前回の手動書き換えで得られた高速化(約14倍)に近い効果が、SQLを一切変更せずに得られたことになります。
実行計画を比較すると、違いは「集計ノードの位置」に現れます。
PostgreSQL18 の実行計画(集計はJOINの後)
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort (cost=14164.87..14164.89 rows=10 width=24) (actual time=195.772..196.615 rows=10.00 loops=1)
Sort Key: (sum(a.abalance)) DESC
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=3328
-> Finalize GroupAggregate (cost=14161.62..14164.70 rows=10 width=24) (actual time=195.759..196.610 rows=10.00 loops=1)
Group Key: t.tid
-> Gather Merge (cost=14161.62..14164.42 rows=24 width=24) (actual time=195.751..196.598 rows=30.00 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Sort (cost=13161.60..13161.63 rows=10 width=24) (actual time=190.890..190.892 rows=10.00 loops=3)
Sort Key: t.tid
-> Partial HashAggregate (cost=13161.33..13161.43 rows=10 width=24) (actual time=190.871..190.873 rows=10.00 loops=3)
Group Key: t.tid
-> Nested Loop (cost=0.43..10234.24 rows=390279 width=16) (actual time=0.058..111.968 rows=333333.33 loops=3)
-> Parallel Index Scan using idx_accounts_bid on pgbench_accounts a (cost=0.43..5332.22 rows=39028 width=12) (actual time=0.014..9.039 rows=33333.33 loops=3)
Index Cond: (bid = 1)
-> Materialize (cost=0.00..23.55 rows=10 width=12) (actual time=0.000..0.001 rows=10.00 loops=100000)
-> Seq Scan on pgbench_tellers t (cost=0.00..23.50 rows=10 width=12) (actual time=0.040..0.102 rows=10.00 loops=3)
Filter: (bid = 1)
Rows Removed by Filter: 990
Planning Time: 0.103 ms
Execution Time: 196.658 ms
Nested Loop が約33万行(3並列で約100万行)を処理し、その後に Partial HashAggregate で集計しています。
PostgreSQL19 の実行計画(集計はJOINの前)
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort (cost=5397.73..5397.75 rows=10 width=24) (actual time=13.317..13.318 rows=10.00 loops=1)
Sort Key: (sum(a.abalance)) DESC
Sort Method: quicksort Memory: 25kB
-> Finalize GroupAggregate (cost=0.71..5397.56 rows=10 width=24) (actual time=13.232..13.314 rows=10.00 loops=1)
Group Key: t.tid
-> Nested Loop (cost=0.71..5389.96 rows=1000 width=24) (actual time=13.227..13.308 rows=10.00 loops=1)
-> Index Scan using pgbench_tellers_pkey on pgbench_tellers t (cost=0.28..42.77 rows=10 width=12) (actual time=0.009..0.086 rows=10.00 loops=1)
Filter: (bid = 1)
Rows Removed by Filter: 990
-> Materialize (cost=0.43..5334.94 rows=100 width=20) (actual time=1.322..1.322 rows=1.00 loops=10)
-> Partial GroupAggregate (cost=0.43..5334.44 rows=100 width=20) (actual time=13.215..13.215 rows=1.00 loops=1)
Group Key: a.bid
-> Index Scan using idx_accounts_bid on pgbench_accounts a (cost=0.43..4440.94 rows=119000 width=8) (actual time=0.006..7.719 rows=100000.00 loops=1)
Index Cond: (bid = 1)
Planning Time: 0.123 ms
Execution Time: 13.347 ms
Partial GroupAggregate(Group Key: a.bid)が Nested Loop の下に入り、10万行を1行に集計してから結合しています。結果として Nested Loop が処理する行数は10行まで減っています。
2.4 この改善による副次的な変化
興味深いのは、PostgreSQL18 では並列ワーカーを2つ使っているのに対し、PostgreSQL19 では並列化していない点です。PostgreSQL18 は約100万行のJOINを処理するために並列化を選んでいますが、PostgreSQL19 は集計によってJOINが10行まで減るため、並列化する必要がないと判断していると考えられます。重い処理そのものが消えた結果、並列化の出番もなくなった、と読めます。
2.5 「バージョンの違い」ではなく「この機能の効果」であることの確認
念のため、PostgreSQL19 内で enable_eager_aggregate を無効化し、同じクエリを実行してみます。
$ sudo -u postgres psql -p 5419 -d pgbench << 'EOF'
SET enable_eager_aggregate = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT t.tid, t.tbalance, COUNT(a.aid) AS account_count, SUM(a.abalance) AS total_balance
FROM pgbench_accounts a JOIN pgbench_tellers t ON t.bid = a.bid
WHERE a.bid = 1 GROUP BY t.tid, t.tbalance ORDER BY total_balance DESC;
EOF
この場合、Execution Time は 160.270 ms となり、実行計画も PostgreSQL18 と同じ「JOINの後で集計する」形に戻りました。
PostgreSQL19(enable_eager_aggregate = off)の実行計画
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sort (cost=18972.07..18972.09 rows=10 width=24) (actual time=159.118..160.133 rows=10.00 loops=1)
Sort Key: (sum(a.abalance)) DESC
Sort Method: quicksort Memory: 25kB
-> Finalize GroupAggregate (cost=18969.74..18971.90 rows=10 width=24) (actual time=159.091..160.114 rows=10.00 loops=1)
Group Key: t.tid
-> Gather Merge (cost=18969.74..18971.67 rows=17 width=24) (actual time=159.081..160.098 rows=20.00 loops=1)
Workers Planned: 1
Workers Launched: 1
-> Sort (cost=17969.73..17969.75 rows=10 width=24) (actual time=156.266..156.268 rows=10.00 loops=2)
Sort Key: t.tid
-> Partial HashAggregate (cost=17969.46..17969.56 rows=10 width=24) (actual time=156.231..156.235 rows=10.00 loops=2)
Group Key: t.tid
-> Nested Loop (cost=0.43..12719.46 rows=700000 width=12) (actual time=0.077..83.019 rows=500000.00 loops=2)
-> Parallel Index Scan using idx_accounts_bid on pgbench_accounts a (cost=0.43..3950.93 rows=70000 width=8) (actual time=0.017..10.325 rows=50000.00 loops=2)
Index Cond: (bid = 1)
-> Materialize (cost=0.00..18.55 rows=10 width=12) (actual time=0.000..0.000 rows=10.00 loops=100000)
-> Seq Scan on pgbench_tellers t (cost=0.00..18.50 rows=10 width=12) (actual time=0.053..0.107 rows=10.00 loops=2)
Filter: (bid = 1)
Rows Removed by Filter: 990
Planning Time: 0.510 ms
Execution Time: 160.270 ms
Partial GroupAggregate(Group Key: a.bid)が消え、Nested Loop が約100万行(500,000×2)を処理してから Partial HashAggregate で集計する、PostgreSQL18 と同じ構造に戻っています。
同一バージョン・同一データで設定だけを切り替えて挙動が変わることから、この差はバージョンの違いではなく Eager Aggregation の効果であると確認できます。
3. 検証(2):NOT IN のanti-join変換
3.1 背景
NOT IN は、NULLの意味論の都合で、従来はanti-joinに変換できないことが知られていました。PostgreSQL19 では、対象列がNULLを含まないと証明できる場合に限り、NOT IN をanti-joinへ変換する改善が入っています。
3.2 検証クエリと実測
主キー同士(aid と tid、いずれもNOT NULL)を対象にした NOT IN を実行します。データは両バージョンとも pgbench -i で揃えた状態です。
$ sudo -u postgres psql -p 5419 -d pgbench << 'EOF'
EXPLAIN (ANALYZE, BUFFERS)
SELECT aid FROM pgbench_accounts
WHERE aid NOT IN (SELECT tid FROM pgbench_tellers);
EOF
| バージョン | 実行計画の構造 | Execution Time |
|---|---|---|
| PostgreSQL18 | hashed SubPlan(NOT INのまま) | 1450.008 ms |
| PostgreSQL19 | Merge Anti Join(変換) | 1368.307 ms |
実行計画は確かに変わりました。PostgreSQL18 は Filter: (NOT (ANY (aid = (hashed SubPlan 1).col1))) と NOT IN をそのまま評価しているのに対し、PostgreSQL19 は Merge Anti Join に変換しています。
PostgreSQL18 の実行計画(hashed SubPlan)
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
Index Only Scan using pgbench_accounts_pkey on pgbench_accounts (cost=18.93..284706.93 rows=5000000 width=4) (actual time=0.296..1103.363 rows=9999000.00 loops=1)
Filter: (NOT (ANY (aid = (hashed SubPlan 1).col1)))
Rows Removed by Filter: 1000
Heap Fetches: 0
SubPlan 1
-> Seq Scan on pgbench_tellers (cost=0.00..16.00 rows=1000 width=4) (actual time=0.007..0.061 rows=1000.00 loops=1)
Planning Time: 0.076 ms
Execution Time: 1450.008 ms
PostgreSQL19 の実行計画(Merge Anti Join)
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Merge Anti Join (cost=0.71..284728.21 rows=9999000 width=4) (actual time=0.586..1170.534 rows=9999000.00 loops=1)
Merge Cond: (pgbench_accounts.aid = pgbench_tellers.tid)
-> Index Only Scan using pgbench_accounts_pkey on pgbench_accounts (cost=0.43..259684.43 rows=10000000 width=4) (actual time=0.013..689.865 rows=10000000.00 loops=1)
Heap Fetches: 0
-> Index Only Scan using pgbench_tellers_pkey on pgbench_tellers (cost=0.28..31.27 rows=1000 width=4) (actual time=0.015..0.080 rows=1000.00 loops=1)
Heap Fetches: 0
Planning Time: 0.217 ms
Execution Time: 1368.307 ms
3.3 計画は変わったが、速度差は小さい
注目したいのは、実行計画は変わったにもかかわらず、Execution Time の差は約6%にとどまった点です。
今回のクエリは、照合相手である pgbench_tellers が1000行と小さく、PostgreSQL 18 の hashed SubPlan 方式でも十分に速く処理できていました。そのため、anti-join への変換による恩恵が小さかったと考えられます。変換が大きな効果を生むのは、サブクエリ側が大きい場合や、その先にさらに結合が続くような場合だと推測されますが、今回の検証範囲では確認できていません。
3.4 変換される条件について
この変換は、対象列がNOT NULLと証明できる場合に限られます。今回 aid・tid がともに主キー(NOT NULL)であるため変換されました。NULLの存在を排除できない場合は、今回のようなanti-joinへの変換は行われないと考えられます。この「安全な場合に限る(when safe)」という条件の詳細は、前編で紹介した方法でメーリングリストの議論や公式ドキュメントを辿ると確認できます。
4. 検証(3):IS NOT DISTINCT FROM の単純化
4.1 「IS NOT DISTINCT FROM」について
検証に入る前に、IS NOT DISTINCT FROM という演算子について簡単に触れておきます。
これは、ひとことで言うと 「NULL も一つの値として扱う =」 です。通常の = は、NULL が絡むと結果が真でも偽でもなく「不明(NULL)」になります。たとえば NULL = NULL は true ではなく NULL を返すため、WHERE 句では取り出せません。IS NOT DISTINCT FROM は、このNULLを普通の値のように扱って等価判定をします。
SELECT NULL IS NOT DISTINCT FROM NULL; -- true (= だと NULL)
SELECT 1 IS NOT DISTINCT FROM NULL; -- false(= だと NULL)
SELECT 1 IS NOT DISTINCT FROM 1; -- true
NULL が絡まない場合(3行目)は = と同じ挙動になり、NULL が絡む場合だけ挙動が変わります。NULLを許容する列同士を「NULL同士なら等しい」とみなして比較したいとき(新旧レコードの差分検出など)に使われます。
IS DISTINCT FROM は SQL:1999、否定形の IS NOT DISTINCT FROM は SQL:2003 で標準化された述語で、PostgreSQL 独自の拡張ではありません。
ここでポイントになるのが、対象の列が NOT NULL(NULLが絶対に入らない)と分かっている場合 です。その場合は「NULLを考慮する意味がない」ため、IS NOT DISTINCT FROM は実質的に通常の = と同じ意味になります。PostgreSQL19 では、この単純化が行われるようになりました。
4.2 検証クエリと実測
主キー aid(NOT NULL)を対象に、基準となる = と、本題の IS NOT DISTINCT FROM を比較します。
$ sudo -u postgres psql -p 5419 -d pgbench << 'EOF'
-- 基準(通常の等価)
EXPLAIN (ANALYZE, BUFFERS)
SELECT aid FROM pgbench_accounts WHERE aid = 500;
-- 本題
EXPLAIN (ANALYZE, BUFFERS)
SELECT aid FROM pgbench_accounts WHERE aid IS NOT DISTINCT FROM 500;
EOF
aid IS NOT DISTINCT FROM 500 の結果は次の通りです。基準の aid = 500 は両バージョンとも Index Only Scan(Index Cond: aid = 500) で 0.1ms 未満でした。
| バージョン | 実行計画 | Execution Time |
|---|---|---|
| PostgreSQL18 | Parallel Index Only Scan + Filter(全走査) | 388.791 ms |
| PostgreSQL19 | Index Only Scan(Index Cond: aid = 500) | 0.019 ms |
差は歴然でした。PostgreSQL18 は IS NOT DISTINCT FROM を単純化できず、条件を Filter として扱っています。インデックスの範囲検索が使えないため、1000万行を全走査(並列化まで行って)して1行を見つけています。
一方 PostgreSQL19 は、Index Cond: (aid = 500) となっており、IS NOT DISTINCT FROM が通常の = 500 に単純化されています。基準の aid = 500 のクエリと完全に同一の計画になり、インデックスで即座に該当行を返しています。
PostgreSQL18 の実行計画(単純化されず全走査)
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.43..212771.87 rows=1 width=4) (actual time=0.253..388.731 rows=1.00 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Index Only Scan using pgbench_accounts_pkey on pgbench_accounts (cost=0.43..211771.77 rows=1 width=4) (actual time=251.887..380.105 rows=0.33 loops=3)
Filter: (NOT (aid IS DISTINCT FROM 500))
Rows Removed by Filter: 3333333
Heap Fetches: 0
Planning Time: 0.030 ms
Execution Time: 388.791 ms
PostgreSQL19 の実行計画(= 500 に単純化)
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
Index Only Scan using pgbench_accounts_pkey on pgbench_accounts (cost=0.43..4.45 rows=1 width=4) (actual time=0.007..0.008 rows=1.00 loops=1)
Index Cond: (aid = 500)
Heap Fetches: 0
Planning Time: 0.112 ms
Execution Time: 0.019 ms
なお、PostgreSQL18 でも主キーのインデックスの葉を走査しているため(Index Only Scan)、テーブル本体を読むよりは速く済んでいますが、それでも1000万エントリの走査に388msを要しています。もし対象がインデックスのない列であれば、さらに大きな差になったと考えられます。
5. 検証(4):大規模ソートの性能変化(radix sort)
PostgreSQL19 では、ソート処理に radix sort を用いる改善が入りました。ここでは1000万行の整数列ソートを題材に、PostgreSQL18 との差を実測します。
この検証はソートキーとなるデータの分布によって結果が大きく変わります。準備段階でこの点につまずいた経緯も含めて記録します。
5.1 準備段階での注意点:ソートキーの分布
はじめ、pgbench_accounts の abalance 列をそのままソートキーに使おうとしました。しかし pgbench -i 直後の abalance は全行が 0 です。値がすべて同じ列をソートしても、実質的に整列済みのデータを並べ替えるだけになり、ソートアルゴリズムの差は現れません。実際、この状態では両バージョンでほとんど差が出ませんでした。
ソートアルゴリズムの違いを見るには、ソートキーの値が十分にばらついている必要があります。そこで abalance 列にランダムな整数を投入し直してから計測することにしました。テーブルやカラムの定義は変更せず、値だけを更新します。
# abalance にランダムな整数を投入してばらつきを作る(両バージョンで実行)
$ sudo -u postgres psql -p 5419 -d pgbench << 'EOF'
UPDATE pgbench_accounts SET abalance = (random() * 1000000000)::int;
VACUUM ANALYZE pgbench_accounts;
EOF
random() は乱数のため、両バージョンで投入される値の並びは一致しません。ただしこの検証で見るのは「ばらついた1000万件の整数をソートする時間」であり、両環境とも同じ条件になるため、比較に支障はないと考えられます。
5.2 検証クエリと実測
work_mem を大きめに設定してディスクソートを避け、メモリ内でのソート処理そのものを比較します。
$ sudo -u postgres psql -p 5419 -d pgbench << 'EOF'
SET work_mem = '1GB';
EXPLAIN (ANALYZE, BUFFERS)
SELECT aid, abalance FROM pgbench_accounts ORDER BY abalance;
EOF
| バージョン | Sortノードの actual time | Execution Time |
|---|---|---|
| PostgreSQL18 | 4842.727 ms | 6933.631 ms |
| PostgreSQL19 | 2573.018 ms | 4519.636 ms |
PostgreSQL18 の実行計画
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------
Sort (cost=1582003.07..1606843.08 rows=9936004 width=8) (actual time=4842.727..6568.842 rows=10000000.00 loops=1)
Sort Key: abalance
Sort Method: quicksort Memory: 627592kB
Buffers: shared hit=207199 read=120670
-> Seq Scan on pgbench_accounts (cost=0.00..427229.04 rows=9936004 width=8) (actual time=1923.022..2628.724 rows=10000000.00 loops=1)
Planning Time: 0.053 ms
Execution Time: 6933.631 ms
PostgreSQL19 の実行計画
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
Sort (cost=1588764.73..1613731.41 rows=9986671 width=8) (actual time=2573.018..2839.654 rows=10000000.00 loops=1)
Sort Key: abalance
Sort Method: quicksort Memory: 627592kB
Buffers: shared hit=189 read=327680
-> Seq Scan on pgbench_accounts (cost=0.00..427735.71 rows=9986671 width=8) (actual time=0.085..1014.448 rows=10000000.00 loops=1)
Planning Time: 0.078 ms
Execution Time: 4519.636 ms
5.3 結果の読み方
この結果を読むうえで、2つ注意しておきたい点があります。
1つ目は、Sort Method の表示だけでは違いが分からない点です。 両バージョンとも Sort Method: quicksort と表示されています。radix sort による改善は内部実装レベルのものであり、実行計画上の表示名は変わらないと考えられます。この改善の効果は、表示名ではなく Sortノードの actual time の差として読み取ることになります。
2つ目は、Execution Time 全体ではなく Sortノードの actual time で比較すべき点です。 実行計画をよく見ると、PostgreSQL18 側は下段の Seq Scan の actual time が計測ごとにばらついていました。これは更新直後のヒープの状態に起因する可能性があり、スキャン部分の条件が両バージョンで完全には揃っていません。
一方、Sortノードの actual time はスキャンが完了した後の純粋なソート処理を表すため、スキャン側のばらつきの影響を受けにくい指標です。今回の条件では、PostgreSQL19のソート処理そのものが速くなっていることが確認できました。内部のradix sort改善がその一因と考えられますが、今回の実測だけではその寄与を切り分けることはできません。
Execution Time 全体での比較は、スキャン部分の条件差を含むため、ここでは参考値として扱います。ソートアルゴリズムの効果を見る目的では、Sortノードの actual time に注目するのが適切だと考えられます。
6. もっと深く学びたい方へ
各機能の詳細は、公式のリリースノートおよびドキュメントを参照してください。
| ページ | URL |
|---|---|
| PostgreSQL19 リリースノート(開発版) | https://www.postgresql.org/docs/19/release-19.html |
| EXPLAIN(PostgresSQL18) | https://www.postgresql.jp/docs/18/sql-explain.html |
| 新機能の調べ方(本記事の前編) | https://qiita.com/matsutomu/items/65812f336356f74eff44 |
| 遅いSQLのチューニング(本記事の土台) | https://qiita.com/matsutomu/items/2ce5f5eb906c996de3b1 |
7. まとめ
本記事では、PostgreSQL19 のプランナ改善4点を PostgreSQL18 と比較しました。
- 検証(1):Eager Aggregation ─ 集計をJOINの前に自動実行する改善。前回手動で行った書き換えが自動化され、今回の検証では約15倍の高速化が見られました。既定で有効です
- 検証(2):NOT IN のanti-join変換 ─ 実行計画は Merge Anti Join に変換されましたが、今回の検証では速度差は約6%にとどまりました。変換の効果はクエリの条件によって変わると考えられます
-
検証(3):IS NOT DISTINCT FROM の単純化 ─ NOT NULL列では通常の
=に単純化され、インデックスが使えるようになりました。今回の検証では大きな差が見られました -
検証(4):radix sort ─
Sort Methodの表示は変わりませんが、Sortノードの actual time で見ると、値がばらついた整数キーのソートで短縮傾向が見られました
4つを通して見えてくるのは、「実行計画が変わること」と「実際に速くなること」は必ずしも一致しない という点です。検証(1)(3)のように劇的に効く改善もあれば、検証(2)のように計画は変わっても差が小さい場合もあります。また検証④のように、表示名が変わらず actual time を見ないと気づけない改善もあります。
新しいバージョンの改善を自分の環境で活かせるかどうかは、リリースノートを読むだけでは判断できません。前回のチューニング記事から一貫してお伝えしている通り、EXPLAIN ANALYZE で実際に測ってみること が、やはり確実な方法だと考えています。
なお、本記事の内容は PostgreSQL19 Beta3 時点のものです。正式リリース後に、あらためて追試したいと考えています。
次回は、ストレージとメンテナンスに関する改善(REPACK・TOAST圧縮・autovacuumの並列化など)を扱う予定です。