CTEと一時テーブル

CTEと一時テーブル

🌍 実世界のアナロジー

複雑な料理を想像してください:

CTEと一時テーブルはどちらも「先に準備して後で使う」アプローチで、複雑なクエリを明確かつ整理された状態にします。


🎯 コア概念

WITH句 / CTE

CTE(共通テーブル式)は WITH キーワードを使用して一時的な名前付き結果セットを定義し、現在のクエリ内でテーブルのように参照できます。

SQL
WITH cte_name AS (
    -- CTEクエリ
    SELECT column1, column2 FROM table1
)
-- メインクエリがCTEを参照
SELECT * FROM cte_name;

利点

再帰的CTE

再帰的CTEは WITH RECURSIVE で定義され、2つの部分からなります:

  1. アンカーメンバー:初期クエリ、再帰の開始点
  2. 再帰メンバ:自分自身を参照し、ステップごとに展開
SQL
WITH RECURSIVE cte_name AS (
    -- アンカー:初期クエリ
    SELECT ... FROM table WHERE condition
    UNION ALL
    -- 再帰:自分自身を参照
    SELECT ... FROM table JOIN cte_name ON condition
)
SELECT * FROM cte_name;

ユースケース:組織図、カテゴリディレクトリ、経路探索、その他の階層データ。

CTEとサブクエリの比較

特徴 CTE サブクエリ
可読性 優れている(先に名前を付け、後で使用) 深くネストすると読みにくい
再利用性 複数回参照 参照されるたびに再実行
再帰 ✅ サポート ❌ 非サポート
パフォーマンス 通常同じ実行計画に最適化される 同じ
スコープ 現在の文のみ 現在の文のみ
💡 ヒント: クエリの論理が複雑で、同じ結果セットを複数回参照する必要がある場合は、CTEを優先してください。

一時テーブル — CREATE TEMPORARY TABLE

一時テーブルは現在のデータベースセッション内に存在し、セッション終了時に自動的に削除されます。

SQL
CREATE TEMPORARY TABLE temp_result (
    id INT,
    name VARCHAR(50),
    amount DECIMAL(10,2)
);

INSERT INTO temp_result SELECT ...;
SELECT * FROM temp_result;

ユースケース

CTEユースケースまとめ

シナリオ 推奨アプローチ
単純なクエリの中間結果 サブクエリ
複雑なクエリをステップに分割 CTE
ツリー/階層データの走査 再帰的CTE
複数のSQL文でデータを共有 一時テーブル
中間結果にインデックスが必要 一時テーブル

WITH RECURSIVEによるツリーデータの走査

従業員テーブルの department_id や自己参照の manager_id のようなフィールドは階層関係を形成できます。再帰的CTEで簡単に走査できます:

SQL
-- 従業員の全上司を見つける(上方向に走査)
-- 部門の全下位部門を見つける(下方向に走査)
-- 日付シリーズ(数値シリーズ)を生成

📝 基本構文

SQL
-- 基本的なCTE
WITH cte_name AS (
    SELECT ...
)
SELECT ... FROM cte_name;

-- 複数のCTE
WITH cte1 AS (...),
     cte2 AS (...)
SELECT ... FROM cte1 JOIN cte2 ON ...;

-- 再帰的CTE
WITH RECURSIVE cte_name AS (
    -- アンカー(開始点)
    SELECT ... FROM table WHERE initial_condition
    UNION ALL
    -- 再帰(自分自身を参照)
    SELECT ... FROM table 
    JOIN cte_name ON join_condition
    WHERE termination_condition
)
SELECT * FROM cte_name;

-- 一時テーブル
CREATE TEMPORARY TABLE temp_name AS
SELECT ... FROM ...;

-- または手動で作成
CREATE TEMPORARY TABLE temp_name (
    col1 INT,
    col2 VARCHAR(50)
);
💡 ヒント:

  • CTEは、直後に続く単一の SELECT/INSERT/UPDATE/DELETE 文でのみ使用可能
  • 再帰的CTEには終了条件が必要。さもなければ無限ループになる
  • 一時テーブルの名前は既存のテーブル名と競合してはならない
  • MySQLは CREATE TEMPORARY TABLE、SQL Serverは #temp_table を使用

📌 例

例:CTEを使った複雑なクエリの分解 — 部門一人当たり売上分析(難易度⭐⭐)

