CodeGym /コース /SQL SELF /CTEを使うときによくあるミスとその回避法

CTEを使うときによくあるミスとその回避法

SQL SELF
レベル 28, レッスン 4
使用可能

さて、今回は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多ければ多いほどイイ!」はパフォーマンスも可読性も下げるだけ。必ずパフォーマンステストとクエリの最適化も忘れずに!

2
タスク
SQL SELF, レベル 28, レッスン 4
ロック未解除
CTEの最適化のためのMATERIALIZEDの使用
CTEの最適化のためのMATERIALIZEDの使用
1
アンケート/クイズ
クエリ最適化、レベル 28、レッスン 4
使用不可
クエリ最適化
クエリ最適化
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION