SQLでデータベースを扱っていると、「一時的にテーブルを作って計算したい」という場面に出会うことがあります。
「サブクエリって複雑そう…」
「一時テーブルを作るのは面倒だな」
そんな時に役立つのがインラインテーブルです。
クエリの中で直接テーブルを定義して、その場で使える——このシンプルで強力な機能を使いこなせば、SQLの表現力が格段に上がります。
今回は、インラインテーブルの基本から、実際の使い方、メリット・デメリット、そして実務での活用例まで、分かりやすく解説していきますね。
インラインテーブルとは?基本を理解しよう

クエリ内で定義される一時的なテーブル
インラインテーブルとは、SQL文の中で直接定義される一時的なテーブルのことです。
別名として:
- 派生テーブル(Derived Table)
- インラインビュー(Inline View)
- サブクエリテーブル
とも呼ばれます。
特徴:
- FROM句の中で定義する
- そのクエリの実行中だけ存在する
- 実際のテーブルとして保存されない
- 名前を付けて、通常のテーブルのように扱える
通常のテーブルとの違い
通常のテーブル:
-- テーブルを作成(永続的)
CREATE TABLE users (
id INT,
name VARCHAR(100),
age INT
);
-- データを挿入
INSERT INTO users VALUES (1, '田中', 28);
テーブルがデータベースに保存され、何度でも使えます。
インラインテーブル:
-- クエリの中で一時的にテーブルを作成
SELECT *
FROM (
SELECT 1 AS id, '田中' AS name, 28 AS age
UNION ALL
SELECT 2, '佐藤', 35
UNION ALL
SELECT 3, '鈴木', 42
) AS inline_users;
クエリが終われば消える、使い捨てのテーブルです。
インラインテーブルの基本的な書き方
FROM句内のサブクエリ
最も一般的な形式です。
基本構文:
SELECT カラム名
FROM (
-- ここにサブクエリ
SELECT ...
FROM テーブル名
WHERE 条件
) AS 別名;
重要なポイント:
- サブクエリを丸括弧
()で囲む - 必ず別名(エイリアス)を付ける
- 別名は
ASを使って指定(DBMSによってはASは省略可)
シンプルな例
例1:年齢が30歳以上のユーザーを抽出してから処理
SELECT *
FROM (
SELECT id, name, age
FROM users
WHERE age >= 30
) AS adult_users;
何が起こっているか:
- 内側のクエリで年齢30歳以上を抽出
- その結果を
adult_usersという名前のインラインテーブルとして扱う - 外側のクエリでそのテーブルから全データを取得
VALUES句を使った定義
SQLによっては、VALUES句で直接データを定義できます。
例2:固定データをインラインテーブルとして使う
SELECT *
FROM (
VALUES
(1, '東京', 1400),
(2, '大阪', 880),
(3, '名古屋', 230)
) AS cities(id, name, population);
メリット:
- テストデータを手軽に作成できる
- 小さなマスターデータを一時的に使える
- クエリが自己完結する
WITH句(共通テーブル式)との関係
WITH句(CTE: Common Table Expression)も、一種のインラインテーブルです。
WITH adult_users AS (
SELECT id, name, age
FROM users
WHERE age >= 30
)
SELECT *
FROM adult_users;
違い:
- FROM句のサブクエリ:その場限り
- WITH句:同じクエリ内で複数回参照できる、読みやすい
WITH句については後ほど詳しく説明します。
インラインテーブルの実用例
例1:集計結果をさらに加工する
シナリオ:部門ごとの平均給与を計算し、全体平均より高い部門を抽出
-- インラインテーブルを使わない場合(一時テーブルが必要)
CREATE TEMPORARY TABLE dept_avg AS
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department;
SELECT *
FROM dept_avg
WHERE avg_salary > (SELECT AVG(avg_salary) FROM dept_avg);
-- インラインテーブルを使う場合(一発で完結)
SELECT *
FROM (
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
) AS dept_avg
WHERE avg_salary > (
SELECT AVG(avg_salary)
FROM (
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
) AS dept_avg2
);
一時テーブルを作らずに、1つのクエリで完結します。
例2:複雑な計算結果に番号を振る
シナリオ:商品の売上ランキングを作成
SELECT
ROW_NUMBER() OVER (ORDER BY total_sales DESC) AS rank,
product_name,
total_sales
FROM (
SELECT
p.product_name,
SUM(o.quantity * o.price) AS total_sales
FROM orders o
JOIN products p ON o.product_id = p.id
GROUP BY p.product_name
) AS sales_summary
ORDER BY rank;
処理の流れ:
- 内側:商品ごとの売上合計を計算
- インラインテーブル
sales_summaryとして結果を保持 - 外側:売上順にランク番号を付与
例3:複数のテーブルを結合する前に絞り込む
シナリオ:2023年の注文データだけを使って分析
SELECT
u.name AS user_name,
orders_2023.order_count,
orders_2023.total_amount
FROM users u
JOIN (
SELECT
user_id,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE order_date >= '2023-01-01'
AND order_date < '2024-01-01'
GROUP BY user_id
) AS orders_2023 ON u.id = orders_2023.user_id;
メリット:
- 不要なデータを事前に除外できる
- パフォーマンスが向上する可能性がある
例4:複数の集計を並べる
シナリオ:月別・年別の売上を一覧表示
SELECT 'Monthly' AS period, month, sales FROM (
SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, SUM(amount) AS sales
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
) AS monthly_sales
UNION ALL
SELECT 'Yearly' AS period, year, sales FROM (
SELECT DATE_FORMAT(order_date, '%Y') AS year, SUM(amount) AS sales
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y')
) AS yearly_sales;
それぞれの集計をインラインテーブルで行い、UNIONで結合しています。
WITH句(CTE)との使い分け
WITH句(共通テーブル式)とは
WITH句は、クエリの冒頭で一時的なテーブルを定義する方法です。
基本構文:
WITH テーブル名 AS (
SELECT ...
)
SELECT *
FROM テーブル名;
インラインテーブル vs WITH句
インラインテーブル(FROM句のサブクエリ):
SELECT *
FROM (
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
) AS dept_avg
WHERE avg_salary > 50000;
メリット:
- その場で定義できる
- シンプルなクエリに向いている
デメリット:
- 複数回参照すると、同じサブクエリを繰り返す必要がある
- 複雑になると読みにくい
WITH句(CTE):
WITH dept_avg AS (
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
)
SELECT *
FROM dept_avg
WHERE avg_salary > 50000;
メリット:
- 読みやすい(上から順に処理が分かる)
- 同じ結果を複数回参照できる
- 再帰クエリが書ける(後述)
デメリット:
- やや冗長になることがある
使い分けの目安
インラインテーブルを使う場合:
- 一度だけ使う簡単な加工
- ネストが浅い(1〜2階層)
- すぐに結果を確認したい時
WITH句を使う場合:
- 同じ結果を複数回使いたい
- ネストが深い(3階層以上)
- クエリの可読性を重視
- チームで共有するクエリ
WITH句の複数定義
WITH句は複数のテーブルを定義できます。
WITH
-- 部門ごとの平均給与
dept_avg AS (
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
),
-- 全体の平均給与
overall_avg AS (
SELECT AVG(salary) AS avg_salary
FROM employees
)
-- メインクエリ
SELECT
da.department,
da.avg_salary,
oa.avg_salary AS company_avg,
da.avg_salary - oa.avg_salary AS difference
FROM dept_avg da
CROSS JOIN overall_avg oa
ORDER BY difference DESC;
利点:
- クエリの構造が明確
- 段階的に処理を書ける
- デバッグしやすい
インラインテーブルのメリット

