CodeGym /コース /SQL SELF /CTE vs サブクエリ:どっちを選ぶ?

CTE vs サブクエリ:どっちを選ぶ?

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

もう知ってると思うけど、CTEを使うとコードが読みやすくなるんだ。でも、いつもCTEを使うべきなの?時にはシンプルなサブクエリの方が速くてラクなこともある。どんな時にどっちが有利なのか、一緒に見ていこう!

サブクエリ:速くてシンプル

サブクエリって、SQLの中にSQLを書くやつだよね。メインのクエリに直接埋め込んで、その場で実行される。単発のシンプルな処理にはピッタリ:

-- 平均価格より高い商品を探す
SELECT product_name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);

ここではサブクエリが一回だけ平均価格を計算して、それで終わり。余計な構造はなし。

パフォーマンス:どっちが速い?

サブクエリは、シンプルな処理ならスピードで勝つことが多い。PostgreSQLはサブクエリを「その場で」最適化できるし、特に一回だけ実行される場合は速い:

-- 速い:サブクエリは一度だけ実行される
SELECT customer_id, order_total
FROM orders
WHERE order_date = (SELECT MAX(order_date) FROM orders);

CTEはデフォルトでマテリアライズされる ― PostgreSQLはまずCTEの結果を計算して、一時テーブルとして保存してから使う。これがシンプルなクエリだと遅くなることも:

-- 遅い:CTEは一時テーブルにマテリアライズされる
WITH latest_date AS (
    SELECT MAX(order_date) AS max_date FROM orders
)
SELECT customer_id, order_total
FROM orders, latest_date
WHERE order_date = max_date;

でも!PostgreSQL 12以降ならマテリアライズをコントロールできる:

-- 強制的にマテリアライズしない
WITH latest_date AS NOT MATERIALIZED (
    SELECT MAX(order_date) AS max_date FROM orders
)
SELECT customer_id, order_total
FROM orders, latest_date
WHERE order_date = max_date;

何度も使うなら:ここはCTEの出番

同じ中間結果を何回も使いたい時、CTEがめっちゃ便利:

-- サブクエリ:同じロジックを2回書く
SELECT
    (SELECT COUNT(*) FROM orders WHERE status = 'completed') AS completed_orders,
    (SELECT COUNT(*) FROM orders WHERE status = 'completed') * 100.0 / COUNT(*) AS completion_rate
FROM orders;

-- CTE:一度計算して2回使う
WITH completed_orders AS (
    SELECT COUNT(*) AS count FROM orders WHERE status = 'completed'
)
SELECT
    co.count AS completed_orders,
    co.count * 100.0 / (SELECT COUNT(*) FROM orders) AS completion_rate
FROM completed_orders co;

複雑な分析:CTEが圧勝

多段階の分析には、CTEでカオスが整理される。売上レポートを比べてみて:

サブクエリの場合(頭がごちゃごちゃ):

SELECT 
    category,
    revenue,
    revenue * 100.0 / (
        SELECT SUM(p.price * oi.quantity)
        FROM order_items oi
        JOIN products p ON oi.product_id = p.product_id
        JOIN orders o ON oi.order_id = o.order_id
        WHERE EXTRACT(year FROM o.order_date) = 2024
    ) AS revenue_share
FROM (
    SELECT 
        p.category,
        SUM(p.price * oi.quantity) AS revenue
    FROM order_items oi
    JOIN products p ON oi.product_id = p.product_id
    JOIN orders o ON oi.order_id = o.order_id
    WHERE EXTRACT(year FROM o.order_date) = 2024
    GROUP BY p.category
) category_revenue;

CTEの場合(全部スッキリ整理):

WITH yearly_sales AS (
    SELECT 
        p.category,
        p.price * oi.quantity AS sale_amount
    FROM order_items oi
    JOIN products p ON oi.product_id = p.product_id
    JOIN orders o ON oi.order_id = o.order_id
    WHERE EXTRACT(year FROM o.order_date) = 2024
),
category_revenue AS (
    SELECT 
        category,
        SUM(sale_amount) AS revenue
    FROM yearly_sales
    GROUP BY category
),
total_revenue AS (
    SELECT SUM(sale_amount) AS total FROM yearly_sales
)
SELECT 
    cr.category,
    cr.revenue,
    cr.revenue * 100.0 / tr.total AS revenue_share
FROM category_revenue cr, total_revenue tr;

再帰:CTEの独壇場

階層構造にはサブクエリは無力。

再帰CTEだけが「マネージャーの全部下を探す」みたいなタスクをこなせる:

WITH RECURSIVE employee_hierarchy AS (
    -- まずはCEOからスタート
    SELECT employee_id, manager_id, name, 1 AS level
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- 各レベルの部下を追加
    SELECT e.employee_id, e.manager_id, e.name, eh.level + 1
    FROM employees e
    JOIN employee_hierarchy eh ON e.manager_id = eh.employee_id
)
SELECT * FROM employee_hierarchy ORDER BY level, name;

デバッグとメンテナンス

CTEはパーツごとに簡単にデバッグできる:

-- まず最初のステップをチェック
WITH active_customers AS (
    SELECT customer_id FROM customers WHERE status = 'active'
)
SELECT COUNT(*) FROM active_customers; -- ロジックが正しいか確認

-- 次のステップを追加
WITH active_customers AS (...),
recent_orders AS (
    SELECT customer_id, COUNT(*) as order_count
    FROM orders
    WHERE order_date >= '2024-01-01'
    GROUP BY customer_id
)
SELECT COUNT(*) FROM recent_orders; -- こっちもチェック

サブクエリはデバッグがちょっと面倒 ― 文脈から引っ張り出さないといけない。

実践アドバイス

サブクエリを使うべき時:

  • ロジックがシンプルで1行で済む時
  • シンプルな処理でパフォーマンスを最大化したい時
  • 中間結果が一度しか使われない時
  • データ量が少ない時

CTEを使うべき時:

  • クエリが複雑で論理的に分けたい時
  • 中間結果を何度も使いたい時
  • コードの読みやすさ・メンテしやすさが大事な時
  • 階層構造(再帰CTE)が必要な時
  • 複雑なロジックをパーツごとにデバッグしたい時

ゴールデンルール

まずはサブクエリから始めよう。もし読みにくくなったりロジックが繰り返されるなら、CTEに切り替えよう。半年後の自分(か、未来の同僚)がきっと感謝してくれるよ!

2
タスク
SQL SELF, レベル 28, レッスン 1
ロック未解除
CTEを使ったデータのフィルタリング
CTEを使ったデータのフィルタリング
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION