「全件走査で息切れする前に」— PostgreSQLの制約除外(Constraint Exclusion)を使いこなそう
やあ。今日はちょっとしたパフォーマンス改善の話をしようか。
PostgreSQLを触っていると、ログデータや時系列データのように、放っておくと数億行に膨れ上がるテーブルを扱うことってあるよね。そんな時、`WHERE`句で期間を指定しているのに、なぜか実行計画が「Sequential Scan(全件走査)」を選択して絶望した経験はないかな?
「インデックスは貼ってあるのに、なんで?」と頭を抱える君へ。今日は、PostgreSQLの隠れた(でも強力な)武器である「制約除外(Constraint Exclusion)」について話をしよう。
—
制約除外って、結局なに?
一言で言うと、「クエリの条件に明らかに合致しないテーブルを、プランナが最初から『見なかったこと』にしてくれる機能」だ。
例えば、1年分のログテーブルを月ごとにパーティション分割しているとする。ユーザーが「1月のデータだけ見たい」と検索したとき、賢いPostgreSQLは「CHECK制約」を見て、2月以降のデータが入っているテーブルをスキャン対象からバッサリ切り捨てる。これが制約除外の仕組みだ。
これがあるおかげで、数テラバイトの巨大なデータセットからでも、特定の期間だけを数ミリ秒で引き抜くことができるようになるんだ。
—
実践:どうやって設定するのか?
仕組みはシンプルだ。テーブルの定義時に`CHECK`制約をしっかり書いておくこと。これが全てと言ってもいい。
例えば、こんなテーブルを作ったとする。
CREATE TABLE logs_2023_01 (
log_id SERIAL PRIMARY KEY,
created_at TIMESTAMP CHECK (created_at >= ‘2023-01-01’ AND created_at < '2023-02-01'),
message TEXT
);
CREATE TABLE logs_2023_02 (
log_id SERIAL PRIMARY KEY,
created_at TIMESTAMP CHECK (created_at >= ‘2023-02-01’ AND created_at < '2023-03-01'),
message TEXT
);
この状態で、1月のデータだけを検索してみよう。
SELECT FROM logs_2023_01
UNION ALL
SELECT FROM logs_2023_02
WHERE created_at >= ‘2023-01-15’ AND created_at < '2023-01-16';
このクエリを実行した時、プランナは「`logs_2023_02`には2月以降のデータしか入っていない(CHECK制約でそう決まっている)」ということを理解する。結果、`logs_2023_02`へのアクセスを完全にスキップして、`logs_2023_01`だけをスキャンするんだ。
---
注意点:現場でハマりやすい罠
この機能、非常に便利なんだけど、いくつか注意点がある。
- 設定値の確認: `postgresql.conf`で `constraint_exclusion` が `on` または `partition` になっているか確認してくれ。デフォルトでオンになっているはずだけど、たまに誰かがいじっていることがあるんだ(笑)。
- 型変換の罠: これが一番よくあるミスなんだけど、`WHERE`句でカラムの型と違う型を比較すると、制約除外が効かないことがある。「`created_at`(TIMESTAMP型)に対して、文字列で比較する」といった書き方は避けよう。プランナが「あ、これ型が違うからCHECK制約が適用できるか判断できないな…念のため全部見とくか」と全件走査に逃げてしまうんだ。
- 最新のPostgreSQLなら: 最近は「宣言的パーティショニング(Declarative Partitioning)」が主流だ。こちらを使えば、制約除外の設定を意識しなくても勝手に最適化が働く。もし君の環境がPostgreSQL 10以降なら、なるべく宣言的パーティショニングへの移行を検討してみてほしい。
—
先輩からのアドバイス
実務でこの最適化を意識すると、クエリが速くなるだけじゃない。データベースの「運用コスト」が劇的に下がるんだ。
古いログテーブルを削除する時も、この構造なら`DROP TABLE`一発で済む。`DELETE`文で重いトランザクションを走らせて、ディスクがパンパンになるまでVACUUMと戦う必要なんてないんだよ。
「とりあえず全部ぶち込む」のは簡単だけど、「どうやって検索するか」を設計段階から意識するのが、僕らエンジニアの腕の見せ所だよね。
もし君のプロジェクトで、クエリのレスポンスが年々重くなっているなら、一度`EXPLAIN`を叩いてみてくれ。そこに「無駄なスキャン」が隠れていないか、確認するだけでも大きな進歩になるはずだ。
また何か詰まったら、いつでも聞きに来てくれ。一緒にコードを読み解こう。
コメント