はじめに
PostgreSQLを使っていると、 「動くけど遅いSQL」や「なぜか重くなる処理」 に遭遇することがあります。
この記事では、実務で遭遇した“重いSQL”を具体的にどう書き直せば高速化できるかを、Before/After形式で紹介します。
- クエリ最適化の基本
- 実務で見逃しがちなNGパターン
- インデックスや集約関数の使い方
など、読んだその日から役立つ知見を詰め込みました。
1. WHERE句で関数を使うとインデックスが効きません
❌ Before:
SELECT * FROM users WHERE to_char(created_at, 'YYYY-MM-DD') = '2024-06-01';
問題点:関数を使用するとインデックスが無効化されます
✅ After:
SELECT * FROM users WHERE created_at >= '2024-06-01' AND created_at < '2024-06-02';
改善点:インデックスが効くよう、日付の範囲指定で記述を変更します
2. OFFSETは重いため、カーソルベースに切り替えましょう
❌ Before:
SELECT * FROM orders ORDER BY created_at DESC OFFSET 10000 LIMIT 20;
問題点:OFFSETが大きいと実質全件スキャンに近くなります
✅ After:
SELECT * FROM orders
WHERE created_at < '2024-06-01 12:00:00'
ORDER BY created_at DESC
LIMIT 20;
改善点:カーソル(ページングの基準値)を使って高速化しています
3. COUNT(*)を速くしたいときは対象を絞りましょう
❌ Before:
SELECT COUNT(*) FROM access_logs;
問題点:巨大テーブルの全件カウントは非常に遅くなります
✅ After:
SELECT reltuples::bigint FROM pg_class WHERE relname = 'access_logs';
改善点:精度よりも速度を優先し、推定件数を取得します
4. EXISTSの代わりにJOIN+LIMIT 1を使うこともできます
❌ Before:
SELECT * FROM users WHERE EXISTS (
SELECT 1 FROM orders WHERE orders.user_id = users.id
);
問題点:EXISTSは条件によっては非効率です
✅ After:
SELECT DISTINCT users.* FROM users
JOIN orders ON orders.user_id = users.id
LIMIT 1;
改善点:LIMITを使うことで、存在チェックを最小処理で実現します
5. GROUP BYとJOINの順番に注意しましょう
❌ Before:
SELECT u.name, COUNT(o.id)
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.name;
問題点:JOIN対象のテーブルが巨大だとGROUP BYが重くなります
✅ After:
SELECT name, order_count FROM (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
) o
JOIN users u ON u.id = o.user_id;
改善点:先に集約してからJOINすることで処理量を削減しています
まとめ:遅いSQLには必ず“理由”があります
課題 | 解決の観点 |
---|---|
関数でWHERE句 | 範囲条件で書き直す |
OFFSET地獄 | カーソルベースに変える |
COUNT全件 | 推定件数で代替する |
EXISTS過多 | JOIN+LIMITで代替可能 |
JOIN+GROUP重複 | 集約を先に行う |
速くなるSQLは、読みやすくなるSQLでもあります。
パフォーマンスは設計の問題であり、構文の問題ではありません。
爆速化のポイントは「当たり前だと思っている構文」を、“実行計画の目線”で見直すことです。
おわりに
PostgreSQLはチューニング性が高い一方で、“気づかなければ損をするポイント”も多くあります。
実務で悩んだとき、この記事が一つの気づきになることを願っています。