モダンSQLの3本柱とは何か
第4章までで扱った基本構文(SELECT/FROM/WHERE/JOIN/GROUP BY)はSQL-92で標準化された古典SQLの領域です。 本章で扱うサブクエリ・CTE・ウィンドウ関数は、SQL:1999以降に整備された「モダンSQL」の中核であり、 これらを使いこなせるかどうかがSQLの実力を大きく左右します。
| 機能 | 標準化 | 主な用途 |
|---|---|---|
| サブクエリ | SQL-92 (相関サブクエリ) | 「あるクエリの結果を使って別のクエリを絞る」 |
| CTE (WITH句) | SQL:1999 (再帰CTEも同時) | 複雑なクエリの可読性向上、再帰処理 |
| ウィンドウ関数 | SQL:2003 (強化はSQL:2011, 2016) | ランキング、移動平均、累積和、行間比較 |
サブクエリ — 置ける場所で3分類
サブクエリ(subquery)は他のクエリの中に埋め込まれたSELECT文で、 返す形によって置ける場所が決まります。
| 種類 | 返り値の形 | 置ける場所 | 例 |
|---|---|---|---|
| スカラサブクエリ | 1行×1列 | SELECT, WHERE, ORDER BY のほぼどこでも | (SELECT AVG(price) FROM products) |
| 行サブクエリ | 1行×N列 | WHERE の行比較 | WHERE (a, b) = (SELECT a, b FROM ...) |
| 表サブクエリ | N行×N列 | FROM, IN, EXISTS | FROM (SELECT ...) AS t |
相関サブクエリ vs 非相関サブクエリ
もう1つの重要な分類が「外側のクエリを参照するかどうか」です。 これは実行コストに直結します。
-- 非相関サブクエリ: 外側を参照しない → 1回だけ実行して使いまわせる
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1回評価してキャッシュ可能
-- 相関サブクエリ: 外側の行を参照する → 外側の各行ごとに再評価 (遅い)
SELECT p1.name, p1.price
FROM products p1
WHERE p1.price > (
SELECT AVG(p2.price)
FROM products p2
WHERE p2.category_id = p1.category_id -- ← 外側を参照 = 相関
);
-- 各行で評価されるので O(n^2) に近いコストになる場合が多い
-- ウィンドウ関数で書き換えるほうが速いケースが多い IN / EXISTS / NOT IN / NOT EXISTS
「特定集合に属する/属さない」判定で頻出の4つの構文は、見た目似ていてもNULLの扱いで罠があります。
-- ✅ IN と EXISTS は基本的に等価
SELECT * FROM orders o WHERE o.user_id IN (SELECT id FROM vip_users);
SELECT * FROM orders o WHERE EXISTS (SELECT 1 FROM vip_users v WHERE v.id = o.user_id);
-- ⚠️ NOT IN の罠: サブクエリが1つでもNULLを返すと結果が空になる
SELECT * FROM orders o WHERE o.user_id NOT IN (SELECT id FROM banned_users);
-- ↑ banned_users.id に NULL が1つでもあれば「全部NULL = UNKNOWN」扱いで
-- どの行も残らない
-- ✅ NOT EXISTS なら NULL安全
SELECT * FROM orders o
WHERE NOT EXISTS (SELECT 1 FROM banned_users b WHERE b.id = o.user_id); CTE (WITH句) — 可読性の革命
CTE(Common Table Expression, 共通テーブル式)はSQL:1999で導入された機能で、 クエリの前に名前付きの中間結果を定義できます。 サブクエリのネスト地獄から脱出する決定打であり、多くのモダンSQLチュートリアルで最初に教えられます。
-- ❌ ネストしたサブクエリは読みづらい
SELECT category, avg_price
FROM (
SELECT category, AVG(price) AS avg_price
FROM (
SELECT category, price
FROM products
WHERE in_stock = TRUE
) t
GROUP BY category
) t2
WHERE avg_price > 1000;
-- ✅ CTEなら上から下に読める
WITH in_stock_products AS (
SELECT category, price
FROM products
WHERE in_stock = TRUE
),
category_avg AS (
SELECT category, AVG(price) AS avg_price
FROM in_stock_products
GROUP BY category
)
SELECT category, avg_price
FROM category_avg
WHERE avg_price > 1000; 再帰CTE — 階層・グラフをSQLで扱う
CTEの中でも強力なのが再帰CTEです。組織図・カテゴリツリー・友達の友達検索など、 階層/グラフ構造を純粋なSQLで表現できます。
-- 組織の階層を全展開(id, name, manager_id を持つ employees テーブル)
WITH RECURSIVE org AS (
-- アンカー: CEO (manager_idがNULL)
SELECT id, name, manager_id, 0 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- 再帰部: 前ステップの結果から、それを上司とする部下を取ってくる
SELECT e.id, e.name, e.manager_id, o.level + 1
FROM employees e
JOIN org o ON e.manager_id = o.id
)
SELECT REPEAT(' ', level) || name AS hierarchy, level
FROM org
ORDER BY level, name; flowchart TD
A[アンカー部分<br/>SELECT ... FROM ... WHERE manager_id IS NULL] --> U[UNION ALL]
U --> R[再帰部分<br/>JOIN で前ステップの結果と結合]
R -->|新しい行があれば繰り返し| U
R -->|新しい行が無くなれば| DONE[終了]
style A fill:#3b82f6,color:#fff
style DONE fill:#10b981,color:#fffウィンドウ関数 — 「集約しない集約」
ウィンドウ関数(window functions, 分析関数とも呼ぶ)は、 GROUP BYのように集約しつつ、行を潰さないという一見矛盾する操作を可能にします。 SQL:2003で標準化され、現代SQLで最もインパクトの大きい機能です。
-- 各社員と、その部署の平均給与を並べて表示
SELECT
name,
dept,
salary,
AVG(salary) OVER (PARTITION BY dept) AS dept_avg
FROM employees;
-- 結果:
-- name | dept | salary | dept_avg
-- Alice | Eng | 800 | 700 <- Engの平均
-- Bob | Eng | 600 | 700
-- Carol | Sales| 500 | 550 <- Salesの平均
-- Dave | Sales| 600 | 550
ウィンドウ関数は 関数(...) OVER (PARTITION BY ... ORDER BY ... frame) という構文で、
3つの要素を制御できます。
| 要素 | 役割 | 例 |
|---|---|---|
| PARTITION BY | 分割キー(GROUP BY相当) | PARTITION BY deptで部署ごとに分ける |
| ORDER BY | ウィンドウ内の順序 | ORDER BY hire_dateで入社順 |
| frame | 現在行からどの範囲を集約対象にするか | ROWS BETWEEN 2 PRECEDING AND CURRENT ROWで直近3行 |
代表的なウィンドウ関数
| 分類 | 関数 | 用途 |
|---|---|---|
| ランキング | ROW_NUMBER() | 重複を許さない連番 (1,2,3,4) |
| ランキング | RANK() | 同点は同順位、次が飛ぶ (1,2,2,4) |
| ランキング | DENSE_RANK() | 同点は同順位、次は飛ばない (1,2,2,3) |
| 行間参照 | LAG(col, n) | n行前の値を取得 |
| 行間参照 | LEAD(col, n) | n行後の値を取得 |
| 集約 | SUM/AVG/COUNT/MIN/MAX | 通常の集約関数をウィンドウとして使える |
| 集約 | FIRST_VALUE / LAST_VALUE / NTH_VALUE | ウィンドウ内の特定位置の値 |
| 分位 | NTILE(n) | n分位に分割 |
| 分位 | PERCENT_RANK() / CUME_DIST() | 相対順位 |
-- 部署ごとの給与ランキング Top 3 を抽出(モダンSQL定番パターン)
SELECT *
FROM (
SELECT
name, dept, salary,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk
FROM employees
) t
WHERE rnk <= 3;
-- 前月比売上の増減率(LAGで1行前を参照)
SELECT
month,
sales,
LAG(sales) OVER (ORDER BY month) AS prev_sales,
(sales - LAG(sales) OVER (ORDER BY month)) * 100.0
/ LAG(sales) OVER (ORDER BY month) AS growth_pct
FROM monthly_sales;
-- 7日間移動平均
SELECT
date, daily_users,
AVG(daily_users) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7d
FROM user_stats; アンチパターン — GROUP BY→ウィンドウ、相関→JOIN
モダンSQLの3本柱を身につけると、書き換えでパフォーマンスが一桁改善するケースが多々あります。
-- アンチパターン: 相関サブクエリで各行の「部署内順位」を計算
SELECT
e1.name, e1.salary,
(SELECT COUNT(*) + 1
FROM employees e2
WHERE e2.dept = e1.dept AND e2.salary > e1.salary) AS rank_in_dept
FROM employees e1;
-- O(n^2)に近いコスト
-- リファクタ後: ウィンドウ関数で1回走査
SELECT
name, salary,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rank_in_dept
FROM employees;
-- O(n log n) になる 第5章のまとめ
- サブクエリは返り値の形(スカラ/行/表)と相関/非相関の2軸で分類する。相関は遅いので可能ならJOINやウィンドウ関数に書き換える
- NOT INはNULLが混じると全行消える罠がある。
NOT EXISTSが安全 - CTE(WITH句)はネストしたサブクエリを上から下に読める形に整理する。再帰CTEで階層・グラフも扱える
- ウィンドウ関数はPARTITION BY(分割)、ORDER BY(順序)、frame(範囲)の3要素で集約範囲を制御する。frame省略のデフォルトに注意
- GROUP BYでは集約できるが行が潰れる。行を残したまま集約したい場合はウィンドウ関数を使う
次章では、ここまでに書いてきたSQLを実際に実行するクエリプランナ/オプティマイザの世界に踏み込みます。 「WHAT」を書けば「HOW」を自動で決める頭脳が、どうやって最適な実行計画を選んでいるのかを解剖します。
理解度チェック
相関サブクエリ(correlated subquery)の定義として正しいものはどれですか?
キーボード: 1〜4 で選択、Enter で回答