さて、今回はCTEのダークサイド――ありがちなミスについて話そうと思う。どんなにイケてるクエリでも、CTEを間違って使うと一発で壊れるからね。でも心配しなくてOK!ここで診断&回避のガイドをしっかり伝授するよ!
1. ミス:CTEのマテリアライズとその影響
PostgreSQLでCTEを使うときの大きな特徴の一つが、デフォルトでマテリアライズされること。つまり、CTEの結果が一旦メモリ(データが多いとディスク)に保存されるんだ。クエリが多かったりデータ量がデカいと、これが実行速度をガッツリ落とす原因になる。
例:
WITH heavy_data AS (
SELECT * FROM large_table
)
SELECT * FROM heavy_data WHERE column_a > 100;
パッと見はCTEでデータをフィルタしてるだけに見える。でも実際はheavy_dataが最初に全部ロード&マテリアライズされてからフィルタがかかる。これ、めっちゃ時間かかることもあるよ。
どう回避する?
PostgreSQL 12以降なら、CTEをinline式(サブクエリみたいな感じ)で使えるようになった。これでマテリアライズ問題が解決できる。1回しか使わないCTEや、中間結果を保存する必要がない場合はこれでOK。
最適化した例:
WITH inline_data AS MATERIALIZED (
SELECT * FROM large_table
)
SELECT * FROM inline_data WHERE column_a > 100;
アドバイス:マテリアライズしたいならMATERIALIZEDを、したくないならNOT MATERIALIZEDを指定しよう。
2. ミス:再帰CTEが無限ループする
再帰CTEは超パワフルだけど、イテレーションの深さ制限なしで使うと無限ループにハマることがある。これ、実行が遅くなるだけじゃなくて、リソースも全部食い尽くすから注意!
例:
WITH RECURSIVE endless_loop AS (
SELECT 1 AS value
UNION ALL
SELECT value + 1
FROM endless_loop
)
SELECT * FROM endless_loop;
このクエリ、止まる条件がないから永遠に行を生成し続けるよ。
どう回避する?
WHEREで明確な停止条件をつけよう。例えば:
WITH RECURSIVE limited_loop AS (
SELECT 1 AS value
UNION ALL
SELECT value + 1
FROM limited_loop
WHERE value < 10
)
SELECT * FROM limited_loop;
アドバイス:大きな階層を再帰CTEで扱うなら、PostgreSQLのmax_recursion_depthオプションで再帰の深さを制限しよう。
3. ミス:UNIONとUNION ALLの使い方ミス
CTEでベースクエリと再帰クエリを組み合わせるとき、UNIONとUNION ALLの選択ミスで予期しない結果になることがある。例えばUNIONは重複行を消すけど、その分計算コストもかかる。
例:
WITH RECURSIVE employee_hierarchy AS (
SELECT employee_id, manager_id
FROM employees
WHERE manager_id IS NULL
UNION -- ここはUNION ALLがベター
SELECT e.employee_id, e.manager_id
FROM employees e
JOIN employee_hierarchy eh ON e.manager_id = eh.employee_id
)
SELECT * FROM employee_hierarchy;
この例だとUNIONが大事な行まで消しちゃうことがあるし、クエリも遅くなる!
どう直す?
重複を消す必要がなければUNION ALLを使おう:
UNION ALL
4. ミス:1つのクエリにCTEを詰め込みすぎ
クエリを構造化しようとして、CTEを10個も20個も入れる人がいるけど、これはコードが読みにくくなるだけじゃなく、PostgreSQLのクエリプランナーも混乱する。
例:
WITH cte1 AS (...),
cte2 AS (...),
cte3 AS (...),
...
cte20 AS (...)
SELECT ...
FROM cte20;
これ、開発者にとっては悪夢だよね。
どう直す?
— クエリをもっとシンプルに分割しよう。巨大な1クエリにCTEを詰め込むより、複数の独立したクエリに分けた方がいい。
— 中間結果を何度も使うなら、一時テーブルに保存するのもアリ。
5. ミス:インデックスなしで重いCTEを使う
大量データを扱うCTEで、テーブルにインデックスを付け忘れるとクエリが激遅になる。インデックスはDBのブーストアイテムだよ!
例:
WITH filtered_data AS (
SELECT * FROM large_table WHERE unindexed_column = 'value'
)
SELECT * FROM filtered_data;
どう直す?
CTEを使う前に、テーブルが最適化されてるかチェックしよう:
CREATE INDEX idx_large_table ON large_table(unindexed_column);
6. ミス:CTEで何度もデータを呼び出そうとする
CTEは一度作られて実行されたら「凍結」される。もし複数箇所で結果を使いたい場合、データは再計算されない――これが時々バグの元になる。
例:
WITH data AS (
SELECT x, y FROM some_table
)
SELECT x FROM data
WHERE y > 10;
-- もう一度dataを再計算したい場合、されないので注意。
どう直す?
動的な再計算や変化が必要なら、CTEじゃなくてサブクエリを使おう。
7. ミス:コメントがない
CTEは超便利だけど、2週間後に自分も他人も読めないSQLクエリなんて誰も嬉しくないよね?
例:
WITH data_filtered AS (
SELECT *
FROM large_table
WHERE some_column > 100
)
SELECT * FROM data_filtered;
1ヶ月後には、なんでこのデータをフィルタしたのか誰も覚えてないはず!
だから、特に複雑なCTEや再帰CTEを使うときはコメントをつけよう:
WITH data_filtered AS (
-- some_column > 100の条件でデータをフィルタする
SELECT *
FROM large_table
WHERE some_column > 100
)
SELECT * FROM data_filtered;
8. ミス:CTEを一時テーブルの代わりに乱用する
一時テーブルの方が向いてるケースもある。例えば、結果を複数のクエリで使いたいときや、超大量データを扱うとき。
例:
WITH temp_data AS (
SELECT * FROM large_table
)
SELECT * FROM temp_data WHERE column_a > 100;
SELECT * FROM temp_data WHERE column_b < 50;
このCTEだと、同じデータなのに2回クエリが実行されちゃう!
どう直す?
何度も使うデータなら、一時テーブルを作ろう:
CREATE TEMP TABLE temp_table AS
SELECT * FROM large_table;
SELECT * FROM temp_table WHERE column_a > 100;
SELECT * FROM temp_table WHERE column_b < 50;
最後のアドバイス
どんな強力な機能も、CTEが万能ってわけじゃない。何のために、どう使うかをちゃんと考えよう。「CTE多ければ多いほどイイ!」はパフォーマンスも可読性も下げるだけ。必ずパフォーマンステストとクエリの最適化も忘れずに!
GO TO FULL VERSION