一時テーブルが不要
従来の方法:
-- 一時テーブルを作成
CREATE TEMPORARY TABLE temp_result AS
SELECT ...;
-- 一時テーブルを使用
SELECT * FROM temp_result;
-- 一時テーブルを削除
DROP TEMPORARY TABLE temp_result;
インラインテーブル:
-- 1つのクエリで完結
SELECT *
FROM (
SELECT ...
) AS temp_result;
メリット:
- 作成・削除の手間がない
- クエリが自己完結する
- 一時テーブルの管理が不要
クエリが再利用しやすい
インラインテーブルを使ったクエリは、そのままコピーすれば他の環境でも動きます。
理由:
- 外部のテーブルに依存しない(元のテーブルは除く)
- 一時テーブルの作成権限が不要
- スクリプトとして保存しやすい
デバッグが容易
インラインテーブルの部分だけを実行して、中間結果を確認できます。
-- まず内側だけを実行して確認
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department;
-- 問題なければ、外側を追加
SELECT *
FROM (
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
) AS dept_avg
WHERE avg_salary > 50000;
複雑な処理を段階的に構築
段階的なアプローチ:
- 基本的な集計を書く
- インラインテーブルで包む
- さらに外側で加工を追加
このように、少しずつ機能を追加できます。
インラインテーブルのデメリット
可読性が低下することがある
ネストが深くなると、読みにくくなります。
悪い例:
SELECT *
FROM (
SELECT *
FROM (
SELECT *
FROM (
SELECT * FROM users
) AS t1
) AS t2
) AS t3;
こうなると、何をしているのか分かりません。
対策:
- ネストは2〜3階層まで
- 深くなる場合はWITH句を使う
パフォーマンスの問題
同じサブクエリを複数回書くと:
SELECT
(SELECT AVG(age) FROM users) AS avg_age,
name,
age - (SELECT AVG(age) FROM users) AS diff
FROM users;
平均年齢を2回計算しています(無駄)。
改善:
WITH avg_table AS (
SELECT AVG(age) AS avg_age FROM users
)
SELECT
a.avg_age,
u.name,
u.age - a.avg_age AS diff
FROM users u
CROSS JOIN avg_table a;
WITH句で1回だけ計算し、結果を再利用します。
DBMSによる最適化の違い
データベースシステムによって、インラインテーブルの最適化方法が異なります。
注意点:
- インラインテーブルが大きいと、メモリを消費する
- インデックスが使えない場合がある
- 統計情報がないため、オプティマイザが最適なプランを選べないことも
対策:
- 実行計画を確認する(EXPLAIN)
- 必要に応じて一時テーブルやビューを使う
実行計画とパフォーマンス
EXPLAINで確認
インラインテーブルのパフォーマンスを確認するには、実行計画を見ましょう。
MySQL / PostgreSQL:
EXPLAIN
SELECT *
FROM (
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
) AS dept_avg
WHERE avg_salary > 50000;
確認ポイント:
- どのテーブルをスキャンしているか
- インデックスが使われているか
- 推定される処理行数
- 結合方法
マテリアライゼーション(実体化)
一部のDBMSは、インラインテーブルを一時的に実体化(メモリやディスクに保存)します。
利点:
- 複数回参照しても、1回だけ計算
- 複雑な処理が速くなることがある
欠点:
- 実体化のオーバーヘッド
- メモリやディスクを消費
制御方法(PostgreSQL):
-- 実体化しない(OFFSET 0 トリック)
SELECT *
FROM (
SELECT * FROM large_table
OFFSET 0 -- 実体化を防ぐ
) AS sub;
インデックスのヒント
インラインテーブルには、インデックスがありません。
対策:
- 元のテーブルの段階で絞り込む
- WHERE条件を内側のクエリに入れる
- 結果が大きくなりすぎないようにする
-- 良い例:早めに絞り込む
SELECT *
FROM (
SELECT *
FROM large_table
WHERE created_at >= '2024-01-01' -- インデックスが使える
) AS filtered
WHERE category = 'Electronics';
-- 悪い例:全件取得してから絞り込む
SELECT *
FROM (
SELECT *
FROM large_table -- 全件取得(遅い)
) AS filtered
WHERE created_at >= '2024-01-01'
AND category = 'Electronics';
各DBMSでの違いと注意点
MySQL
特徴:
- インラインテーブルに必ず別名が必要
- 派生テーブル(Derived Table)と呼ばれる
エイリアスの必須性:
-- エラー:別名がない
SELECT * FROM (SELECT * FROM users);
-- 正しい
SELECT * FROM (SELECT * FROM users) AS u;
最適化:
- MySQL 5.7以降は、派生テーブルの最適化が改善
- マテリアライゼーションと結合プッシュダウンを自動判断
PostgreSQL
特徴:
- インラインビュー(Inline View)と呼ばれることが多い
- 強力な最適化機能
エイリアス:
-- PostgreSQLでは別名は推奨だが、一部のケースで省略可能
-- ただし、明示的に付けるべき
SELECT * FROM (SELECT * FROM users) AS u;
CTE(WITH句)の最適化:
- PostgreSQL 12以降、CTEのインライン展開が改善
- MATERIALIZED / NOT MATERIALIZED キーワードで制御可能
WITH dept_avg AS MATERIALIZED (
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
)
SELECT * FROM dept_avg;
SQL Server
特徴:
- 派生テーブル(Derived Table)をサポート
- 強力なクエリオプティマイザ
VALUES句:
SELECT *
FROM (VALUES
(1, '東京'),
(2, '大阪')
) AS Cities(ID, Name);
テーブル変数との違い:
DECLARE @table TABLEは永続的なセッション変数- インラインテーブルはクエリ内だけ
Oracle
特徴:
- インラインビューと呼ばれる
- 歴史が長く、最適化が洗練されている
エイリアス:
-- Oracleでは AS は不要(省略可能)
SELECT * FROM (SELECT * FROM users) u;
ROWNUM との組み合わせ:
-- 上位10件を取得
SELECT *
FROM (
SELECT * FROM users ORDER BY created_at DESC
)
WHERE ROWNUM <= 10;
実務での活用パターン
レポート作成
月次レポート:
SELECT
report_month,
total_sales,
total_orders,
avg_order_value
FROM (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS report_month,
SUM(amount) AS total_sales,
COUNT(*) AS total_orders,
AVG(amount) AS avg_order_value
FROM orders
WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
) AS monthly_report
ORDER BY report_month DESC;
データクレンジング
重複データの除外:
SELECT DISTINCT *
FROM (
SELECT
email,
MAX(created_at) AS latest_date
FROM users
GROUP BY email
) AS unique_emails
JOIN users u ON unique_emails.email = u.email
AND unique_emails.latest_date = u.created_at;
ランキング作成
売上トップ10の商品:
SELECT *
FROM (
SELECT
product_name,
total_sales,
RANK() OVER (ORDER BY total_sales DESC) AS sales_rank
FROM (
SELECT
p.name AS product_name,
SUM(o.quantity * o.price) AS total_sales
FROM orders o
JOIN products p ON o.product_id = p.id
GROUP BY p.name
) AS sales_data
) AS ranked_products
WHERE sales_rank <= 10;
データのピボット
年度別・部門別の集計:
SELECT
department,
SUM(CASE WHEN year = 2022 THEN sales ELSE 0 END) AS sales_2022,
SUM(CASE WHEN year = 2023 THEN sales ELSE 0 END) AS sales_2023,
SUM(CASE WHEN year = 2024 THEN sales ELSE 0 END) AS sales_2024
FROM (
SELECT
department,
YEAR(order_date) AS year,
SUM(amount) AS sales
FROM orders
GROUP BY department, YEAR(order_date)
) AS yearly_sales
GROUP BY department;
よくある質問と回答
Q1:インラインテーブルに別名を付け忘れたらどうなる?
A:多くのDBMSでエラーになります。
エラーメッセージの例(MySQL):
Every derived table must have its own alias
対策:
必ず AS エイリアス名 を付ける習慣をつけましょう。
Q2:インラインテーブルとビューの違いは?
A:永続性と再利用性が異なります。
インラインテーブル:
- クエリ実行時のみ存在
- そのクエリでしか使えない
- データベースに保存されない
ビュー:
- データベースに定義として保存される
- 複数のクエリから参照できる
- 権限管理ができる
使い分け:
- 一度きり → インラインテーブル
- 頻繁に使う → ビュー
Q3:パフォーマンスが悪い時はどうすれば?
A:以下の方法を試してください。
対策:
- 実行計画を確認(EXPLAIN)
- 内側のクエリで絞り込みを強化
- WITH句に変更(複数回参照する場合)
- 一時テーブルやビューを検討(大量データの場合)
- インデックスを追加(元のテーブルに)
Q4:ネストはどこまで深くしていい?
A:2〜3階層が限度です。
推奨:
- 1階層:シンプル、読みやすい
- 2階層:まだOK
- 3階層:限界、WITH句を検討
- 4階層以上:避けるべき
改善方法:
WITH句を使って、段階的に定義しましょう。
Q5:インラインテーブルにINDEXは使える?
A:いいえ、使えません。
理由:
インラインテーブルは一時的な結果セットで、物理的なテーブルではないため。
対策:
- 元のテーブルの段階でインデックスを活用
- 早めにデータを絞り込む
- 結果が大きい場合は一時テーブルを検討
まとめ:インラインテーブルで柔軟なクエリを書こう
インラインテーブルは、SQLクエリの表現力を大きく広げる強力な機能です。
この記事のポイント:
- インラインテーブルはFROM句内で定義される一時的なテーブル
- 派生テーブル、インラインビューとも呼ばれる
- 必ず別名(エイリアス)を付ける必要がある
- 一時テーブルを作らずに、1つのクエリで複雑な処理が可能
- WITH句(CTE)はインラインテーブルの発展形で、読みやすい
- 集計結果の加工、ランキング作成、データクレンジングなどで活躍
- ネストは2〜3階層まで、それ以上はWITH句を使う
- パフォーマンスを考慮し、EXPLAINで実行計画を確認
- MySQL、PostgreSQL、SQL Server、Oracleで微妙に動作が異なる
- 一度きりの処理ならインラインテーブル、頻繁に使うならビュー
実践のコツ:
- まず内側のクエリを単独で実行して確認
- 動作を確認してから外側を追加
- 複雑になったらWITH句に書き換え
- 実行計画を見て、パフォーマンスを確認
インラインテーブルを使いこなせば、複雑なデータ分析も1つのクエリで表現できるようになります。
最初は簡単な例から始めて、少しずつ複雑な処理に挑戦してみてください。
SQLの世界が、ぐっと広がりますよ!

コメント