SQL
WITH dept_stats AS (
    SELECT 
        d.department_id,
        d.department_name AS department,
        COUNT(DISTINCT e.employee_id) AS employee_count,
        COUNT(o.order_id) AS order_count,
        COALESCE(SUM(o.total_amount), 0) AS total_sales
    FROM departments d
    LEFT JOIN employees e ON d.department_id = e.department_id
    LEFT JOIN orders o ON e.employee_id = o.employee_id AND o.status != 'cancelled'
    GROUP BY d.department_id, d.department_name
)
SELECT 
    department,
    employee_count,
    order_count,
    total_sales,
    CASE WHEN employee_count > 0 THEN ROUND(total_sales / employee_count, 2) ELSE 0 END AS sales_per_capita,
    CASE WHEN employee_count > 0 THEN ROUND(order_count * 1.0 / employee_count, 2) ELSE 0 END AS orders_per_capita
FROM dept_stats
ORDER BY sales_per_capita DESC;
▶ 試してみよう

結果

TEXT
department | employee_count | order_count | total_sales | sales_per_capita | orders_per_capita
-----------+----------------+-------------+-------------+------------------+------------------
Sales      |              2 |           4 |    15100.00 |          7550.00 |              2.00
Tech       |              2 |           3 |     8750.00 |          4375.00 |              1.50
Marketing  |              1 |           0 |        0.00 |             0.00 |              0.00
Finance    |              0 |           0 |        0.00 |             0.00 |              0.00

説明dept_stats CTEがまずベースデータを集計し、メインクエリが一人当たりの指標を計算します。論理が明確で保守しやすいです。

例:再帰的CTE — 従業員の管理階層(難易度⭐⭐)

employees テーブルに直属の上司を示す manager_id フィールドがあると仮定します:

SQL
-- まずmanager_idカラムとデータを追加
ALTER TABLE employees ADD COLUMN manager_id INT;

UPDATE employees SET manager_id = NULL WHERE employee_id = 1;  -- Zhang Wei:社長
UPDATE employees SET manager_id = 1    WHERE employee_id = 2;  -- Li Na -> Zhang Wei
UPDATE employees SET manager_id = 1    WHERE employee_id = 3;  -- Wang Qiang -> Zhang Wei
UPDATE employees SET manager_id = 3    WHERE employee_id = 4;  -- Zhao Min -> Wang Qiang
UPDATE employees SET manager_id = 3    WHERE employee_id = 5;  -- Liu Yang -> Wang Qiang
UPDATE employees SET manager_id = NULL WHERE employee_id = 6;  -- Chen Jing:上司なし

-- 再帰クエリ:Zhang Weiから始め、全部下を見つける(下方向に走査)
WITH RECURSIVE emp_hierarchy AS (
    -- アンカー:開始点(Zhang Wei)
    SELECT 
        employee_id,
        CONCAT(first_name, last_name) AS name,
        manager_id,
        0 AS level,
        CAST(CONCAT(first_name, last_name) AS CHAR(500)) AS path
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- 再帰:部下を見つける
    SELECT 
        e.employee_id,
        CONCAT(e.first_name, e.last_name),
        e.manager_id,
        h.level + 1,
        CAST(CONCAT(h.path, ' → ', e.first_name, e.last_name) AS CHAR(500))
    FROM employees e
    INNER JOIN emp_hierarchy h ON e.manager_id = h.employee_id
)
SELECT 
    CONCAT(REPEAT('  ', level), name) AS org_chart,
    level,
    path
FROM emp_hierarchy
ORDER BY path;
▶ 試してみよう

結果

TEXT
org_chart      | level | path
---------------+-------+----------------------------
Zhang Wei      |     0 | Zhang Wei
  Li Na        |     1 | Zhang Wei → Li Na
  Wang Qiang   |     1 | Zhang Wei → Wang Qiang
    Zhao Min   |     2 | Zhang Wei → Wang Qiang → Zhao Min
    Liu Yang   |     2 | Zhang Wei → Wang Qiang → Liu Yang
Chen Jing      |     0 | Chen Jing

説明

  1. アンカーメンバーが「上司なし」の全トップレベル従業員を見つける
  2. 再帰メンバがレベルごとに部下を見つけ、level + 1 で深さを記録
  3. REPEAT(' ', level) がインデントで階層を視覚的に表示
  4. CAST(... AS CHAR(500)) が再帰中の文字列切り捨てを防止

🎬 シナリオ演習

シナリオ1:顧客生涯価値をステップごとに計算

