【入門】MySQLのWITH句(共通テーブル式)使い方ガイド – 可読性・再利用性を高める魔法のクエリ
データベースからデータを取得する際、SELECT文は非常に強力なツールです。しかし、複雑な条件でデータを絞り込んだり、複数の集計を組み合わせたり、階層構造を扱ったりする場合、クエリはあっという間に長大になり、読みづらく、理解しにくくなってしまいます。このような悩みを解決してくれる強力な機能の一つが、今回ご紹介するMySQLのWITH句(Common Table Expression – 共通テーブル式、略してCTE)です。
この記事では、データベース操作の初心者の方でも、WITH句の基本的な使い方から、再帰的な処理、そして実践的な活用例までを網羅的に学ぶことができます。約5000語にわたる詳細な解説と豊富なコード例を通して、WITH句を自在に操り、より効率的で理解しやすいクエリを書くためのスキルを身につけましょう。
はじめに:なぜWITH句を学ぶのか?
あなたがもし、以下のような経験や目標を持っているなら、WITH句はきっとあなたの強力な味方になるはずです。
- 長いSELECT文を読むのが億劫だと感じている。
- サブクエリを多用した結果、クエリの構造が複雑になりすぎて困っている。
- 同じ中間結果を何度も計算している気がする。
- 組織図や部品構成表のような階層データをSQLで扱いたい。
- より効率的に、そして「賢く」データベースを扱えるようになりたい。
WITH句は、これらの課題に対するMySQLからの答えの一つです。一時的な名前付きの結果セットを定義することで、クエリを論理的なまとまりに分割し、可読性を劇的に向上させることができます。これは、まるで複雑な計算問題を解く際に、途中の計算結果に名前を付けて整理していくようなものです。
この記事では、まずWITH句が何であるかを理解し、なぜそれを使うべきなのか、そのメリットを明確にします。次に、基本的な構文と使い方を学び、いくつかの簡単な例から始めます。さらに、現実のデータ分析で役立つ応用例を通じて、WITH句の威力を実感していただきます。特に、階層構造データを扱うための「再帰WITH句」については、その強力な機能と具体的な使い方を丁寧に解説します。最後に、WITH句を使う上での注意点や、パフォーマンスに関する考慮事項についても触れ、実践で困らないための知識を習得します。
さあ、WITH句の世界へ足を踏み入れ、あなたのSQLスキルを次のレベルへ引き上げましょう!
1. WITH句(共通テーブル式 – CTE)とは何か?
まずは、WITH句が一体何者なのか、その定義と特徴を理解することから始めましょう。
1.1 定義:単一クエリ内の一時的な名前付き結果セット
WITH句(Common Table Expression – 共通テーブル式)は、単一のSELECT文、またはINSERT、UPDATE、DELETE、REPLACE文の前に定義される一時的な名前付き結果セットです。この「名前付き結果セット」は、メインクエリや、後続に定義される他のWITH句の中から参照することができます。
重要な点は、CTEはあくまで一時的であり、その定義は単一のクエリ内でのみ有効であるということです。クエリが実行されれば、CTEは消滅します。データベースのスキーマの一部として永続的に保存されるわけではありません。
1.2 特徴
- 一時性: 定義されたクエリの実行期間中のみ存在します。
- 名前付き: 結果セットに分かりやすい名前を付けることができます。
- 参照可能範囲: 定義されたWITH句ブロック内、およびその直後のメインクエリから参照できます。複数のCTEを定義した場合、後続のCTEは先行するCTEを参照できます。
- 再帰性: 自身を参照する再帰的なクエリを定義することができます(再帰WITH句)。これは、階層構造データの扱いなどで非常に強力な機能です。
- 非更新性: CTEはあくまで結果セットであり、それ自体を直接更新することはできません。更新が必要な場合は、基になるテーブルに対して操作を行う必要があります。
1.3 ビューとの比較
WITH句と似た機能として、「ビュー(View)」があります。ビューもまた、SELECT文の結果に名前を付けて参照できる機能です。しかし、両者には重要な違いがあります。
特徴 | WITH句(CTE) | ビュー(View) |
---|---|---|
永続性 | 一時的(クエリ実行中のみ有効) | 永続的(データベースに定義が保存される) |
参照範囲 | 定義したクエリ内のみ有効 | 定義後、どのクエリからも参照可能 |
パラメータ | クエリ内で動的に定義可能 | 定義時に固定される(一部例外あり) |
複雑さ | 中間的な計算や複雑な処理に適している | 標準的な集計や結合結果の再利用に適している |
作成/削除 | クエリごとに自動的に作成/破棄される | CREATE VIEW , DROP VIEW で管理する |
再帰 | 再帰クエリを定義できる | 標準的には再帰クエリを定義できない |
簡単に言うと、ビューは複数のクエリから繰り返し参照される、永続的な仮想テーブルであるのに対し、WITH句は単一の複雑なクエリを分解するために、そのクエリ内で一時的に使用される仮想テーブルであると言えます。使い分けとしては、頻繁に使う共通のデータ抽出や集計結果にはビューを、特定の複雑なクエリ内でのみ必要な中間結果にはWITH句を使用するのが一般的です。
1.4 サブクエリとの比較
WITH句のもう一つの比較対象は、サブクエリ(副問い合わせ)です。サブクエリは、別のクエリの中にネストされたクエリで、その結果を親クエリで利用します。
例えば、特定条件を満たす顧客のリストを取得し、そのリストに対してさらに別の処理を行う場合、サブクエリを使用すると以下のようになります。
sql
-- サブクエリを使った例
SELECT
o.order_id,
o.order_date,
o.total_amount
FROM
orders o
WHERE
o.customer_id IN (
SELECT
c.customer_id
FROM
customers c
WHERE
c.registration_date >= '2023-01-01' -- 特定期間に登録した顧客
);
同じ処理をWITH句を使って書くと、以下のようになります。
sql
-- WITH句を使った例
WITH RecentCustomers AS (
SELECT
c.customer_id
FROM
customers c
WHERE
c.registration_date >= '2023-01-01' -- 特定期間に登録した顧客
)
SELECT
o.order_id,
o.order_date,
o.total_amount
FROM
orders o
WHERE
o.customer_id IN (SELECT customer_id FROM RecentCustomers); -- CTEを参照
この簡単な例では、サブクエリでもWITH句でも大きな違いは感じられないかもしれません。しかし、クエリが複雑になるにつれて、WITH句のメリットが際立ってきます。
サブクエリを深くネストしていくと、内側のクエリから外側のクエリ、そしてそのまた外側…というように、クエリの構造をたどるのが難しくなります。また、同じサブクエリの結果を複数回利用したい場合でも、通常は同じサブクエリを繰り返し記述するか、さらに複雑な構造にする必要があります。
一方、WITH句を使えば、各中間結果に意味のある名前(例: RecentCustomers
)を付けることができ、それらを独立したブロックとして定義できます。これにより、クエリ全体の流れが分かりやすくなり、可読性が向上します。複数のCTEを定義すれば、複雑な処理を段階的に分解して記述することも可能です。また、定義したCTEは、その後のクエリ内で複数回参照することができます(MySQL 8.0以降では、同じCTEへの複数回の参照は、Optimizerによって一度だけ実行されるように最適化される可能性が高いです)。
2. なぜWITH句を使うのか?(メリット)
ここまでで、WITH句が何であるか、そしてビューやサブクエリとの違いを見てきました。では、具体的にWITH句を使うことでどのようなメリットが得られるのでしょうか?
2.1 可読性の向上
これがWITH句を使う最大の理由と言えるでしょう。複雑なクエリは、いくつものJOIN
やサブクエリが組み合わさって、非常に読みにくくなります。WITH句を使えば、クエリを意味のある小さな単位(中間結果)に分割し、それぞれに名前を付けて定義できます。
“`sql
— WITH句なしの複雑な例(架空)
SELECT
d.department_name,
AVG(e.salary),
(SELECT COUNT(*) FROM employees WHERE department_id = e.department_id AND hire_date >= ‘2020-01-01’) — サブクエリ1
FROM
employees e
JOIN
departments d ON e.department_id = d.department_id
WHERE
e.status = ‘Active’
AND e.employee_id IN (SELECT employee_id FROM performance_reviews WHERE score > 80) — サブクエリ2
GROUP BY
d.department_name;
— WITH句を使った例
WITH ActiveEmployees AS (
SELECT
employee_id,
department_id,
salary,
hire_date
FROM
employees
WHERE
status = ‘Active’
),
HighPerformingEmployees AS (
SELECT
employee_id
FROM
performance_reviews
WHERE
score > 80
),
ActiveHighPerformingEmployees AS (
SELECT
ae.employee_id,
ae.department_id,
ae.salary,
ae.hire_date
FROM
ActiveEmployees ae
JOIN
HighPerformingEmployees hpe ON ae.employee_id = hpe.employee_id
),
DepartmentStats AS (
SELECT
department_id,
AVG(salary) AS avg_salary,
COUNT(CASE WHEN hire_date >= ‘2020-01-01’ THEN 1 ELSE NULL END) AS new_hires_count — 新規採用者数もCTE内で計算可能
FROM
ActiveHighPerformingEmployees
GROUP BY
department_id
)
SELECT
d.department_name,
ds.avg_salary,
ds.new_hires_count
FROM
DepartmentStats ds
JOIN
departments d ON ds.department_id = d.department_id;
“`
上記の例は少し大げさですが、WITH句を使うことで、まず「アクティブな従業員」を定義し、次に「高パフォーマンスの従業員」を定義し、それらを組み合わせて「アクティブかつ高パフォーマンスな従業員」を定義し、最後にその結果を使って部門別の統計を計算する、というように、処理の流れが順を追って記述されていることが分かります。各CTEが何を表しているのか名前を見れば明らかであり、メインクエリは非常にシンプルになります。
2.2 メンテナンス性の向上
可読性が高いクエリは、当然ながらメンテナンスも容易です。もしクエリの途中の条件を変更する必要が出てきても、WITH句を使っていれば、関連するCTEの定義部分だけを修正すれば済みます。サブクエリが深くネストされている場合、どの部分が何に対応しているのかを把握するだけでも時間がかかり、修正箇所を見つけ出すのが困難になることがあります。
2.3 再利用性(非再帰CTE)
定義した非再帰CTEは、そのWITH句ブロック内および直後のメインクエリから複数回参照できます。例えば、ある中間結果を使って複数の異なる集計や分析を行いたい場合に便利です。
sql
-- 例:最近の売上データを使って、合計売上と平均単価の両方を計算する
WITH RecentSales AS (
SELECT
order_id,
customer_id,
order_date,
total_amount,
(SELECT COUNT(*) FROM order_items WHERE order_id = o.order_id) AS item_count -- サブクエリでアイテム数を取得(例として)
FROM
orders o
WHERE
order_date >= '2024-01-01'
)
-- メインクエリでRecentSalesを複数回参照
SELECT
(SELECT COUNT(*) FROM RecentSales) AS total_order_count, -- 最近の注文数
SUM(total_amount) AS total_revenue, -- 最近の合計売上
AVG(total_amount / item_count) AS avg_item_price -- 平均アイテム単価(アイテム数で割る場合)
FROM
RecentSales; -- ここでもRecentSalesを参照
この例では、RecentSales
というCTEを定義し、それをメインクエリ内で複数回参照しています。これにより、最近の売上データを一度取得するだけで、複数の異なる統計値を効率的に計算できます。
2.4 再帰クエリの実装
これこそがWITH句の最大の特徴の一つです。WITH句のRECURSIVE
キーワードを使用することで、再帰的なクエリ、すなわち自分自身の結果を参照しながら処理を繰り返すクエリを定義できます。これは、組織図、部品構成表、カテゴリツリーのような階層構造データを扱う際に不可欠な機能です。
“`sql
— 例:従業員の報告ラインをたどる(再帰CTE)
WITH RECURSIVE EmployeeHierarchy AS (
— アンカーメンバー:最上位の従業員(社長など、manager_idがNULL)
SELECT
employee_id,
employee_name,
manager_id,
0 AS level — 階層レベル
FROM
employees
WHERE
manager_id IS NULL
UNION ALL
-- 再帰メンバー:アンカーメンバーまたは前回の再帰メンバーの部下
SELECT
e.employee_id,
e.employee_name,
e.manager_id,
eh.level + 1 -- 階層レベルを増やす
FROM
employees e
JOIN
EmployeeHierarchy eh ON e.manager_id = eh.employee_id -- 自身の結果(EmployeeHierarchy)を参照
)
SELECT
*
FROM
EmployeeHierarchy
ORDER BY
level, employee_id;
“`
この再帰CTEの例については、後ほど詳細に解説します。このように、WITH句はサブクエリや通常のJOINでは実現が難しい、自己参照的な処理を可能にします。
2.5 複雑な計算の中間結果
段階的な集計や計算を行う際に、各ステップの結果をCTEとして定義していくことで、最終的な計算ロジックを追いやすくなります。
sql
-- 例:各顧客の初回の注文日と最終の注文日を求め、さらにその期間の合計購入金額を計算する
WITH CustomerFirstLastOrder AS (
SELECT
customer_id,
MIN(order_date) AS first_order_date,
MAX(order_date) AS last_order_date
FROM
orders
GROUP BY
customer_id
),
CustomerTotalAmount AS (
SELECT
customer_id,
SUM(total_amount) AS total_purchased_amount
FROM
orders
GROUP BY
customer_id
)
SELECT
c.customer_id,
cflo.first_order_date,
cflo.last_order_date,
cta.total_purchased_amount
FROM
customers c
JOIN
CustomerFirstLastOrder cflo ON c.customer_id = cflo.customer_id
JOIN
CustomerTotalAmount cta ON c.customer_id = cta.customer_id;
この例では、顧客ごとの初・最終注文日を計算するCTEと、合計購入金額を計算するCTEを別々に定義し、最後にそれらを組み合わせて顧客情報と紐づけています。それぞれのCTEは単一の目的を持っており、全体のクエリが非常に理解しやすくなっています。
これらのメリットを理解すると、なぜ多くのデータベースプロフェッショナルが複雑なクエリを書く際にWITH句を積極的に使用するのかが分かります。それでは次に、実際にWITH句を使ってみましょう。
3. 基本的なWITH句の使い方
WITH句の基本的な構文と、単一または複数のCTEを定義する方法を学びます。
3.1 基本構文
非再帰WITH句の基本構文は以下のようになります。
“`sql
WITH
cte_name_1 AS (
— cte_name_1 を定義する SELECT 文
SELECT column1, column2, …
FROM table_name
WHERE condition
GROUP BY …
HAVING …
ORDER BY … — ORDER BY/LIMITはMySQL 8.0.14以降、トップレベルのSELECT文または再帰CTEで有効になる
LIMIT …
),
cte_name_2 AS (
— cte_name_2 を定義する SELECT 文
— cte_name_1 を参照することも可能
SELECT another_column, …
FROM cte_name_1 — 例:先行するCTEを参照
JOIN another_table ON …
WHERE another_condition
),
… — 複数のCTEをカンマ区切りで記述
— メインクエリ:ここで定義されたCTEを参照して最終的な結果を取得する
SELECT
final_column_1, final_column_2, …
FROM
cte_name_1 — 定義したCTEを参照
JOIN
cte_name_2 ON … — 別のCTEを参照
WHERE
final_condition
ORDER BY …
LIMIT …;
“`
WITH
キーワードで始まります。- その後、
cte_name AS ( SELECT ... )
という形式で各CTEを定義します。cte_name
は、その一時結果セットに付ける名前です。後続のクエリからこの名前で参照します。AS
キーワードが続きます。- 括弧
()
の中に、そのCTEの結果を定義するSELECT
文を記述します。このSELECT
文は、通常のSELECT
文と同じように、FROM
,JOIN
,WHERE
,GROUP BY
,HAVING
などを自由に使うことができます。
- 複数のCTEを定義する場合は、それぞれの定義をカンマ
,
で区切ります。 - 定義したCTEは、その直後に続く単一の
SELECT
,INSERT
,UPDATE
,DELETE
, またはREPLACE
文から参照できます。この参照する文を「メインクエリ」と呼びます。
重要: MySQL 8.0.14より前のバージョンでは、CTE内のSELECT文でORDER BY
やLIMIT
を使用するとエラーになるか無視されることがありました。MySQL 8.0.14以降では、非再帰CTEのSELECT文内でこれらが許可されますが、その効果はCTEの結果セットの構造には影響せず、あくまでCTE内の操作(例: LIMIT
を使ったトップNの取得)に限定される場合があります。最終的な結果セットの順序付けや制限は、メインクエリのORDER BY
やLIMIT
で行うのが一般的です。再帰CTEについては、再帰メンバー内でのORDER BY
やLIMIT
は通常許可されず、アンカーメンバーでのみ意味を持つ場合が多いです。
3.2 単一のCTEの例
最も簡単なケースとして、一つのCTEを定義する例を見てみましょう。
例1:簡単なフィルタリング
products
テーブルから、価格が1000円以上の商品を抽出するCTEを定義し、その結果を表示します。
“`sql
— サンプルデータ用のテーブルとデータを作成 (もしない場合)
— 仮に以下の構造とデータがあるとします:
— CREATE TABLE products (
— product_id INT PRIMARY KEY,
— product_name VARCHAR(100),
— price DECIMAL(10, 2),
— category VARCHAR(50)
— );
— INSERT INTO products (product_id, product_name, price, category) VALUES
— (1, ‘Laptop’, 120000.00, ‘Electronics’),
— (2, ‘Mouse’, 2500.00, ‘Electronics’),
— (3, ‘Keyboard’, 8000.00, ‘Electronics’),
— (4, ‘Desk’, 15000.00, ‘Furniture’),
— (5, ‘Chair’, 7000.00, ‘Furniture’),
— (6, ‘Monitor’, 25000.00, ‘Electronics’),
— (7, ‘Notebook’, 500.00, ‘Stationery’),
— (8, ‘Pen’, 100.00, ‘Stationery’);
— CTEを使って価格が1000円以上の商品を抽出
WITH HighPriceProducts AS (
SELECT
product_id,
product_name,
price,
category
FROM
products
WHERE
price >= 1000.00
)
— メインクエリでCTEの結果を表示
SELECT
product_name,
price
FROM
HighPriceProducts
WHERE
category = ‘Electronics’; — さらにフィルタリングを追加
“`
解説:
WITH HighPriceProducts AS (...)
で、HighPriceProducts
という名前のCTEを定義しています。- このCTEの定義の中では、
products
テーブルから価格が1000円以上の商品を抽出しています。 - その後のメインクエリ
SELECT product_name, price FROM HighPriceProducts WHERE category = 'Electronics';
では、定義したHighPriceProducts
という名前の結果セットをテーブルのように扱ってデータを取得しています。 - この例では、元のテーブルから価格でフィルタリングした結果を一度CTEとして定義し、そのCTEに対してさらにカテゴリでフィルタリングを行っています。WITH句を使わない場合、価格とカテゴリ両方の条件を直接
products
テーブルに適用するだけかもしれませんが、CTEを使うことで、まず「高価格な商品リスト」という中間結果を作成し、それから「その中で電化製品はどれか」というように、思考プロセスに沿ったクエリの書き方ができます。
例2:簡単な集計
orders
テーブルから、各顧客の合計注文金額を計算するCTEを定義し、その結果を表示します。
“`sql
— サンプルデータ用のテーブルとデータを作成 (もしない場合)
— 仮に以下の構造とデータがあるとします:
— CREATE TABLE orders (
— order_id INT PRIMARY KEY,
— customer_id INT,
— order_date DATE,
— total_amount DECIMAL(10, 2)
— );
— INSERT INTO orders (order_id, customer_id, order_date, total_amount) VALUES
— (101, 1, ‘2024-01-10’, 5500.00),
— (102, 2, ‘2024-01-11’, 12000.00),
— (103, 1, ‘2024-01-15’, 8000.00),
— (104, 3, ‘2024-01-18’, 3000.00),
— (105, 2, ‘2024-01-20’, 7500.00),
— (106, 1, ‘2024-01-22’, 4000.00);
— CTEを使って顧客ごとの合計注文金額を計算
WITH CustomerTotalOrders AS (
SELECT
customer_id,
SUM(total_amount) AS total_purchased
FROM
orders
GROUP BY
customer_id
)
— メインクエリでCTEの結果を表示し、合計金額の高い順に並べる
SELECT
customer_id,
total_purchased
FROM
CustomerTotalOrders
ORDER BY
total_purchased DESC;
“`
解説:
WITH CustomerTotalOrders AS (...)
で、CustomerTotalOrders
というCTEを定義しています。- このCTEの定義では、
orders
テーブルをcustomer_id
でグループ化し、それぞれのtotal_amount
を合計しています。 - メインクエリでは、この
CustomerTotalOrders
の結果セットからデータを取得し、total_purchased
の降順で並べ替えています。
このように、CTEは集計処理の結果を名前付きの結果セットとして扱う場合にも非常に便利です。まず集計を行い、その集計結果に対してさらに条件を付けたり、並べ替えたり、他のテーブルと結合したりといった次のステップの処理を明確に記述できます。
3.3 複数のCTEの例
一つのクエリで複数のCTEを定義することもよくあります。複数のCTEはカンマ区切りで記述します。重要なのは、後続のCTEは先行するCTEを参照できるという点です。
例3:複数のステップでデータを準備
products
テーブルからカテゴリ別の平均価格を計算するCTEと、特定のカテゴリ(例: ‘Electronics’)の商品数を計算する別のCTEを定義し、最後に両方の結果を組み合わせて表示します。
“`sql
— 例1で使用したproductsテーブルとデータをそのまま使用します
— 複数のCTEを定義
WITH CategoryAveragePrice AS (
SELECT
category,
AVG(price) AS avg_price
FROM
products
GROUP BY
category
),
ElectronicsProductCount AS (
SELECT
COUNT(*) AS electronics_count
FROM
products
WHERE
category = ‘Electronics’
)
— メインクエリで両方のCTEの結果を組み合わせる
— この例では、結合するキーがないため、少し不自然ですが例として示します
— 実際には、結合可能なキーを持つ別のCTEと結合するか、
— それぞれ独立した集計結果として表示することが多いです。
SELECT
cap.category,
cap.avg_price,
epc.electronics_count — ElectronicsProductCountの結果は1行なので、そのまま参照できる
FROM
CategoryAveragePrice cap
CROSS JOIN — 結合キーがないのでCROSS JOIN(この例では結果セットが1行なので実質全行に同じ値が付く)
ElectronicsProductCount epc;
— より一般的なユースケース:異なるCTEの結果を別の目的で使用する例
WITH HighPriceProducts AS (
SELECT product_id, product_name, price, category
FROM products
WHERE price >= 10000.00
),
CategorySales AS (
SELECT p.category, SUM(o.total_amount) AS category_total_sales
FROM orders o
JOIN products p ON o.product_id = p.product_id — ordersテーブルにproduct_idがあるとして
GROUP BY p.category
)
SELECT
hpp.product_name,
hpp.price,
cs.category_total_sales — 該当カテゴリの合計売上を結合して表示
FROM
HighPriceProducts hpp
JOIN
CategorySales cs ON hpp.category = cs.category; — categoryで結合
“`
解説:
- 最初の例では、
CategoryAveragePrice
とElectronicsProductCount
という2つのCTEをカンマで区切って定義しています。 CategoryAveragePrice
はカテゴリごとの平均価格を計算しています。ElectronicsProductCount
は’Electronics’カテゴリの商品数を計算しています。- メインクエリでは、これら2つのCTEを参照しています。この例のように、異なる集計結果を組み合わせて表示する場合、結合キーがない場合は
CROSS JOIN
などを使うことになります(ただし、この特定の例の構造は少し人工的です)。
より現実的な例として、2つ目のクエリでは、高価格帯商品リストとカテゴリ別売上合計という、関連性はあるが計算ロジックが異なる2つのCTEを定義し、それらをカテゴリで結合して表示しています。このように、複数のCTEを定義することで、複雑なデータ準備や異なる角度からの集計を段階的に行い、最終的にそれらを組み合わせて目的のデータセットを作成することができます。
4. 実践的なWITH句の活用例
基本的な使い方が分かったところで、より実践的なシナリオでWITH句がどのように役立つかを見ていきましょう。
4.1 複雑なクエリの分割:段階的な集計
複数のステップで集計やフィルタリングを行う必要がある場合、WITH句はクエリ構造を整理するのに役立ちます。
例4:特定期間の売上を顧客ランク別に集計
orders
テーブルとcustomers
テーブルがあるとします。2024年1月中の注文を対象に、顧客の「ランク」(例: 購入金額に応じて決定)ごとに合計売上を集計したいとします。顧客ランクは、全期間の合計購入金額で決定するとします。
“`sql
— サンプルデータ追加 (customersテーブルにcustomer_name, customer_rankカラムがあるとして)
— INSERT INTO customers (customer_id, customer_name, registration_date, customer_rank) VALUES
— (1, ‘Alice’, ‘2023-05-01’, ‘Gold’),
— (2, ‘Bob’, ‘2023-06-15’, ‘Silver’),
— (3, ‘Charlie’, ‘2023-07-20’, ‘Bronze’);
— ordersテーブルは例2のものを使用
WITH OrdersInJan2024 AS (
— ステップ1: 2024年1月中の注文を抽出
SELECT
order_id,
customer_id,
total_amount
FROM
orders
WHERE
order_date >= ‘2024-01-01’ AND order_date < ‘2024-02-01’
),
CustomerTotalOrderAmountInJan AS (
— ステップ2: 1月中の注文について、顧客ごとの合計金額を計算
SELECT
customer_id,
SUM(total_amount) AS monthly_total
FROM
OrdersInJan2024 — 先行するCTEを参照
GROUP BY
customer_id
)
— ステップ3: 顧客ランクと紐づけて、ランク別に集計
SELECT
c.customer_rank,
SUM(ctoa.monthly_total) AS total_revenue_in_jan,
COUNT(DISTINCT ctoa.customer_id) AS distinct_customers_in_jan
FROM
CustomerTotalOrderAmountInJan ctoa
JOIN
customers c ON ctoa.customer_id = c.customer_id — 顧客ランク情報はcustomersテーブルにあるとする
GROUP BY
c.customer_rank
ORDER BY
total_revenue_in_jan DESC;
“`
解説:
OrdersInJan2024
CTEで、まず対象となる注文(2024年1月中の注文)を絞り込みます。CustomerTotalOrderAmountInJan
CTEで、1の結果を使って顧客ごとの合計注文金額を集計します。ここで、先行するOrdersInJan2024
を参照しています。- メインクエリで、2の結果と
customers
テーブルをcustomer_id
で結合し、顧客ランクごとにさらに集計を行っています。
このように、WITH句を使うことで、「まず対象データを絞る」「次に顧客ごとに合計する」「最後に顧客ランクでグループ化して合計する」という処理の流れが、CTEの定義順に沿って非常に分かりやすく記述されています。もしこれをサブクエリで実現しようとすると、おそらく3重以上のネストが必要になり、クエリの可読性は大きく損なわれるでしょう。
4.2 中間結果のフィルタリング/加工
CTEで取得した中間結果に対して、さらにフィルタリングや加工を加えたい場合にもWITH句は有効です。
例5:各カテゴリで売上上位3商品とその売上合計
orders
テーブルとproducts
テーブルがあるとします。各カテゴリにおいて、最も売れている上位3商品の合計売上金額を知りたいとします。
これは、ウィンドウ関数(特にROW_NUMBER()
やRANK()
)とWITH句を組み合わせる典型的な例です。
“`sql
— ordersテーブルにproduct_idカラムがあるとし、ordersテーブルのデータを追加
— INSERT INTO orders (order_id, customer_id, order_date, total_amount, product_id) VALUES
— (101, 1, ‘2024-01-10’, 5500.00, 1), — Laptop
— (102, 2, ‘2024-01-11’, 12000.00, 4), — Desk
— (103, 1, ‘2024-01-15’, 8000.00, 3), — Keyboard
— (104, 3, ‘2024-01-18’, 3000.00, 5), — Chair
— (105, 2, ‘2024-01-20’, 7500.00, 2), — Mouse
— (106, 1, ‘2024-01-22’, 4000.00, 1); — Laptop
— (107, 3, ‘2024-01-25’, 15000.00, 6); — Monitor
— (108, 2, ‘2024-01-26’, 2500.00, 1); — Laptop
WITH ProductSales AS (
— ステップ1: 商品ごとの合計売上を計算
SELECT
p.product_id,
p.product_name,
p.category,
SUM(o.total_amount) AS total_sold
FROM
orders o
JOIN
products p ON o.product_id = p.product_id
GROUP BY
p.product_id, p.product_name, p.category
),
RankedProductSales AS (
— ステップ2: 各カテゴリ内で売上順にランク付け
SELECT
product_id,
product_name,
category,
total_sold,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY total_sold DESC) as rn — カテゴリ別に、売上高で順位付け
FROM
ProductSales — 先行するCTEを参照
)
— ステップ3: ランクが3位以内の商品を抽出し、カテゴリ別に合計売上を再集計
SELECT
category,
SUM(total_sold) AS top3_total_sales — 上位3商品の合計売上
FROM
RankedProductSales — 先行するCTEを参照
WHERE
rn <= 3 — ランクが3位以内
GROUP BY
category
ORDER BY
category;
“`
解説:
ProductSales
CTEで、まずorders
テーブルとproducts
テーブルを結合し、商品ごとの合計売上を計算します。RankedProductSales
CTEで、1の結果を利用して、ウィンドウ関数ROW_NUMBER()
を使って各カテゴリ内での売上順位を計算します。PARTITION BY category
でカテゴリごとに独立した順位付けを行い、ORDER BY total_sold DESC
で売上の降順に並べています。- メインクエリで、2の結果から
rn <= 3
の条件で上位3商品を抽出し、その結果をカテゴリ別に集計して合計売上を計算しています。
このように、WITH句を使うことで、「商品別売上計算」→「カテゴリ内順位付け」→「上位抽出と合計計算」という複数の処理ステップを、それぞれ別のCTEとして定義し、明確な流れで記述できます。ウィンドウ関数の結果をその後のクエリでフィルタリング・集計したい場合、このようにCTEを介して行うのは非常に一般的なパターンです。
4.3 複数のCTEでのデータ準備と結合
異なるソースからのデータや、異なるロジックで計算された中間結果を別々のCTEとして準備し、最後にメインクエリで結合して目的のデータセットを作成するパターンです。
例6:顧客情報、注文情報、問い合わせ情報を組み合わせて分析
customers
テーブル、orders
テーブル、support_tickets
テーブルがあるとします。各顧客について、総購入金額とオープン中の問い合わせ件数を同時に表示したいとします。
“`sql
— サンプルデータ追加 (support_ticketsテーブルを作成しデータ追加)
— CREATE TABLE support_tickets (
— ticket_id INT PRIMARY KEY,
— customer_id INT,
— status VARCHAR(50), — 例: ‘Open’, ‘Closed’, ‘Pending’
— created_date DATE
— );
— INSERT INTO support_tickets (ticket_id, customer_id, status, created_date) VALUES
— (201, 1, ‘Closed’, ‘2024-01-05’),
— (202, 1, ‘Open’, ‘2024-01-25’),
— (203, 2, ‘Open’, ‘2024-01-26’),
— (204, 3, ‘Closed’, ‘2024-01-28’);
WITH CustomerTotalOrders AS (
— ステップ1: 顧客ごとの合計注文金額を計算
SELECT
customer_id,
SUM(total_amount) AS total_purchased
FROM
orders
GROUP BY
customer_id
),
CustomerOpenTickets AS (
— ステップ2: 顧客ごとのオープン中の問い合わせ件数を計算
SELECT
customer_id,
COUNT(*) AS open_ticket_count
FROM
support_tickets
WHERE
status = ‘Open’
GROUP BY
customer_id
)
— ステップ3: 顧客情報、合計注文金額、オープン問い合わせ件数を結合
SELECT
c.customer_id,
c.customer_name,
COALESCE(cto.total_purchased, 0) AS total_purchased, — 注文がない顧客は0を表示
COALESCE(cot.open_ticket_count, 0) AS open_ticket_count — 問い合わせがない顧客は0を表示
FROM
customers c
LEFT JOIN
CustomerTotalOrders cto ON c.customer_id = cto.customer_id — 全顧客を表示するためLEFT JOIN
LEFT JOIN
CustomerOpenTickets cot ON c.customer_id = cot.customer_id; — 全顧客を表示するためLEFT JOIN
“`
解説:
CustomerTotalOrders
CTEで、orders
テーブルから顧客ごとの合計注文金額を計算します。CustomerOpenTickets
CTEで、support_tickets
テーブルからオープン中の問い合わせを抽出し、顧客ごとの件数を計算します。- メインクエリで、
customers
テーブルを基に、1と2のCTEをそれぞれcustomer_id
でLEFT JOIN
しています。LEFT JOIN
を使うことで、注文や問い合わせがまったくない顧客も結果に含まれるようにしています。COALESCE
関数は、LEFT JOIN
によって発生しうるNULL値を0に変換するために使用しています。
この例のように、性質の異なる複数の集計や抽出結果を、それぞれ独立したCTEとして定義し、最後にメインクエリで結合することで、異なる角度からの情報を統合した分析を行うことができます。各CTEの定義はシンプルに保たれるため、クエリ全体の理解が容易になります。
5. 再帰WITH句(Recursive CTE)
さて、WITH句の最も強力で特徴的な機能の一つである「再帰WITH句」について詳しく見ていきましょう。再帰WITH句は、自身の結果セットを参照しながら繰り返し処理を行うことで、階層構造データの展開や、連番生成など、通常のクエリでは難しい処理を実現します。
5.1 再帰CTEとは?
再帰CTEは、以下のような特性を持ちます。
RECURSIVE
キーワード:WITH
キーワードの直後にRECURSIVE
を付けます。- アンカーメンバー (Anchor Member): 再帰処理の開始点となるSELECT文です。これは通常のSELECT文であり、自身を参照しません。再帰処理の「最初の一歩」を定義します。
- 再帰メンバー (Recursive Member): アンカーメンバーの結果、または前回の再帰メンバーの結果を参照するSELECT文です。このメンバーが繰り返し実行され、結果セットが積み重ねられていきます。
UNION
またはUNION ALL
: アンカーメンバーと再帰メンバーの結果を結合するために使用します。通常は重複を除外しないUNION ALL
が使われます。- 終了条件: 再帰メンバーのJOIN条件やWHERE句に、再帰がいつ終了するかを制御する条件を含める必要があります。終了条件がないと、無限ループに陥る可能性があります。
再帰CTEは、アンカーメンバーを実行して最初の結果セットを作成し、次にその結果セットを基に再帰メンバーを実行します。得られた新しい結果セットに対して、再び再帰メンバーを実行するというサイクルを、再帰メンバーが結果を返さなくなるまで繰り返します。
5.2 再帰CTEの構文
再帰WITH句の基本構文は以下のようになります。
“`sql
WITH RECURSIVE cte_name AS (
— アンカーメンバー (再帰の開始点)
SELECT …
FROM base_table
WHERE initial_condition
UNION [ALL] -- 通常は UNION ALL を使用
-- 再帰メンバー (自身を参照し、繰り返し実行される)
SELECT ...
FROM base_table
JOIN cte_name ON join_condition -- ここで自身(cte_name)を参照
WHERE termination_condition -- 再帰を終了させる条件
)
— メインクエリ:最終的な再帰CTEの結果セットを参照
SELECT …
FROM cte_name
WHERE final_condition;
“`
5.3 再帰CTEの活用例:階層構造データの展開
最も一般的で強力な再帰CTEの活用例は、階層構造データのクエリです。
例7:従業員の報告ライン(上司・部下関係)をたどる
従業員テーブルemployees
があり、各従業員が自分の上司を示すmanager_id
を持つとします。特定の従業員から開始して、その部下を階層的に全て取得したい場合に使います。
“`sql
— サンプルデータ用のemployeesテーブルを作成・データ追加
— CREATE TABLE employees (
— employee_id INT PRIMARY KEY,
— employee_name VARCHAR(100),
— manager_id INT NULL, — NULLは最上位(マネージャーがいない)
— FOREIGN KEY (manager_id) REFERENCES employees(employee_id) — 自己参照外部キー(オプション)
— );
— INSERT INTO employees (employee_id, employee_name, manager_id) VALUES
— (1, ‘Boss (CEO)’, NULL),
— (2, ‘Alice (Manager A)’, 1),
— (3, ‘Bob (Manager B)’, 1),
— (4, ‘Charlie (Lead A1)’, 2),
— (5, ‘David (Dev A1-1)’, 4),
— (6, ‘Eve (Dev A1-2)’, 4),
— (7, ‘Frank (Lead B1)’, 3),
— (8, ‘Grace (Dev B1-1)’, 7);
— 再帰CTEを使って、ID=2 (Alice) の部下ツリーを全て取得
WITH RECURSIVE EmployeeHierarchy AS (
— アンカーメンバー: 再帰の開始点(ID=2の従業員)
SELECT
employee_id,
employee_name,
manager_id,
0 AS level — 階層レベル(開始点をレベル0とする)
FROM
employees
WHERE
employee_id = 2 — 特定の従業員から開始
UNION ALL -- アンカーと再帰の結果を結合
-- 再帰メンバー: 前回の結果セット(EmployeeHierarchy)の従業員を上司とする従業員(部下)を取得
SELECT
e.employee_id,
e.employee_name,
e.manager_id,
eh.level + 1 -- レベルを1増やす
FROM
employees e
JOIN
EmployeeHierarchy eh ON e.manager_id = eh.employee_id -- 自身の結果セット(eh)を参照
)
— メインクエリ: 再帰CTEの結果を表示
SELECT
— レベルをインデントとして表示するなどの加工が可能
CONCAT(REPEAT(‘ ‘, level), employee_name) AS indented_name,
employee_id,
manager_id,
level
FROM
EmployeeHierarchy
ORDER BY
level, employee_id;
“`
解説:
WITH RECURSIVE EmployeeHierarchy AS (...)
で、RECURSIVE
キーワードを付けて再帰CTEEmployeeHierarchy
を定義します。- アンカーメンバー:
SELECT ... FROM employees WHERE employee_id = 2
の部分です。ここで、再帰の起点となる従業員(ID=2のAlice)を選択しています。level
というカラムを追加し、開始点なので0を設定しています。 UNION ALL
: アンカーメンバーの結果と再帰メンバーの結果を結合します。- 再帰メンバー:
SELECT ... FROM employees e JOIN EmployeeHierarchy eh ON e.manager_id = eh.employee_id
の部分です。FROM employees e
:employees
テーブルから従業員情報を取得します。JOIN EmployeeHierarchy eh ON e.manager_id = eh.employee_id
: ここで重要なのは、自身の結果セットであるEmployeeHierarchy
を参照していることです。このJOIN
条件は、「employees
テーブルの従業員のマネージャーIDが、前回のEmployeeHierarchy
の結果セットに含まれる従業員のIDと一致する」という条件です。つまり、「前回のステップで取得された従業員(上司)の部下を取得する」という処理を行っています。eh.level + 1
: 前回のステップで取得した従業員のレベルに1を足すことで、現在の従業員の階層レベルを計算しています。
- 再帰の進行:
- ステップ1: アンカーメンバーが実行され、
EmployeeHierarchy
にAlice(ID=2, level=0)が追加されます。 - ステップ2: 再帰メンバーが実行されます。
EmployeeHierarchy
にはAlice(ID=2)がいるので、e.manager_id = 2
を満たす従業員(Charlie, ID=4)が検索され、EmployeeHierarchy
にCharlie(ID=4, level=1)が追加されます。 - ステップ3: 再び再帰メンバーが実行されます。
EmployeeHierarchy
にはAlice(ID=2)とCharlie(ID=4)がいるので、e.manager_id = 2
またはe.manager_id = 4
を満たす従業員が検索されます。e.manager_id = 4
を満たす従業員(David, ID=5; Eve, ID=6)が見つかり、EmployeeHierarchy
に追加されます(level=2)。 - ステップ4: 再び再帰メンバーが実行されます。
EmployeeHierarchy
にはAlice, Charlie, David, Eveがいますが、これらの従業員を上司とする従業員(つまりmanager_id
が2, 4, 5, 6のいずれかに一致する従業員)はもういません。再帰メンバーが何も結果を返さなくなるため、再帰は終了します。
- ステップ1: アンカーメンバーが実行され、
- メインクエリ: 最後に、完成した
EmployeeHierarchy
という結果セット全体を表示しています。CONCAT(REPEAT(' ', level), employee_name)
は、レベルに応じて名前の前にスペースを追加し、階層構造を見やすくするための加工です。
この例では、特定の従業員から「下方向」(部下)へ階層をたどりましたが、アンカーメンバーと再帰メンバーのJOIN
条件を変えれば、「上方向」(上司チェーン)や、兄弟要素など、様々な階層構造をたどることができます。
例8:指定範囲の連番生成
再帰CTEは、テーブルに依存しない連番や日付系列などを生成するためにも使われます。
“`sql
— 再帰CTEを使って1から10までの連番を生成
WITH RECURSIVE Numbers AS (
— アンカーメンバー: 開始点 (1)
SELECT 1 AS n
UNION ALL -- 結果を結合
-- 再帰メンバー: 前回の結果に1を足す
SELECT n + 1
FROM Numbers -- 自身の結果セットを参照
WHERE n < 10 -- 終了条件: nが10未満の間繰り返す
)
— メインクエリ: 生成された連番を表示
SELECT n FROM Numbers;
“`
解説:
WITH RECURSIVE Numbers AS (...)
で再帰CTENumbers
を定義します。- アンカーメンバー:
SELECT 1 AS n
で、最初の数値として1を生成します。 UNION ALL
: 結果を結合します。- 再帰メンバー:
SELECT n + 1 FROM Numbers WHERE n < 10
で、前回のステップで得られた数値n
に1を足した新しい数値を生成します。 - 終了条件:
WHERE n < 10
が終了条件です。再帰メンバーは、Numbers
の結果セットにあるn
が10未満である限り実行されます。n
が10になった次のステップでは、WHERE 10 < 10
が偽となり、再帰メンバーは結果を返さなくなるため、再帰が終了します。 - メインクエリ: 生成された1から10までの連番を表示します。
このように、再帰CTEはテーブルデータに依存しないシンプルな繰り返し処理にも応用できます。
6. WITH句使用上の注意点とパフォーマンス
WITH句は非常に便利ですが、使用する上でいくつか注意すべき点や、パフォーマンスに関する考慮事項があります。
6.1 パフォーマンスへの影響(特にMySQL 8.0以降)
MySQL 8.0より前のバージョンでは、CTEは単なるマクロのように展開されることが多く、最適化の面で課題がありました。しかし、MySQL 8.0以降では、OptimizerがCTEを「マテリアライズ化(Materialization)」するかどうかを判断するようになりました。
- マテリアライズ化される場合: CTEの結果セットが一時テーブルとしてメモリまたはディスクに格納されます。同じCTEが複数回参照される場合、結果を再計算するのではなく、一時テーブルから読み込むため、パフォーマンスが向上する可能性があります。また、Optimizerはマテリアライズ化された一時テーブルに適切なインデックスを付与することで、メインクエリからのアクセスを高速化できる場合があります。
- マテリアライズ化されない場合: CTEは展開され、その定義がメインクエリの一部として直接評価されます。これは古いバージョンや、Optimizerがマテリアライズ化が有利ではないと判断した場合に起こります。同じCTEが複数回参照される場合、その都度計算が繰り返される可能性があるため注意が必要です。
パフォーマンスに関する考慮事項:
- MySQLのバージョンを確認する: 8.0以降を使用しているか確認しましょう。特に古いバージョンでは、CTEのパフォーマンスが期待通りにならない可能性があります。
EXPLAIN
で実行計画を確認する: 複雑なWITH句を含むクエリでは、必ずEXPLAIN
を使ってOptimizerがどのようにクエリを実行するか(特にCTEがマテリアライズ化されているか、一時テーブルが使われているかなど)を確認しましょう。- インデックス: CTE内のSELECT文が参照する基になるテーブルのカラムには、適切なインデックスが定義されているか確認しましょう。CTEの結果セット自体にインデックスを直接張ることはできませんが、Optimizerがマテリアライズ化された一時テーブルにインデックスを張ることで性能が改善する場合があります。
- 不必要なデータのフィルタリング: CTEの最初のステップで、できるだけ不要なデータをフィルタリングして中間結果のサイズを小さくすると、その後の処理が効率化される可能性が高いです。
- 再帰CTEの終了条件: 再帰CTEでは、終了条件が正しく設定されていないと無限ループに陥るリスクがあります。また、再帰の深さが非常に深くなる場合、パフォーマンス問題やスタックオーバーフローが発生する可能性があります。
@@cte_max_depth
システム変数で再帰の最大深度を制御できます(デフォルトは1000)。
総じて、MySQL 8.0以降ではCTEのパフォーマンスは大きく改善されていますが、万能ではありません。複雑なケースや大規模データでは、EXPLAIN
による確認と適切なチューニングが重要です。
6.2 スコープ
CTEは、それを定義したWITH
句の直後に続く単一のSELECT
, INSERT
, UPDATE
, DELETE
, REPLACE
文でのみ有効です。別のクエリや、同じ接続内の後続のクエリからそのCTEを参照することはできません。
“`sql
WITH MyCTE AS (
SELECT …
)
SELECT * FROM MyCTE; — OK
— このクエリからはMyCTEを参照できません
SELECT * FROM MyCTE; — ERROR! Unknown table ‘MyCTE’
“`
この一時性がWITH句の特徴であり、ビューとの大きな違いです。
6.3 更新可能性
CTE自体は、基になるテーブルに対する「ビュー」のようなものです。したがって、CTEに対して直接INSERT
, UPDATE
, DELETE
操作を行うことはできません(ただし、特定の条件下で更新可能なビューと同様に、単一のテーブルから単純な形で定義された非再帰CTEに対しては、MySQL 8.0以降で更新操作が可能な場合があります。しかし、一般的にはCTEは更新のために使うのではなく、参照用と考えるべきです)。
データを更新したい場合は、WITH句で更新対象を特定する中間結果を作成し、その結果を使って基になるテーブルに対して更新操作を行うことになります。
例9:WITH句を使って更新対象を特定し、DELETEする
価格が100円未満の商品を削除したいとします。
sql
-- 削除する対象をCTEで特定
WITH LowPriceProducts AS (
SELECT product_id
FROM products
WHERE price < 100.00
)
-- DELETE文でCTEの結果を利用して削除
DELETE FROM products
WHERE product_id IN (SELECT product_id FROM LowPriceProducts);
この例では、LowPriceProducts
というCTEで削除対象のproduct_id
リストを生成し、そのリストをDELETE
文のIN
句で利用しています。このように、WITH句は更新処理の対象を絞り込むための中間ステップとして使うことができます。
6.4 再帰メンバー内での制限
再帰CTEの再帰メンバー(UNION [ALL]
より下の部分)では、以下の操作が制限または禁止される場合があります。
GROUP BY
HAVING
- 集計関数 (
SUM
,AVG
,COUNT
,MIN
,MAX
など) - ウィンドウ関数
ORDER BY
(アンカーメンバーまたはメインクエリでのみ有効)LIMIT
(アンカーメンバーまたはメインクエリでのみ有効)DISTINCT
(UNION ALL
ではなくUNION
を使うことで、再帰結果の重複行を自動的に排除できますが、これ自体が再帰メンバーの制限というよりは結合方法の選択です)
これらの制限は、再帰処理の性質上、各ステップで集計などを行ってしまうと、次のステップへの入力データが正しく構築できなくなるためです。再帰メンバーは「次にたどるノード」を特定するために使われ、集計や順序付けといった処理は、再帰CTEの結果セット全体に対して、メインクエリや別の非再帰CTEで行うのが一般的です。
7. 発展的なトピック
最後に、WITH句に関連する、より発展的なトピックに簡潔に触れておきます。
7.1 ビュー、一時テーブル、CTEの使い分け
機能 | 永続性 | 参照範囲 | 再帰 | 更新 | 用途 |
---|---|---|---|---|---|
ビュー | 永続 | 全クエリから参照可能 | × | △ (条件による) | 標準的な集計/結合結果の再利用、権限管理 |
一時テーブル | 一時 | セッション内 | × | ○ | 大規模な中間結果の保存、複雑なデータ操作 |
CTE | 一時 | 定義したクエリ内 | ○ | △ (条件による) | 複雑なクエリの分解、再帰処理、可読性向上 |
- ビュー: 複数のクエリやユーザーが繰り返し参照する、比較的固定的な中間結果や、複雑な基盤テーブル構造を隠蔽したい場合に適しています。セキュリティのために特定カラムだけを見せたい場合にも使われます。
- 一時テーブル: 中間結果が非常に大きく、メモリに収まらない場合や、中間結果に対して複数回の異なる操作(インデックス作成、更新、結合など)を行いたい場合に適しています。ただし、作成・破棄の管理が必要です。
- CTE: 特定の複雑なクエリ内でのみ必要な中間結果を定義し、クエリの可読性やメンテナンス性を向上させたい場合に最適です。再帰処理が可能なのはCTEだけです。中間結果が比較的小規模な場合や、一時テーブルを作成するほどではない場合に特に有効です。
これらの機能を組み合わせることも可能です。例えば、ビューの結果をWITH句で参照したり、WITH句で生成した結果を一時テーブルに挿入したりといった使い方があります。
7.2 MySQL OptimizerによるCTEの扱い
MySQL 8.0以降のOptimizerは、CTEの実行計画を決定する際に、前述のマテリアライズ化を含め、様々な最適化を試みます。EXPLAIN
出力で、CTEがどのように扱われているか(例: table: <cte_name>
, select_type: DERIVED
やselect_type: SUBQUERY
またはselect_type: UNION
など)を確認することで、Optimizerの判断を理解し、必要であればクエリの構造やインデックスを調整してパフォーマンスを改善することができます。特に再帰CTEは慎重な最適化が必要となるため、実行計画の確認は不可欠です。
8. まとめ
この記事では、MySQLのWITH句(共通テーブル式 – CTE)について、その定義、基本的な使い方、そして様々な応用例を詳細に解説しました。
WITH句を使う主なメリットは以下の通りです。
- 可読性の向上: 複雑なクエリを論理的なステップに分割し、理解しやすくなります。
- メンテナンス性の向上: クエリの修正が容易になります。
- 再利用性: 定義した中間結果をクエリ内で複数回参照できます(特にMySQL 8.0以降でのマテリアライズ化により効率的)。
- 再帰クエリの実装: 階層構造データや連番生成など、自己参照的な処理を可能にします。
- 中間結果の明確化: 複雑な計算や集計の各ステップを独立して定義できます。
また、単一のCTEだけでなく、複数のCTEを組み合わせて複雑なデータ処理を行う方法や、再帰CTEを使った階層データの扱いについても具体的な例を通じて学びました。
WITH句は非常に強力なツールですが、特にパフォーマンスに関してはMySQLのバージョンやクエリの構造によって挙動が異なる可能性があるため、EXPLAIN
を使った実行計画の確認が重要であること、そして再帰CTEには終了条件の設定が不可欠であることなど、いくつかの注意点も押さえておく必要があります。
データベース操作において、WITH句は複雑な問題をシンプルに分解し、より効率的で理解しやすいクエリを書くための強力な武器となります。最初は少し難しく感じるかもしれませんが、簡単な例から始めて、徐々に複雑なクエリに挑戦していくことで、その真価を実感できるはずです。
ぜひ、ご自身のデータベース環境でこの記事のコード例を試してみてください。そして、日々の業務や学習の中で、どのようにWITH句が活用できるかを考えてみましょう。WITH句を使いこなすことで、あなたのSQLスキルは間違いなく向上し、より高度なデータ分析や操作が可能になるでしょう。
これで、MySQLのWITH句に関する詳細な入門ガイドは終わりです。最後までお読みいただき、ありがとうございました。この知識が、あなたのデータベースジャーニーの一助となれば幸いです。