8.

SQL インラインビュー完全ガイド (FROM 句サブクエリ / CTE 比較)

編集
この記事の要点
  • インラインビュー (Inline View) = FROM 句に書くサブクエリ。一時的な仮想テーブル
  • 構文: SELECT * FROM (SELECT ... FROM ... WHERE ...) AS xエイリアス (AS x) 必須
  • 通常の VIEW と違い 永続化されない。クエリ実行ごとに毎回評価
  • TOP N (上位 N 件) や順位付け (ROW_NUMBER) と相性が良い
  • 深くなりがちなので、可読性を上げたいときは CTE (WITH 句) に置き換えると良い

インラインビューとは

SQL の FROM 句にサブクエリを書き、その結果を仮想テーブルのように扱う技法をインラインビュー (Inline View) と呼びます。「インライン (in-line) = 行内」の意味で、永続的な VIEW オブジェクトを作らずクエリ内に埋め込みます。

-- 基本構文
SELECT *
FROM (
    SELECT user_id, COUNT(*) AS order_count
    FROM orders
    WHERE created_at >= '2026-01-01'
    GROUP BY user_id
) AS recent_orders   -- ★ エイリアス必須
WHERE order_count >= 10;

VIEW (永続ビュー) との違い

項目VIEW (永続ビュー)インラインビュー
定義場所DB に CREATE VIEW★ クエリ内 (FROM 句)
名前あり (再利用可能)そのクエリ限り
永続化定義は永続都度生成・破棄
権限管理VIEW にGRANT可能元テーブル権限
用途共通の集計・絞り込みを再利用その場の中間結果
-- 永続 VIEW
CREATE VIEW recent_orders AS
SELECT user_id, COUNT(*) AS order_count
FROM orders
WHERE created_at >= '2026-01-01'
GROUP BY user_id;

-- どこからでも使える
SELECT * FROM recent_orders WHERE order_count >= 10;

-- 一方インラインビューは 1 クエリ限り
SELECT *
FROM (SELECT user_id, COUNT(*) AS order_count
      FROM orders WHERE ... GROUP BY user_id) AS r
WHERE r.order_count >= 10;

典型ユースケース 1: 集計結果を更に絞り込む

SQL では集計関数の結果に対して直接 WHERE は使えず HAVING が必要ですが、インラインビューで集計後の絞り込みを分かりやすく書けます。

-- HAVING でも書ける
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 10;

-- インラインビューで段階的に
SELECT *
FROM (
    SELECT user_id, COUNT(*) AS cnt
    FROM orders
    GROUP BY user_id
) AS t
WHERE cnt >= 10;
-- → t.cnt のエイリアスを再利用できる

典型ユースケース 2: TOP N (上位 N 件)

-- カテゴリごとに売上 TOP 3 (ROW_NUMBER + インラインビュー)
SELECT category_id, product_name, sales
FROM (
    SELECT
        category_id,
        product_name,
        sales,
        ROW_NUMBER() OVER (
            PARTITION BY category_id
            ORDER BY sales DESC
        ) AS rn
    FROM products
) AS ranked
WHERE rn <= 3;

-- 国別の人口最大都市
SELECT country, city, population
FROM (
    SELECT
        country,
        city,
        population,
        ROW_NUMBER() OVER (PARTITION BY country ORDER BY population DESC) AS rn
    FROM cities
) AS t
WHERE rn = 1;

典型ユースケース 3: 結合前に絞り込み

-- ❌ 全件 JOIN してから絞り込み (重い)
SELECT u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.created_at >= '2026-01-01';

-- ✅ インラインビューで先に絞り込む
SELECT u.name, ro.total
FROM users u
JOIN (
    SELECT user_id, SUM(amount) AS total
    FROM orders
    WHERE created_at >= '2026-01-01'
    GROUP BY user_id
) AS ro ON u.id = ro.user_id;
-- → DB によっては自動最適化されるが、明示的に書くと安定

相関サブクエリとの違い

種類場所外側参照
スカラサブクエリSELECT / WHERESELECT (SELECT MAX(...) FROM ...)
インラインビュー★ FROM不可 (LATERAL 除く)FROM (SELECT ...) AS t
相関サブクエリWHERE / SELECT★ するWHERE EXISTS (SELECT 1 FROM ... WHERE x = outer.x)
CTE (WITH)クエリ先頭不可WITH x AS (...) SELECT ...

CTE (Common Table Expression / WITH 句)

インラインビューが深くネストすると読みづらいので、CTE で名前を付けて先頭に分離する方が読みやすくなります。

