プロのデータベースエンジニアを目指す上で、実行計画(Execution Plan)を正確に読み解く力は必須スキルです。SQLが「なぜ遅いのか」を推測ではなく、確信を持って指摘できるようになることが本シリーズの目標です。
第1回は、100万件規模のデータを用いて、PostgreSQL 16における「全件スキャン」の実行計画を読み解くための実験環境を構築します。
0. 動作環境
本記事では、エンタープライズ環境で標準的なOracle Linux 9を使用します。
$ cat /etc/os-release
NAME="Oracle Linux Server"
VERSION="9.7"
1. PostgreSQL 16 のインストール
OS標準のリポジトリではなく、PostgreSQL公式のPGDGリポジトリから最新のv16を導入します。
# ① 公式リポジトリの追加
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
# ② OS標準モジュールの無効化とインストール
sudo dnf -qy module disable postgresql
sudo dnf install -y postgresql16-server
# ③ データベースの初期化
sudo /usr/pgsql-16/bin/postgresql-16-setup initdb
# ④ サービスの起動と自動起動設定
sudo systemctl enable --now postgresql-16
2. ログイン認証の仕組みと設定
PostgreSQLのデフォルトは「ピア認証」です。作業をスムーズにするため、自分のOSユーザーと同じ名前のDBユーザーを作成しておきます。
# OSユーザーと同じ名前のDBユーザーとDBを作成
sudo -u postgres createuser --superuser $(whoami)
sudo -u postgres createdb $(whoami)
# 以降は「psql」だけで接続可能になります
psql -c "SELECT version();"
3. 学習用サンプルの構築(100万件データ)
十分なデータ量を持つテーブルを作成し、プランナが「コスト」を計算しやすい状態を作ります。
-- ① 100万件のユーザーテーブル
CREATE TABLE users (
user_id INT,
user_name TEXT,
age INT,
registered_at TIMESTAMP
);
INSERT INTO users (user_id, user_name, age, registered_at)
SELECT
i,
'user_name_' || i,
(random() * 80 + 10)::int,
now() - (random() * interval '365 days')
FROM generate_series(1, 1000000) s(i);
-- ② 300万件の注文履歴テーブル
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
user_id INT,
amount DECIMAL(10, 2),
order_date TIMESTAMP
);
INSERT INTO orders (user_id, amount, order_date)
SELECT
(random() * 999999 + 1)::int,
(random() * 10000)::decimal,
now() - (random() * interval '365 days')
FROM generate_series(1, 3000000) s(i);
-- 統計情報を最新にする
ANALYZE users;
ANALYZE orders;
4. 実行計画の取得と解読
インデックスがない状態で、特定の user_id を検索した際の実行計画を確認します。
EXPLAIN ANALYZE SELECT * FROM users WHERE user_id = 500000;
出力された実行計画(全内容)
Gather (cost=1000.00..14444.43 rows=1 width=32) (actual time=98.328..99.137 rows=1 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on users (cost=0.00..13444.33 rows=1 width=32) (actual time=71.401..85.771 rows=0 loops=3)
Filter: (user_id = 500000)
Rows Removed by Filter: 333333
Planning Time: 0.163 ms
Execution Time: 99.167 ms
実行計画の読み方のポイント
実行計画を読む際の最大のコツは、「インデント(字下げ)が最も深い行から順に、上に向かって読み進める」ことです。
今回の例では、以下の順序でデータが流れています(-> は上の処理へデータを渡す子ノードを示します)。
- Parallel Seq Scan:まず、複数のプロセスでテーブル
users を読み込み始めます。
- Filter:読み込みながら、その行が
user_id = 500000 かどうかを判定します。
- Gather:各プロセスの結果を合流させ、最終的な結果として返します。
「Parallel Seq Scan」= 全件スキャンの証拠
プロのエンジニアがこれを見て「全件スキャンだ」と断定する理由は2つあります。
- 名称の意味:
Seq Scan は Sequential Scan(順次走査)の略。インデックスを使わず、先頭から1行ずつ順番に中身を確認する動作そのものを指します。
- Rows Removed by Filter:これは条件に合わず除外された行数です。今回
loops=3(3プロセス)が約33万件ずつ除外しており、合計で約100万件をチェックしたことがわかります。
| 項目 |
意味 |
| cost=1000.00..14444.43 |
プランナが算出した予測コスト。左が最初の1行、右が完了までの累積。 |
| actual time=... |
実測値。このクエリは約99ミリ秒かかっています。 |
| loops=3 |
処理が3つのプロセスで並列実行されたことを示します。 |
まとめ
100万件のデータに対し、インデックスがないとDBは「力技(全件スキャン)」で解決しようとします。たった1件を探すために100万件を読み込むこの挙動を、実行計画から読み取ることができました。
次回は、この「無駄」を劇的に改善するインデックスの作成と、それに伴う実行計画の変化を解説します。