「とりあえずクエリ投げてない?」PostgreSQLの性能を劇的に変える『述語のプッシュダウン』の話
やあ。最近コードレビューをしていて、ふと思ったことがあるんだ。「SQLって、ただ結果が返ってくればいいってもんじゃないよな」ってね。
特にPostgreSQLを使っていると、データ量が数百万、数千万件になった瞬間に「あれ、昨日まで速かったのに…」なんてことがよくある。その原因の多くは、実はデータベースのオプティマイザを信じすぎているか、逆にオプティマイザが迷うような書き方をしていることにあるんだ。
今日は、そんな泥沼にハマらないための必須テクニック「述語のプッシュダウン(Predicate Pushdown)」について、現場の視点から深掘りしてみようと思う。
—
そもそも「述語のプッシュダウン」って何?
簡単に言うと、「絞り込み条件(WHERE句)を、できるだけ早い段階で適用して、処理する行数を減らすこと」だ。
想像してみてほしい。君が図書館で「2020年以降に出版された、量子力学の専門書」を探すとしよう。
- 悪いやり方: 図書館にある全ジャンルの全書籍を一度自分のデスクに運んできて、そこから量子力学を探し、さらに年号で絞り込む。
- 良いやり方: 司書さんに「量子力学コーナーの、2020年以降の棚だけ見てきて」と頼む。
データベースの世界も同じだ。特にサブクエリやJOINが絡むとき、PostgreSQLが気を利かせて「先に絞り込んでから結合しようぜ」と判断してくれるのがプッシュダウンなんだけど、書き方次第では「先に全部結合してから絞り込む」という非効率なプランを選択してしまうことがある。
—
具体的に見てみよう:非効率なクエリの正体
例えば、こんなクエリはどうだろう。
— よくある「やってしまいがちな」クエリ
SELECT
FROM (
SELECT FROM orders
JOIN users ON orders.user_id = users.id
) AS combined_data
WHERE combined_data.status = ‘COMPLETED’;
一見何の問題もなさそうに見えるよね。でも、PostgreSQLの古いバージョンや複雑なビュー越しの場合、この書き方だと「一旦全件結合してから、最後にステータスで絞り込む」という、とんでもなく重い処理をすることがあるんだ。
これを明示的に、あるいは構造的に「先に絞り込む」形に書き換えるのが、エンジニアの腕の見せ所だ。
こう変えるだけで世界が変わる
SELECT
FROM (
SELECT FROM orders WHERE status = ‘COMPLETED’
) AS completed_orders
JOIN users ON completed_orders.user_id = users.id;
こう書けば、PostgreSQLは迷わず「`orders`テーブルを先にスキャンして、条件に合う行だけ取り出してから`users`と結合しよう」と判断する。これがまさにプッシュダウンの恩恵を最大限に引き出す書き方だ。
—
現場で役立つチェックリスト
僕がレビューでよく指摘するのは、以下のケースだ。心当たりがないかチェックしてみてほしい。
- ビュー(View)を多用していないか?
- 複雑なViewをJOINすると、オプティマイザがプッシュダウンの判断を諦めることがある。Viewの中身が何をしているか、一度`EXPLAIN ANALYZE`で確認する癖をつけよう。
- 関数をWHERE句で使っていないか?
- `WHERE TO_CHAR(created_at, ‘YYYY-MM-DD’) = ‘2023-01-01’` みたいに列に関数を適用すると、インデックスが効かなくなるだけじゃなく、プッシュダウンの最適化も阻害される。`created_at >= ‘2023-01-01’ AND created_at < '2023-01-02'` と書くのが鉄則だ。
- EXPLAIN ANALYZEを相棒にする
- クエリの実行計画に `Filter:` と出ていたら、それは「スキャンした後に絞り込んでいる」証拠。もしその行数が多いなら、そこがボトルネックだ。
—
最後に:完璧を求めすぎないことも大切
もちろん、PostgreSQLのオプティマイザは非常に優秀だ。最近のバージョン(PostgreSQL 12以降など)では、かなり賢くプッシュダウンしてくれる。だから、「なんでもかんでも手動で書き換えなきゃ!」と神経質になる必要はない。
ただ、「データが増えたときに、このクエリはどう動くんだろう?」と想像する力は、データベースエンジニアにとって一番の武器になる。
もしパフォーマンスで悩んだら、まずは `EXPLAIN ANALYZE` を叩いて、PostgreSQLが「どの順番でデータを処理しているか」を覗いてみてほしい。きっと、そこに解決のヒントが隠れているはずだから。
それじゃ、また現場で会おう。いいSQLライフを!
コメント