もう知ってると思うけど、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に切り替えよう。半年後の自分(か、未来の同僚)がきっと感謝してくれるよ!
GO TO FULL VERSION