【実務・中級編】 CTEマテリアライゼーション – PostgreSQL

「WITH句はただの読みやすいクエリじゃない」——PostgreSQLのCTEマテリアライゼーションと付き合う作法

やあ。最近、SQLのパフォーマンスチューニングで頭を抱えてるって? 分かるよ、その気持ち。

PostgreSQLを触り始めた頃、みんな一度は`WITH`句(CTE)の虜になるんだ。「複雑なクエリがこんなにスッキリ書けるなんて!」ってね。でも、ある日突然、何の前触れもなくクエリが遅くなる。そんな経験、ないかな?

今日は、そんな罠になりがちな「CTEマテリアライゼーション(Materialization)」について、現場目線で深掘りしてみよう。

—

CTEは「最適化の敵」になることもある

PostgreSQL 12以前、CTEは「最適化の境界」だった。つまり、`WITH`句で定義したテーブルは必ず一度メモリ(あるいはディスク)に書き出されて、完全に独立した一時テーブルとして扱われていたんだ。

これをマテリアライゼーションと呼ぶ。

これが便利な時もある。巨大な結果セットを一度だけ作って、それを何度も参照したいときには最強だよね。でも、逆に言えば、「外側のクエリで絞り込み条件(WHERE句)があるのに、CTEの中では全件スキャンしてから絞り込んでいる」なんて無駄な動きを強制されることもあった。

PostgreSQL 12以降の進化

PostgreSQL 12からは、デフォルトで「条件が良ければインライン展開(CTEの中身をクエリ本体に直接埋め込む)」するように賢くなった。でも、この「賢さ」が裏目に出ることもあるんだ。

—

実践:マテリアライゼーションを制御するテクニック

実務でよくあるのが、「インライン展開されてほしくないのに展開されて遅くなる」パターンや、その逆だ。そんな時は、`MATERIALIZED` または `NOT MATERIALIZED` というヒント句を使って、こちらで主導権を握る必要がある。

具体例:インライン展開させたくないケース

例えば、ものすごく重い計算の結果を、メインクエリで何度も使い回したいとする。

WITH heavy_calculation AS MATERIALIZED (
SELECT user_id, SUM(amount) as total
FROM transactions
GROUP BY user_id
)
SELECT
FROM heavy_calculation h1
JOIN heavy_calculation h2 ON h1.user_id = h2.user_id + 1;

ここで `MATERIALIZED` を明示的に指定しているのは、「この計算、めちゃくちゃ重いから、一度だけ実行して結果をキャッシュしておいてくれ!」というプランナーへの命令だ。

もしこれを書かないと、PostgreSQLが気を利かせて(?)インライン展開してしまい、同じ重い計算を2回走らせてしまう……なんて悲劇が起きる。「何度も使うなら、計算を使い回すためにマテリアライズさせる」。これが鉄則だ。

逆に、インライン展開してほしいケース

逆に、CTEの中で絞り込みをしているのに、マテリアライズのせいで全件スキャンが走っている場合はこうだ。

WITH recent_orders AS NOT MATERIALIZED (
SELECT FROM orders WHERE created_at > ‘2023-01-01’
)
SELECT FROM recent_orders WHERE user_id = 123;

`NOT MATERIALIZED` を使うことで、「いちいちキャッシュを作らずに、メインのWHERE句と統合してインライン展開してくれ!」と指示できる。これによって、`user_id` のインデックスがちゃんと効くようになるわけだ。

—

先輩からのアドバイス:どう使い分ける?

現場でエンジニアにアドバイスするなら、まずはこれを確認してほしい。

1. 実行計画(EXPLAIN ANALYZE)を見ること。
「なんとなく遅い」で終わらせず、CTEが `CTE Scan` になっているのか、それともバラバラに展開されているのかを確認するんだ。
2. 副作用を理解する。
`MATERIALIZED` はキャッシュを作るコストがかかる。データ量が小さいならインライン展開の方が速いし、データ量が膨大なら一度固めた方がいい。
3. 迷ったら「分割」を検討する。
CTEで悩むくらいなら、普通にテンポラリテーブルを作ってインデックスを貼る、という選択肢も忘れないでほしい。DBエンジニアとしての「正攻法」は、時に一番の近道になることもある。

—

最後に

CTEは、コードの可読性を上げるための「魔法の杖」じゃない。あくまでクエリの一部なんだ。

「便利だから」という理由だけで使い続けるんじゃなくて、「PostgreSQLのオプティマイザと対話する手段」としてCTEを捉えてみると、チューニングの景色がガラッと変わるはずだよ。

もしクエリが遅くなったら、まずは `EXPLAIN` を叩いて、プランナーが気を利かせすぎているのか、それともサボっているのか、その目で確かめてみてくれ。

それじゃ、また現場で会おう。良いクエリライフを!

コメント

タイトルとURLをコピーしました