SQL
WITH order_summary AS (
    -- ステップ1:各顧客の注文データを集計
    SELECT 
        customer_id,
        MIN(order_date) AS first_purchase,
        MAX(order_date) AS last_purchase,
        COUNT(order_id) AS order_count,
        SUM(total_amount) AS total_spent
    FROM orders
    WHERE status != 'cancelled'
    GROUP BY customer_id
),
customer_ltv AS (
    -- ステップ2:生涯日数と支出頻度を計算
    SELECT 
        customer_id,
        first_purchase,
        last_purchase,
        order_count,
        total_spent,
        DATEDIFF(last_purchase, first_purchase) AS lifetime_days,
        CASE 
            WHEN DATEDIFF(last_purchase, first_purchase) > 0 
            THEN ROUND(total_spent / (DATEDIFF(last_purchase, first_purchase) / 30.0), 2)
            ELSE total_spent
        END AS monthly_avg_spent
    FROM order_summary
)
SELECT 
    customer_id,
    first_purchase,
    last_purchase,
    order_count,
    total_spent,
    lifetime_days,
    monthly_avg_spent,
    CASE
        WHEN monthly_avg_spent >= 3000 THEN '高額顧客'
        WHEN monthly_avg_spent >= 1000 THEN '中額顧客'
        ELSE '低額顧客'
    END AS customer_tier
FROM customer_ltv
ORDER BY total_spent DESC;

ポイント:複数のCTEをチェーンし、各ステップの論理が明確です。まず集計し、次に計算し、最後に分類します。ネストされたサブクエリよりはるかに可読性が高いです。

シナリオ2:日付シリーズを生成して売上トレンド分析

SQL
WITH RECURSIVE date_series AS (
    -- アンカー:開始日
    SELECT CAST('2026-01-01' AS DATE) AS dt
    UNION ALL
    -- 再帰:毎回1日追加
    SELECT DATE_ADD(dt, INTERVAL 1 DAY)
    FROM date_series
    WHERE dt < '2026-06-30'
)
SELECT 
    ds.dt AS date,
    COALESCE(COUNT(o.order_id), 0) AS order_count,
    COALESCE(SUM(o.total_amount), 0) AS sales
FROM date_series ds
LEFT JOIN orders o ON o.order_date = ds.dt AND o.status != 'cancelled'
GROUP BY ds.dt
ORDER BY ds.dt;

ポイント:再帰的CTEが連続的な日付シリーズを生成し、ordersテーブルとのLEFT JOINにより注文がない日も表示されます(値は0)。トレンドチャートの作成に適しています。


❓ よくある質問

質問:CTEと一時テーブルの違いは何ですか? 回答: CTEは現在の文内でのみ有効で、文の終了時に消滅します。一時テーブルはセッション全体で有効で、複数のSQL文から参照できます。CTEは単一の複雑なクエリを分解するのに適し、一時テーブルは中間結果に複数回アクセスする必要があるシナリオに適しています。

質問:再帰的CTEは無限ループになりますか? 回答: はい、適切な終了条件がない場合は無限ループになります。再帰部分に深さまたは範囲を制限する WHERE 条件を必ず含めてください。MySQLには cte_max_recursion_depth パラメータ(デフォルト1000)があり、無限再帰を防止します。

質問:CTEにインデックスを作成できますか? 回答: いいえ、CTE自体にインデックスを作成することはできません。中間結果にインデックスが必要な場合は、代わりに一時テーブルを使用してください。一時テーブルはインデックスをサポートしています。

質問:複数のCTEは相互に参照できますか? 回答: はい。同じ WITH 句で定義された複数のCTEは、それ以前のCTEを参照できます。ただし、相互参照はできません(CTE AがCTE Bを参照し、CTE BもCTE Aを参照する場合)。


📖 まとめ

技術 スコープ 再利用可能 再帰 インデックス可能
サブクエリ 単一文
CTE 単一文 ✅ 複数回参照
一時テーブル セッション全体 ✅ 複数のSQL文

📝 演習

  1. CTEを使って各部門の最高給与従業員を見つけ、その給与と部門平均の差を表示してください。
  2. 再帰的CTEを使って1から20までの数値シリーズを生成してください。
  3. 一時テーブルを使って各顧客の注文サマリーデータを保存し、その一時テーブルに基づいて顧客ティア分析を実行してください。
  4. 思考問題:CTEがメインクエリから2回参照された場合、2回実行されますか?EXPLAIN で検証してください。

次のレッスン

👉 21-transactions - トランザクション処理:SQLトランザクションのACID特性、BEGIN/COMMIT/ROLLBACK構文、SAVEPOINTセーブポイント、トランザクション分離レベル、デッドロックの概念を学びます。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%