-- ❌ インラインビューが深い
SELECT *
FROM (
    SELECT category_id, AVG(price) AS avg_price
    FROM (
        SELECT category_id, price
        FROM products
        WHERE deleted_at IS NULL
    ) AS active_products
    GROUP BY category_id
) AS category_avg
WHERE avg_price > 1000;

-- ✅ CTE で段階的に
WITH active_products AS (
    SELECT category_id, price
    FROM products
    WHERE deleted_at IS NULL
),
category_avg AS (
    SELECT category_id, AVG(price) AS avg_price
    FROM active_products
    GROUP BY category_id
)
SELECT * FROM category_avg WHERE avg_price > 1000;

-- 再帰 CTE で組織図のような階層を辿る
WITH RECURSIVE org AS (
    SELECT id, name, parent_id, 0 AS level
    FROM employees WHERE parent_id IS NULL
    UNION ALL
    SELECT e.id, e.name, e.parent_id, o.level + 1
    FROM employees e
    JOIN org o ON e.parent_id = o.id
)
SELECT * FROM org ORDER BY level, id;

各 DB の対応状況

DBインラインビューCTE再帰 CTE
MySQL 8.0+
MySQL 5.7 以下××
MariaDB 10.2+
PostgreSQL
Oracle
SQL Server
SQLite 3.8.3+

パフォーマンスの注意点

  • 多くの DB はインラインビューをマージして最適化する (MySQL 5.7+, PostgreSQL, Oracle, SQL Server)
  • ただし ORDER BY / LIMIT / GROUP BY を含むと materialize される傾向 (= テンポラリテーブル化)
  • 大きな結果セットをインラインビューにするとメモリ・ディスク負荷
  • 外側からインデックスが効かなくなることがある → EXPLAIN で確認
  • PostgreSQL 12+ では CTE もマージされるようになった (古い PG は CTE が最適化フェンスだった)
-- 実行計画を確認
EXPLAIN
SELECT *
FROM (SELECT user_id, COUNT(*) AS cnt FROM orders GROUP BY user_id) AS t
WHERE cnt >= 10;

-- PostgreSQL の場合
EXPLAIN ANALYZE ...;

エイリアス必須に注意

-- ❌ MySQL / PostgreSQL: ERROR (エイリアスが無い)
SELECT * FROM (SELECT id FROM users);

-- ✅ AS で名前を付ける (AS は省略可だが付ける方が読みやすい)
SELECT * FROM (SELECT id FROM users) AS t;

-- インラインビュー内のカラム名が重複する場合は外側で参照不能
SELECT a, b
FROM (
    SELECT u.id AS a, o.id AS a, ...  -- ❌ 重複
    FROM users u JOIN orders o ON ...
) AS t;

-- 明示的に別名を付ける
SELECT t.uid, t.oid
FROM (
    SELECT u.id AS uid, o.id AS oid
    FROM users u JOIN orders o ON u.id = o.user_id
) AS t;

UPDATE / DELETE での利用

-- PostgreSQL / SQL Server で UPDATE FROM
UPDATE orders o
SET status = 'flagged'
FROM (
    SELECT user_id
    FROM orders
    GROUP BY user_id
    HAVING SUM(amount) > 1000000
) AS big_spenders
WHERE o.user_id = big_spenders.user_id;

-- MySQL は UPDATE with JOIN
UPDATE orders o
JOIN (
    SELECT user_id FROM orders GROUP BY user_id HAVING SUM(amount) > 1000000
) AS big ON o.user_id = big.user_id
SET o.status = 'flagged';

FAQ

Q: インラインビューと CTE、どちらを使うべき?
A: 1 度しか使わないならインラインビューでも CTE でもよいが、段階が深い・複数回参照するなら CTE が圧倒的に読みやすい。新規 SQL なら CTE 推奨。

Q: インラインビューでインデックスは効く?
A: 内側の元テーブルのインデックスは効きます。外側からインラインビュー結果に対する更なる検索は、Optimizer がマージできれば効くが、materialize されるとフルスキャンになることも。EXPLAIN で確認

Q: ネストが深くて読めない
A: CTE に書き直すか、永続 VIEW として切り出すと劇的に読みやすくなります。SQL は左から右・上から下に読める形にするのが原則。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. SELECT文
  2. INSERT
  3. UPDATE文
  4. DELETE文
  5. WHERE句
  6. JOIN句
  7. 集合演算子
  8. インラインビュー
  9. 副問い合わせ (サブクエリ)

最近更新/作成されたページ