サブクエリとは、別のクエリの中に入れ子になったクエリです。CTE(共通テーブル式)は、同じ内側のクエリを WITH で文の先頭に移動して名前を付けたもので、長いクエリを一連のステップとして読めるようになります。例は PostgreSQL、MySQL、SQLite、SQL Server で動作します。
CTE とサブクエリの使い方
サブクエリは使う場所にそのまま書きます。FROM ではテーブルとして働き、WHERE や SELECT では値やリストを返します。CTE は先に定義し、その後に続くクエリからテーブルのように使います:
-- Subquery: written where it's used
SELECT columns FROM (SELECT ...) AS name;
-- CTE: named first, then used like a table
WITH name AS (SELECT ...)
SELECT columns FROM name;
SELECT n * 2 AS doubled FROM (SELECT 21 AS n) AS numbers;
-- Output: 42
WITH numbers AS (SELECT 21 AS n)
SELECT n * 2 AS doubled FROM numbers;
-- Output: 42
実際のテーブルでは、どちらの形式でも同じ行が返ります。同じフィルターを両方の書き方で示します:
-- Input: people
-- first_name | last_name | job | favorite_food
-- -----------+-----------+----------+--------------
-- Matthew | Ortiz | Chef | cheese
-- Matt | Novak | Pilot | pizza
-- Mattie | Brooks | Engineer | cheese
-- Priya | Nair | Teacher | cheese
-- Subquery
SELECT *
FROM (SELECT first_name, last_name, job FROM people WHERE favorite_food = 'cheese') AS cheese_lovers
WHERE first_name LIKE 'Matt%';
-- CTE
WITH cheese_lovers AS (
SELECT first_name, last_name, job FROM people WHERE favorite_food = 'cheese'
)
SELECT * FROM cheese_lovers
WHERE first_name LIKE 'Matt%';
-- Output of both:
-- first_name | last_name | job
-- -----------+-----------+---------
-- Matthew | Ortiz | Chef
-- Mattie | Brooks | Engineer
知っておくべきルール
-
CTE は 1 つの文の中でしか有効ではありません。
WITHの直後のクエリでは使えますが、次の文では使えません。複数のクエリで結果を保持したい場合は、一時テーブルかビューを使ってください。 -
FROM内のサブクエリには名前を付けてください。上のAS numbersのようにします。名前がないと MySQL と SQL Server ではエラーになります。 - 単一の値として使うサブクエリは 1 行だけを返す必要があります:
SELECT (SELECT 1 UNION ALL SELECT 2) AS n;
-- ERROR: more than one row returned by a subquery used as an expression
-- (SQLite doesn't complain: it quietly uses the first row, 1)
- 速度は通常同じです。一般的にデータベースは、1 回だけ使われる CTE を同等のサブクエリと同じように実行計画を立てるため、読みやすい方を選んでください。
別の構文
自分自身を参照する CTE(後述)は WITH RECURSIVE と書きます:
-- In SQL Server, leave out RECURSIVE
WITH RECURSIVE counter AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM counter WHERE n < 3
)
SELECT n FROM counter;
-- Output:
-- n
-- -
-- 1
-- 2
-- 3
SQL Server では、WITH の前の文はセミコロンで終わる必要があります。そのため T-SQL のコードでは CTE を ;WITH で始めることがよくあります。
サブクエリではなく CTE を使うべきなのはいつ?
小さなステップが 1 つだけならサブクエリで十分です。クエリに複数のステップがある場合、同じ結果を 2 回使う場合、または自分自身を参照する必要がある場合は CTE に切り替えましょう。
計算した 1 つの値でフィルターする
内側のクエリが、注文合計の平均のような比較用の値やリストを返すだけなら、WHERE 内のサブクエリが最も短く書けます。CTE でも動きますが、不要な名前が増えるだけです。
長いクエリを名前付きのステップに分ける
クエリに段階がある場合(顧客ごとの合計、次に上位数件、最後に最終リスト)は、各段階に名前を付けましょう。1 つの WITH の後に複数の CTE をカンマ区切りで並べると、それぞれが前の CTE を使えるため、クエリを内側から外側ではなく上から下へ読めるようになります。
同じ結果を複数回使う
1 つの文の中で CTE は何度でも参照できるため、2 回必要な結果(合計のリストとその平均、各合計と総合計など)を 1 回書くだけで済みます。サブクエリでは内側のクエリ全体をコピーすることになります。
階層をたどる
自分自身を参照できるのは CTE だけです。再帰 CTE はいくつかの行から始まり、新しい行が見つからなくなるまでテーブルとの結合を繰り返します。これにより組織図やカテゴリツリーをたどったり、連続した数値や日付を生成したりできます。
CTE とサブクエリの実例
カテゴリ平均より高い価格の商品
このサブクエリは外側のクエリの p.category を参照するため、各商品が属するカテゴリごとに計算されます(相関サブクエリ):
-- Input: products
-- name | category | price
-- --------------+-----------+-------
-- Desk Lamp | Lighting | 24.99
-- Floor Lamp | Lighting | 89.00
-- Office Chair | Furniture | 149.00
-- Standing Desk | Furniture | 399.00
-- Bookshelf | Furniture | 120.00
SELECT name, category, price
FROM products p
WHERE price > (
SELECT AVG(price) FROM products WHERE category = p.category
)
ORDER BY category, name;
-- Output:
-- name | category | price
-- --------------+-----------+-------
-- Standing Desk | Furniture | 399.00
-- Floor Lamp | Lighting | 89.00
カテゴリごとの売れ筋商品
名前付きの 3 つのステップで構成します。商品ごとの販売数を合計し、カテゴリ内で商品を順位付けし、最上位の商品だけを残します:
-- Input: sales
-- id | product | category | units
-- ---+--------------+-----------+------
-- 1 | Desk Lamp | Lighting | 3
-- 2 | Floor Lamp | Lighting | 5
-- 3 | Desk Lamp | Lighting | 4
-- 4 | Office Chair | Furniture | 2
-- 5 | Bookshelf | Furniture | 6
WITH product_totals AS (
SELECT category, product, SUM(units) AS units
FROM sales
GROUP BY category, product
),
ranked AS (
SELECT category, product, units,
RANK() OVER (PARTITION BY category ORDER BY units DESC) AS place
FROM product_totals
)
SELECT category, product, units
FROM ranked
WHERE place = 1
ORDER BY category;
-- Output:
-- category | product | units
-- ----------+-----------+------
-- Furniture | Bookshelf | 6
-- Lighting | Desk Lamp | 7
地域ごとの売上シェア
region_totals は 2 回使われます。1 回は行の取得に、もう 1 回は総合計を求めるサブクエリの中で使われます:
-- Input: orders
-- id | region | amount
-- ---+--------+--------
-- 1 | North | 1200.00
-- 2 | South | 450.00
-- 3 | North | 300.00
-- 4 | West | 800.00
-- 5 | South | 250.00
WITH region_totals AS (
SELECT region, SUM(amount) AS total
FROM orders
GROUP BY region
)
SELECT
region,
total,
ROUND(100.0 * total / (SELECT SUM(total) FROM region_totals), 1) AS pct_of_sales
FROM region_totals
ORDER BY total DESC;
-- Output:
-- region | total | pct_of_sales
-- -------+---------+-------------
-- North | 1500.00 | 50.0
-- West | 800.00 | 26.7
-- South | 700.00 | 23.3
サブクエリで書くと、GROUP BY のクエリが 2 回登場することになります。
従業員の上司の系列
1 人の従業員から始めて、manager_id がなくなるまで上へたどります:
-- Input: employees
-- id | name | manager_id
-- ---+-----------+-----------
-- 1 | Maya Chen | NULL
-- 2 | Sam Ortiz | 1
-- 3 | Jo Kim | 2
-- 4 | Lee Ross | 2
-- 5 | Ana Diaz | 1
-- In SQL Server, leave out RECURSIVE
WITH RECURSIVE chain AS (
SELECT id, name, manager_id, 0 AS level
FROM employees
WHERE name = 'Jo Kim'
UNION ALL
SELECT e.id, e.name, e.manager_id, chain.level + 1
FROM employees e
JOIN chain ON e.id = chain.manager_id
)
SELECT level, name FROM chain ORDER BY level;
-- Output:
-- level | name
-- ------+----------
-- 0 | Jo Kim
-- 1 | Sam Ortiz
-- 2 | Maya Chen
CTE とサブクエリについての関連情報
このブログの記事:
- SQL 一時テーブルガイド: 中間結果を複数の文で保持する
-
SQL 重複検出ガイド:
ROW_NUMBER()を使った CTE で重複行を見つける - SQL 重複削除ガイド: CTE を使って重複を削除する
- SQL GROUP BY ガイド: 多くの CTE の土台となる集計
- SQL JOIN 図解ガイド: CTE を他のテーブルと結合する
公式ドキュメント:
- PostgreSQL: WITH クエリとサブクエリ式
- MySQL: WITH(共通テーブル式)とサブクエリ
- SQLite: WITH 句
- SQL Server: WITH common_table_expressionとサブクエリ
Beekeeper Studioは無料のオープンソースのデータベースGUIです
これまで使った中で最高のSQLクエリ&エディタツールです。データベースの管理に必要なものがすべて揃っています。 - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studioは高速で直感的、そして使いやすいツールです。多数のデータベースに対応し、Windows、Mac、Linuxで快適に動作します。
Beekeeper Studio 利用者の声
「Beekeeper Studio で以前の SQL ワークフローを完全に置き換えました。高速で直感的で、データベース作業がまた楽しくなりました。」
「多くのデータベース GUI を試しましたが、Beekeeper は機能とシンプルさのバランスが完璧です。とにかくちゃんと動きます。」