
【初心者向け】 PostgreSQL の EXPLAIN の出力を確認してみた
最近 PostgreSQL をよく触るのですが、EXPLAIN 結果について曖昧な理解でした。
今回は以下のように手を動かしながら、どんな内容が出力されているのか見てみました。
- 10 万行のレコードを持つサンプルテーブルを作成
- 任意のクエリについて EXPLAIN を実行
- テーブルにインデックスを追加し、再度 EXPLAIN を実行
- EXPLAIN 出力結果およびインデックス有無で何が変わるのかを確認する
実行環境
実行環境は EC2(AL2023)にインストールした PostgreSQL を使用しました。
EC2 に PostgreSQL をインストールする手順としては下記をご参照ください。
今回は PostgreSQL 17.7 を利用し、検証していきます。
[ec2-user@ip-10-0-13-178 ~]$ sudo -u postgres psql
psql (17.7)
Type "help" for help.
postgres=# select * from version();
version
--------------------------------------------------------------------------------------------------------------
PostgreSQL 17.7 on x86_64-amazon-linux-gnu, compiled by gcc (GCC) 11.5.0 20240719 (Red Hat 11.5.0-5), 64-bit
(1 row)
検証用 DB とテーブルの作成
PostgreSQL に接続し以下を実行
-- 検証用データベース(explain_test)の作成
postgres=# CREATE DATABASE explain_test;
CREATE DATABASE
-- explain_test データベースが作成できているか確認
postgres=# \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
--------------+----------+----------+-----------------+---------+---------+--------+-----------+-----------------------
explain_test | postgres | UTF8 | libc | C.UTF-8 | C.UTF-8 | | |
postgres | postgres | UTF8 | libc | C.UTF-8 | C.UTF-8 | | |
...
(4 rows)
-- データベース切り替え
postgres=# \c explain_test
You are now connected to database "explain_test" as user "postgres".
-- テーブル(users)作成
explain_test=# CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT,
age INT
);
CREATE TABLE
-- 10 万件のデータを投入
explain_test=# INSERT INTO users (name, age)
SELECT
'user_' || i,
(random() * 100)::INT
FROM generate_series(1, 100000) i;
INSERT 0 100000
-- レコード件数の確認
explain_test=# SELECT COUNT(*) FROM users;
count
--------
100000
(1 row)
作成したテーブルの構造を以下に示します。
-- 最初の 5 行を表示
explain_test=# SELECT * FROM users LIMIT 5;
id | name | age
----+--------+-----
1 | user_1 | 17
2 | user_2 | 47
3 | user_3 | 89
4 | user_4 | 17
5 | user_5 | 36
(5 rows)
-- 最後の 5 行を表示
explain_test=# SELECT * FROM users ORDER BY id DESC LIMIT 5;
id | name | age
--------+-------------+-----
100000 | user_100000 | 39
99999 | user_99999 | 54
99998 | user_99998 | 65
99997 | user_99997 | 66
99996 | user_99996 | 65
(5 rows)
--各カラムの説明
id: 1 〜 100,000 までの連番
name: user_1 〜 user_100000 までの文字列
age: 0 〜 100 のランダムな整数
補足:
age をランダムな整数にしているのは
「WHERE age = 25」のような条件で絞り込んだときに
インデックスの効果が出やすくするためです。
EXPLAIN の実行
EXPLAIN コマンドでクエリの実行計画を確認します。
explain_test=# EXPLAIN SELECT * FROM users WHERE age = 25;
QUERY PLAN
-----------------------------------------------------------
Seq Scan on users (cost=0.00..1887.00 rows=960 width=18)
Filter: (age = 25)
(2 rows)
上記の出力結果のそれぞれの意味について以下に示します。
Seq Scan on users
PostgreSQL がどんな方法でデータを取得するかを示したもの。
「Seq Scan = Sequential Scan = 全件スキャン」なので、テーブルの先頭から末尾まで全件を1行ずつ読む動作になる。
シーケンシャルスキャンは、日本語に直訳すると「順次検索」という形になります。端的に言ってしまえば「すべてのデータを順に検索する」ということで、結果として通常はすべてのデータを読み込んで返します。
一番の基本、シーケンシャルスキャン - gihyo.jp
https://gihyo.jp/admin/serial/01/rdbms-autumn-sky/0002
この問い合わせにはWHERE句がありませんので、テーブル行をすべてスキャンしなければなりません。 このためプランナは単純なシーケンシャルスキャン計画を使用することを選びました。
PostgreSQL 17.6文書 - 14.1. EXPLAINの利用
https://www.postgresql.jp/document/17/html/using-explain.html
cost=0.00..1887.00
PostgreSQL がこのクエリにかかるコストを見積もった値。
cost=A..B の形式。 A が初期コスト(最初の1行を返すまでのコスト)、B が総コスト(全行を返し終わるまでのコスト)
cost に単位はない。これらの数値は PostgreSQL が内部で使う相対的な指標。
数字が大きいほど重い処理と考えれば OK
・初期処理の推定コスト。 出力段階が開始できるようになる前に消費される時間、例えば、SORTノードで実行されるソート処理の時間です。
・全体推定コスト。 これは計画ノードが実行完了である、つまりすべての利用可能な行を受け取ることを前提として示されます。 実際には、ノードの親ノードはすべての利用可能な行を読む前に停止する可能性があります(以下のLIMITの例を参照)。PostgreSQL 17.6文書 - 14.1. EXPLAINの利用
https://www.postgresql.jp/document/17/html/using-explain.html
rows=960
PostgreSQL が「このクエリは約960行返すだろう」と推定した値。
これはあくまで推定値であり、実際の件数と異なる場合がある。
・この計画ノードが出力する行の推定数。ここでも、ノードが実行を完了することを前提としています。
PostgreSQL 17.6文書 14.1. EXPLAINの利用
https://www.postgresql.jp/document/17/html/using-explain.html
width=18
取得される行の 1 行あたりの平均サイズ(バイト単位)。
各カラムのデータ型のサイズを元に算出される(例:integer は 4 バイト、boolean は 1 バイトなど)。
概算なので、実際の値と異なることがある。
・この計画ノードが出力する行の(バイト単位での)推定平均幅。
PostgreSQL 17.6文書 14.1. EXPLAINの利用
https://www.postgresql.jp/document/17/html/using-explain.html
表 8.1に組み込みの汎用データ型をすべて示します。
表8.1 データ型PostgreSQL 17.6文書 第8章 データ型
https://www.postgresql.jp/document/17/html/datatype.html
Filter: (age = 25)
WHERE 句の条件が表示されている。
今回の EXPLAIN 結果では Seq Scan と Filter がセットになっているので、10万件を全部読んだあとに age = 25 の行だけを残すという動きとなる。
960 件を取得するために 10 万件を全部読んでいることになるため、
大量のデータから少数の行を絞り込む場合は非効率になる可能性がある。
EXPLAINの出力が、Seq Scan計画ノードに付随する「フィルタ」条件として適用されるWHERE句を表示していることに注意してください。 これは、この計画ノードがスキャンした各行に対してその条件を検査することを意味し、その条件を通過したもののみが出力されます。
PostgreSQL 17.6文書 - 14.1. EXPLAINの利用
https://www.postgresql.jp/document/17/html/using-explain.html
インデックスを付与して EXPLAIN を実行
現状ではインデックスは PRIMARY KEY(id カラム)だけ存在しています。
なので、age カラムにインデックスを追加します。
-- 現状のインデックス状態を確認
-- users テーブルには id カラムのみインデックスが作られている
explain_test=# \d users
Table "public.users"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+-----------------------------------
id | integer | | not null | nextval('users_id_seq'::regclass)
name | text | | |
age | integer | | |
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
-- age カラムにインデックスを作成
explain_test=# CREATE INDEX idx_users_age ON users(age);
CREATE INDEX
-- 作成できたか確認
-- idx_users_age が作成できているので OK
explain_test=# \d users
Table "public.users"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+-----------------------------------
id | integer | | not null | nextval('users_id_seq'::regclass)
name | text | | |
age | integer | | |
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
"idx_users_age" btree (age)
前項と同じクエリの EXPLAIN を実行します。
explain_test=# EXPLAIN SELECT * FROM users WHERE age = 25;
QUERY PLAN
-------------------------------------------------------------------------------
Bitmap Heap Scan on users (cost=11.73..690.90 rows=960 width=18)
Recheck Cond: (age = 25)
-> Bitmap Index Scan on idx_users_age (cost=0.00..11.49 rows=960 width=0)
Index Cond: (age = 25)
(4 rows)
上記結果より、インデックスを貼ったことで、前項の Seq Scan から Bitmap Heap Scan に変化したことがわかります。
実行順序について
EXPLAIN の出力はインデントが深いノードから先に実行されます。
今回の場合、以下の順番で実行されます。
Step 1: Bitmap Index Scan on idx_users_age
インデックスを使って age = 25 の行がどこにあるかを調べる
Step 2: Bitmap Heap Scan on users
Step 1 で調べた場所から実際のデータを取りに行く
Bitmap Index Scan と Bitmap Heap Scan については以下の記事が参考になります。
端的に言うと、Bitmap Index Scan で行がどこにあるのかを把握し、それを元に Bitmap Heap Scan で実際にデータを取りに行くという仕組みです。
Bitmap Index Scan uses an index to identify matching rows for a query condition. Instead of directly fetching rows, it creates a bitmap in memory where each bit corresponds to a row in the table. For example, A set bit (1) indicates that the corresponding row matches the condition.
(Bitmap Index Scanは、クエリ条件に一致する行を特定するためにインデックスを使用します。直接行を取得する代わりに、テーブルの各行に対応するビットを持つビットマップをメモリ上に作成します。例えば、セットされたビット(1)は、対応する行が条件に一致することを示します。)Bitmap Heap Scan uses the bitmap produced by the Bitmap Index Scan to efficiently fetch the actual rows from the heap (table data files). It groups these accesses by data pages, reducing random disk I/O.
(Bitmap Heap Scanは、Bitmap Index Scanによって生成されたビットマップを使用して、ヒープ(テーブルデータファイル)から実際の行を効率的に取得します。これらのアクセスをデータページごとにグループ化することで、ランダムディスクI/Oを削減します。)PostgreSQL Bitmap Index Scan And Bitmap Heap Scan
https://kenwagatsuma.com/blog/postgresql-bitmap-index-scan-and-bitmap-heap-scan
cost について
親ノードである Bitmap Heap Scan の cost=11.73..690.90 は
子ノードである Bitmap Index Scan の cost=0.00..11.49 を含んだ上での総コストです。
すなわち全体の総コストは 690.90 となります。
上位ノードのコストには、すべての子ノードのコストもその中に含まれていることを理解することは重要です。
PostgreSQL 17.6文書 - 14.1. EXPLAINの利用
https://www.postgresql.jp/document/17/html/using-explain.html
インデックスなし vs インデックスあり
最後に実行結果の再掲と両者(インデックスなし、あり)の比較です。
explain_test=# EXPLAIN SELECT * FROM users WHERE age = 25;
QUERY PLAN
-----------------------------------------------------------
Seq Scan on users (cost=0.00..1887.00 rows=960 width=18)
Filter: (age = 25)
(2 rows)
explain_test=# EXPLAIN SELECT * FROM users WHERE age = 25;
QUERY PLAN
-------------------------------------------------------------------------------
Bitmap Heap Scan on users (cost=11.73..690.90 rows=960 width=18)
Recheck Cond: (age = 25)
-> Bitmap Index Scan on idx_users_age (cost=0.00..11.49 rows=960 width=0)
Index Cond: (age = 25)
(4 rows)
両者の比較を以下に示します。総コストが 1887.00 から 690.90 へ、約 63% 削減されました。
| 項目 | インデックスなし | インデックスあり |
|---|---|---|
| スキャン方法 | Seq Scan | Bitmap Heap Scan |
| 総コスト | 1887.00 | 690.90 |
| 処理ステップ | 1段階 | 2段階 |
| 動き | 全件読んで絞り込む | 場所を調べてから取りに行く |
終わりに
今回は、簡単なサンプルテーブルを用意して、EXPLAIN 出力を確認してみました。
EXPLAIN を使うことで、クエリを実行する前にスキャン方法(全件スキャン、ビットマップスキャンなど)や推定コストを確認することができます。
また、クエリが段階的に処理されていることなどもわかるので、自分が作成したクエリが想定した動きをしているかを確認する際にも役立ちそうです。
本ブログがどなたかのお役に立てば幸いです。
参考情報










