PostgreSQLは、オープンソースながら高機能なデータベースとして多くの現場で利用されています。しかし、データ量の増加や複雑なクエリ処理が進むと、「クエリの実行速度が遅い」「レスポンスが悪くなった」といったパフォーマンスの問題に直面することも少なくありません。
本記事では、「PostgreSQLの処理速度を上げる」ために実践できるテクニックを、できるだけわかりやすく解説していきます。インデックスの使い方からクエリの最適化、設定ファイルのチューニングまで、幅広い視点からパフォーマンス改善のポイントを紹介します。
「最近、データベースが重いな・・・」と感じている方は、ぜひこの記事を参考に、改善への第一歩を踏み出してみてください。
インデックスの適切な活用
データベースの処理速度を上げるうえで、まず注目したいのがインデックスの活用です。インデックスは、書籍の巻末にある索引のようなもので、検索処理を効率化してくれる仕組みです。特にデータ量が増えてくると、その効果は絶大です。
基本のインデックス
PostgreSQLでは、テーブルの特定の列に対してインデックスを作成できます。例えば、以下のようなSQLでインデックスを張ることができます。
CREATE INDEX idx_users_email ON users(email);
このようにインデックスを張ることで、email
列を使った検索の処理速度が大きく向上します。
インデックスが効かないケースに注意
インデックスを作成していても、以下のようなケースではインデックスが使われない(スキャンされない)ことがあります。
- 関数を使った検索(例:
LOWER(email) = 'test@example.com'
) - ワイルドカードの前方一致(例:
LIKE '%example.com'
) - キャストや計算式を含む検索
- 統計的に選択度が低い(=ヒット数が多すぎる)列
こうした場合は、関数インデックス(式インデックス)の活用や、クエリの書き方の工夫が必要です。
-- 関数インデックスの例
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
効果を確認するには?
インデックスが実際に効いているかどうかを確認するには、EXPLAIN
またはEXPLAIN ANALYZE
を使います。
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';
「Seq Scan(全件走査)」ではなく「Index Scan」と表示されていれば、インデックスが利用されています。
クエリの最適化
いくらハードウェアや設定を整えても、クエリそのものが非効率だと処理速度はなかなか改善しません。実際、パフォーマンス問題の多くは「クエリの書き方」に起因しているケースが多いです。
この章では、よくあるクエリの落とし穴や、パフォーマンスを向上させるためのポイントを紹介します。
EXPLAIN / EXPLAIN ANALYZE でボトルネックを把握
まず最初にやるべきは、現状のクエリがどのように実行されているかを知ることです。
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 123;
このように書くと、PostgreSQLがどのようにテーブルをスキャンし、どれくらい時間がかかっているかがわかります。
注目すべきポイントは以下の通りです。
Seq Scan
(全件走査)なのか、Index Scan
が使われているか- 実行時間(Total runtime)
- 行数の見積もり精度(rows)
これを使って、「どこにムダがあるか」を探すのがクエリ改善の第一歩です。
サブクエリ vs JOIN の使い分け
複雑なクエリになってくると、「サブクエリ」と「JOIN」の使い分けが重要になります。
-- サブクエリ例(非効率になりがち)
SELECT * FROM orders
WHERE customer_id IN (SELECT id FROM customers WHERE region = 'Tokyo');
-- JOINを使った例(効率的なケースが多い)
SELECT o.* FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.region = 'Tokyo';
JOINの方が、インデックスを活用しやすく、実行計画も安定しやすい傾向があります。もちろんケースバイケースですが、パフォーマンスが悪いサブクエリはJOINへの書き換えを検討してみましょう。
CTE(WITH句)の使いすぎに注意
PostgreSQLではCTE(Common Table Expression)を使ってクエリを見やすく整理できますが、実はパフォーマンス的には注意が必要です。
CTEはPostgreSQL上では一時的なサブクエリとして毎回フル実行されるため、JOINやサブクエリよりも遅くなることがあります。
-- パフォーマンスに影響を与える可能性がある例
WITH recent_orders AS (
SELECT * FROM orders WHERE created_at > now() - interval '7 days'
)
SELECT * FROM recent_orders WHERE amount > 10000;
最適化されないCTEは実行計画が分断されるため、大量データを扱う場合は注意が必要です。
LIMIT / OFFSET の落とし穴
大量データをページングで扱うときによく使うLIMIT
とOFFSET
ですが、OFFSETが大きいとレスポンスが遅くなることがあります。
SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 10000;
このクエリでは、PostgreSQLは1万件をスキップしてからデータを返す必要があるため、意外と重い処理になります。
対策としては、
- カーソル方式やIDの範囲指定を検討する
- 前回取得したIDを元に次ページを取得するスタイルにする
不要なデータアクセスの削減
PostgreSQLの処理速度を上げるうえで、もう一つ重要なのが「いかに無駄なデータを読まないか」という視点です。どんなにインデックスやクエリが工夫されていても、そもそも不要なデータまで取得してしまうと意味がありません。
この章では、データアクセスを最小限に抑えるためのポイントを紹介します。
必要なカラムだけをSELECTする
よくあるパターンが、何も考えずに「SELECT *
」を使ってしまうこと。
-- 悪い例:全カラムを取得してしまう
SELECT * FROM users WHERE status = 'active';
このような書き方は、不要なカラムまで読み込んでしまい、I/Oコストが無駄に高くなる原因になります。特にテーブルにBLOBやTEXTのような重たいカラムがある場合は注意が必要です。
-- 良い例:必要なカラムだけを明示する
SELECT id, name, email FROM users WHERE status = 'active';
これだけでも、レスポンスが目に見えて速くなるケースもあります。
適切なフィルター(WHERE句)の活用
クエリで条件を絞ることは基本中の基本ですが、WHERE句の書き方次第でインデックスの有無にも影響します。
以下のような書き方は、インデックスが効かない典型例です。
-- NG例:関数を使ってしまっている
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';
これを回避するためには、関数インデックスを使うか、クエリ側での関数利用を避けるようにしましょう。
テーブルパーティショニングの活用
テーブルのデータが数千万件、億単位に達すると、いくらインデックスがあっても処理に限界があります。
そんな時に有効なのが「テーブルパーティショニング(分割)」です。
パーティショニングを使えば、PostgreSQLは必要なパーティション(物理的な小さなテーブル)だけにアクセスするようになり、スキャンコストを大きく削減できます。
-- 例:日付ごとのパーティションに分割
CREATE TABLE orders (
id SERIAL,
customer_id INT,
created_at DATE,
amount NUMERIC
) PARTITION BY RANGE (created_at);
たとえば、「2024年4月以降のデータだけを検索したい」というようなケースで、不要な過去データをスキャンせずに済むようになります。
ビューやマテリアライズドビューの使い分け
よく使う集計結果や結合結果は、ビュー(VIEW
)やマテリアライズドビュー(MATERIALIZED VIEW
)として定義することで、毎回のクエリ処理を軽くできます。
- ビュー:動的にデータを取得(都度実行される)
- マテリアライズドビュー:一度実行して結果を保存、更新は明示的に行う
特に重いJOIN
やGROUP BY
を含むクエリは、マテリアライズドビュー化すると効果的です。
autovacuum の設定確認
PostgreSQLには、自動的にテーブルのメンテナンスを行ってくれる便利な機能があります。それがautovacuum
(オートバキューム)。
ただし、このautovacuum
の挙動が適切でないと、パフォーマンスがジワジワと悪化していく原因になります。
autovacuumとは?
PostgreSQLはMVCC(マルチバージョン同時実行制御)という仕組みによって、更新や削除があってもすぐには行を物理的に消さずに“過去バージョン”として残します。
このため、以下のような問題が発生します:
- 不要なデータが蓄積し、テーブルサイズが膨張する
- インデックスの精度が落ちる
- クエリのスキャン効率が悪化する
これらを自動的にクリーンアップしてくれるのがautovacuum
です。
実行状況を確認する
まずは現在のautovacuum
の実行状況を確認してみましょう。
SELECT relname, last_autovacuum, last_analyze
FROM pg_stat_user_tables
ORDER BY last_autovacuum DESC;
このクエリで、各テーブルが最後にいつautovacuum
されたかが分かります。
もし、更新が頻繁なテーブルで「last_autovacuum
がNULL」の場合、それは要注意!
パラメータ設定を見直そう
autovacuum
の挙動はPostgreSQLの設定ファイル(通常はpostgresql.conf
)でコントロールできます。
主要なパラメータ
パラメータ名 | 内容 |
---|---|
autovacuum_vacuum_threshold | VACUUM実行の行数しきい値(デフォルト50) |
autovacuum_vacuum_scale_factor | VACUUM実行の割合しきい値(デフォルト0.2 = 20%) |
autovacuum_naptime | 実行間隔(デフォルト1分) |
autovacuum_max_workers | 同時に動けるワーカー数(デフォルト3) |
特定のテーブルだけautovacuum
を強化したいときは、ALTER TABLE
で個別設定もできます。
ALTER TABLE orders SET (autovacuum_vacuum_scale_factor = 0.05);
この例では、「ordersテーブルは5%更新されたらvacuumしてね」という設定になります。
autovacuum の負荷とチューニング
autovacuum
は便利ですが、裏で勝手に動いているため、タイミングが悪いとパフォーマンスに悪影響を与えることもあります。
そんな時は、
- 実行頻度をコントロールする
- 影響の大きいテーブルは手動でVACUUM/ANALYZEする
autovacuum_vacuum_cost_limit
などで負荷の上限を調整する
といった対応で、バランスの良いチューニングが可能です。
まとめ
本記事では、PostgreSQLのパフォーマンスを向上させるためにいくつかの方法を紹介しました。
PostgreSQLのパフォーマンスを向上させるには、「設定をいじれば速くなる」なんて単純な話ではなく、クエリ・データ構造・システム設定の三位一体の見直しが必要です。
処理速度が上がると、アプリも、ユーザー体験も、運用のストレスもすべてが変わります。
「なんとなく使ってる」から「ちゃんと理解して速く使う」へ。
これが、PostgreSQLと本気で向き合う第一歩です。
日々の運用の中で「速さ」と「安定性」の両立を目指すためにも、今回の内容が皆さまの参考になれば幸いです。
コメント