Step 3:FROM句のサブクエリ

📊 Step 3: FROM句のサブクエリ

インラインビューで複雑な集計結果を再利用しよう!

📋 このステップで学ぶこと
  • インラインビュー(派生テーブル)とは何か
  • FROM句でサブクエリを使う方法と必要なルール
  • 複雑な集計結果を再利用するテクニック
  • 複数のサブクエリを結合する方法
  • サブクエリの入れ子構造
  • 可読性を保つコツ

🎯 1. FROM句のサブクエリとは?

1-1. 前のステップの復習

Step 2では、WHERE句でサブクエリを使いました。「平均価格より高い商品」のように、条件の中でサブクエリを使う方法でした。

今回は、FROM句でサブクエリを使う方法を学びます。これは全く違う使い方で、サブクエリの結果を「一時的なテーブル」として扱います。

1-2. FROM句のサブクエリとは

通常、FROM句にはテーブル名を書きますよね。

※横にスクロールできます

— 普通のクエリ:テーブル名を指定 SELECT * FROM products;

しかし、FROM句にはサブクエリを書くこともできます。サブクエリの結果が「一時的なテーブル」のように扱われます。

※横にスクロールできます

— FROM句にサブクエリを使う SELECT * FROM ( SELECT product_name, price FROM products WHERE price >= 1000 ) AS expensive_products;

この書き方をインラインビューまたは派生テーブル(Derived Table)と呼びます。

💡 インラインビューのイメージ

料理で例えると、「材料を下ごしらえしてから調理する」イメージです。

  • サブクエリ = 下ごしらえ(材料を切る、茹でるなど)
  • メインクエリ = 本調理(下ごしらえした材料を使って料理する)

先にデータを加工・集計してから、その結果に対してさらに処理を行うことができます。

1-3. FROM句のサブクエリの重要なルール

FROM句でサブクエリを使うとき、必ず守るべきルールがあります。

⚠️ 必須ルール:別名(エイリアス)をつける

FROM句のサブクエリには、必ず AS 別名 をつける必要があります。つけないとエラーになります!

※横にスクロールできます

— ❌ エラーになる例(別名がない) SELECT * FROM ( SELECT product_name, price FROM products ); — エラー! Every derived table must have its own alias — ✅ 正しい例(別名をつける) SELECT * FROM ( SELECT product_name, price FROM products ) AS product_list; — ← AS で別名をつける
なぜ別名が必要なのか:

サブクエリの結果は「仮想的なテーブル」になります。テーブルには名前が必要なので、別名をつけて名前を与えます。この名前は、メインクエリの中でそのサブクエリを参照するときに使います。

1-4. なぜFROM句でサブクエリを使うのか?

FROM句のサブクエリが特に役立つのは、集計した結果に対して、さらに処理をしたいときです。

活用シーン:
  • 集計結果をフィルタリング:カテゴリ別の平均価格が2000円以上のカテゴリだけを表示
  • 集計結果をさらに集計:月別売上から、四半期ごとの平均を計算
  • 複雑な計算を段階的に:利益率を計算してから、利益率が高い商品を抽出
  • 複数の集計を結合:商品数の統計と売上の統計を別々に計算して結合

1-5. WHERE句のサブクエリとの違い

WHERE句のサブクエリとFROM句のサブクエリは、役割が異なります。

📊 WHERE句 vs FROM句 のサブクエリ比較
項目 WHERE句のサブクエリ FROM句のサブクエリ
役割 条件の値を計算 一時的なテーブルを作成
返すもの 値やリスト テーブル(行と列)
別名 不要 必須
主な用途 平均との比較、存在チェック 集計結果の再利用、段階的な処理

📊 2. 基本的な使い方

2-1. 最もシンプルな例

まずは、最もシンプルな例から始めましょう。「1000円以上の商品」を取得して、その結果を使います。

やりたいこと:1000円以上の商品から、価格が高い順にトップ5を取得する

まず、サブクエリ部分(下ごしらえ)を確認します。

※横にスクロールできます

— サブクエリ部分:1000円以上の商品を取得 SELECT product_name, price, stock FROM products WHERE price >= 1000;

次に、この結果を「一時的なテーブル」としてFROM句に入れます。

※横にスクロールできます

— 完成形:サブクエリの結果を使ってさらに処理 SELECT product_name, price, stock FROM ( — まず1000円以上の商品を取得(下ごしらえ) SELECT product_name, price, stock FROM products WHERE price >= 1000 ) AS expensive_products — 別名をつける ORDER BY stock DESC — 在庫が多い順に並べる LIMIT 5; — トップ5を取得
クエリの解説:
  • サブクエリ: SELECT ... FROM products WHERE price >= 1000 → 1000円以上の商品を取得
  • AS expensive_products → サブクエリの結果に「expensive_products」という名前をつける
  • メインクエリ: この「expensive_products」から、在庫順にトップ5を取得
実行結果のイメージ:
product_name price stock
USBケーブル 1,200 200
ワイヤレスマウス 2,500 150
Bluetoothスピーカー 8,500 120
📌 ポイント:この例はサブクエリなしでも書ける

実はこの例は WHERE price >= 1000 ORDER BY stock DESC LIMIT 5 と1つのクエリで書けます。しかし、次の例のように集計結果を使う場合は、FROM句のサブクエリが必要になります。

2-2. 集計結果をフィルタリングする

FROM句のサブクエリが本当に役立つのは、GROUP BYで集計した結果を、さらに絞り込みたいときです。

やりたいこと:カテゴリ別の平均価格を計算し、平均価格が2000円以上のカテゴリだけを表示する

「平均価格が2000円以上」という条件をつけたいのですが、ここで問題があります。

⚠️ よくある間違い:WHERE句でAVGは使えない

※横にスクロールできます

— ❌ これはエラーになる! SELECT category, AVG(price) as avg_price FROM products GROUP BY category WHERE AVG(price) >= 2000; — エラー! 集計関数はWHEREで使えない

WHERE句は、GROUP BYで集計するに個々の行を絞り込むためのものです。集計した結果に対する条件は、HAVINGを使うか、FROM句のサブクエリを使います。

解決策1: HAVINGを使う

※横にスクロールできます

— HAVINGを使う方法 SELECT category, AVG(price) as avg_price FROM products GROUP BY category HAVING AVG(price) >= 2000;

解決策2: FROM句のサブクエリを使う

まず、カテゴリ別の集計を行うサブクエリを作ります。

※横にスクロールできます

— Step 1: カテゴリ別に集計(サブクエリ部分) SELECT category, AVG(price) as avg_price, COUNT(*) as product_count FROM products GROUP BY category;
このサブクエリの結果イメージ:
category avg_price product_count
家電 8,500 120
食品 500 250
家具 15,000 45

次に、この結果をFROM句に入れて、条件で絞り込みます。

※横にスクロールできます

— Step 2: 完成形 SELECT category, avg_price, product_count FROM ( — カテゴリ別に集計 SELECT category, AVG(price) as avg_price, COUNT(*) as product_count FROM products GROUP BY category ) AS category_stats — 別名をつける WHERE avg_price >= 2000 — 集計結果に対してWHEREが使える! ORDER BY avg_price DESC;
クエリの解説:
  • サブクエリ: カテゴリ別の平均価格と商品数を計算
  • AS category_stats → サブクエリの結果に名前をつける
  • WHERE avg_price >= 2000 → サブクエリで計算した avg_price に対して条件をつける
HAVINGとの違い:

HAVINGでも同じことができますが、FROM句のサブクエリを使うとより複雑な条件追加の処理を柔軟に行えます。

2-3. 計算列を使ったフィルタリング

やりたいこと:各商品の利益率を計算し、利益率が50%以上の商品を抽出する

利益率の計算式は「(販売価格 – 原価) ÷ 原価 × 100」です。この計算結果に対して条件をつけたいので、FROM句のサブクエリが便利です。

まず、利益率を計算するサブクエリを作ります。

※横にスクロールできます

— Step 1: 利益率を計算(サブクエリ部分) SELECT product_name, price, cost, ROUND((price – cost) * 100.0 / cost, 2) as profit_rate FROM products WHERE cost > 0; — 0で割るのを防ぐ
計算式の解説:
  • (price - cost) → 利益額(販売価格 – 原価)
  • * 100.0 / cost → 原価に対する割合(%)を計算
  • ROUND(..., 2) → 小数点以下2桁に丸める
  • WHERE cost > 0 → 原価が0の商品を除外(0で割るとエラーになるため)

次に、完成形のクエリを作ります。

※横にスクロールできます

— Step 2: 完成形 SELECT product_name, price, cost, profit_rate FROM ( SELECT product_name, price, cost, ROUND((price – cost) * 100.0 / cost, 2) as profit_rate FROM products WHERE cost > 0 ) AS profit_analysis WHERE profit_rate >= 50 — 利益率50%以上 ORDER BY profit_rate DESC;
実行結果のイメージ:
product_name price cost profit_rate
高級コーヒー豆 3,000 800 275.00
オリジナルTシャツ 2,500 1,000 150.00
デザイナーズマグ 1,800 1,000 80.00
💡 FROM句のサブクエリのメリット

サブクエリで計算した profit_rate を、メインクエリのWHERE句でもSELECT句でもORDER BY句でも自由に使えます。計算を1回書くだけで、複数の場所で再利用できるのが便利です!

🔄 3. 集計結果を再利用する

3-1. FROM句のサブクエリの真価

FROM句のサブクエリの最大のメリットは、集計結果をさらに加工できることです。「集計の集計」や「段階的な処理」が可能になります。

例えば、「顧客ごとの注文統計を計算して、そこからVIP顧客を抽出する」といった複雑な処理ができます。

3-2. 例1:顧客別の注文統計からVIP顧客を抽出

やりたいこと:顧客ごとの注文件数と総購入額を計算し、注文10回以上かつ総額10万円以上のVIP顧客を抽出する

これを実現するには、まず「顧客ごとの統計」を計算し、その結果から「VIPの条件に合う顧客」を絞り込みます。

Step 1: 顧客ごとの統計を計算するサブクエリを作る

※横にスクロールできます

— 顧客ごとに注文統計を計算 SELECT customers.customer_id, customers.customer_name, COUNT(orders.order_id) as order_count, SUM(orders.total_amount) as total_spent, AVG(orders.total_amount) as avg_order_amount FROM customers LEFT JOIN orders ON customers.customer_id = orders.customer_id GROUP BY customers.customer_id, customers.customer_name;
クエリの解説:
  • LEFT JOIN → 注文がない顧客も含める
  • COUNT(orders.order_id) → 注文件数をカウント
  • SUM(orders.total_amount) → 総購入額を計算
  • AVG(orders.total_amount) → 平均注文額を計算
  • GROUP BY → 顧客ごとにグループ化
サブクエリの結果イメージ:
customer_id customer_name order_count total_spent avg_order_amount
1001 田中太郎 25 350,000 14,000
1002 鈴木花子 3 15,000 5,000
1003 佐藤次郎 15 180,000 12,000

Step 2: この結果をFROM句に入れて、VIP条件で絞り込む

※横にスクロールできます

— 完成形:VIP顧客を抽出 SELECT customer_name, order_count, total_spent, avg_order_amount FROM ( — 顧客ごとに集計 SELECT customers.customer_id, customers.customer_name, COUNT(orders.order_id) as order_count, SUM(orders.total_amount) as total_spent, AVG(orders.total_amount) as avg_order_amount FROM customers LEFT JOIN orders ON customers.customer_id = orders.customer_id GROUP BY customers.customer_id, customers.customer_name ) AS customer_stats WHERE order_count >= 10 — 注文10回以上 AND total_spent >= 100000 — 総額10万円以上 ORDER BY total_spent DESC;
クエリの解説:
  • サブクエリ: 顧客ごとの統計を計算
  • AS customer_stats → サブクエリに「customer_stats」という名前をつける
  • WHERE order_count >= 10 AND total_spent >= 100000 → VIP条件で絞り込み
  • ORDER BY total_spent DESC → 購入額が多い順に並べる
なぜこの書き方が必要か:

COUNT()SUM() の結果は、通常の WHERE 句では直接参照できません。FROM句のサブクエリを使うことで、集計結果を「普通の列」のように扱えるようになります。

3-3. 例2:カテゴリ別統計の全体平均を計算

やりたいこと:カテゴリ別の平均価格を計算し、その「平均の平均」(全カテゴリの平均価格の平均)を求める

これは「集計の集計」の典型的な例です。まずカテゴリ別に平均を出し、その結果に対してさらにAVGを使います。

Step 1: カテゴリ別の平均価格を計算

※横にスクロールできます

— カテゴリ別の平均価格 SELECT category, AVG(price) as avg_price, COUNT(*) as product_count FROM products GROUP BY category;

Step 2: その結果に対して全体の統計を計算

※横にスクロールできます

— 完成形:カテゴリ別平均価格の統計 SELECT AVG(avg_price) as overall_avg_price, — 平均価格の平均 MAX(avg_price) as highest_category_avg, — 最も高いカテゴリの平均 MIN(avg_price) as lowest_category_avg, — 最も低いカテゴリの平均 COUNT(*) as category_count — カテゴリ数 FROM ( SELECT category, AVG(price) as avg_price, COUNT(*) as product_count FROM products GROUP BY category ) AS category_averages WHERE product_count >= 10; — 商品数10個以上のカテゴリのみ対象
クエリの解説:
  • サブクエリ: カテゴリごとの平均価格と商品数を計算
  • WHERE product_count >= 10 → 商品数が少ないカテゴリを除外
  • AVG(avg_price) → カテゴリ別平均価格の平均(= 全カテゴリの平均価格の平均)
  • MAX(avg_price) → 最も平均価格が高いカテゴリの値
  • MIN(avg_price) → 最も平均価格が低いカテゴリの値
実行結果のイメージ:
overall_avg_price highest_category_avg lowest_category_avg category_count
5,250.50 25,000.00 500.00 8

3-4. 例3:月別売上から増加月を抽出

やりたいこと:月別の売上を計算し、前月より売上が増加した月だけを表示する

これは少し複雑な例です。月別売上を計算した後、前月のデータと比較する必要があります。ここではウィンドウ関数のLAGを使います(ウィンドウ関数は後のステップで詳しく学びます)。

Step 1: 月別の売上を集計

※横にスクロールできます

— 月別の売上 SELECT STRFTIME(‘%Y-%m’, order_date) as order_month, SUM(total_amount) as monthly_sales FROM orders GROUP BY STRFTIME(‘%Y-%m’, order_date);
クエリの解説:
  • STRFTIME('%Y-%m', order_date) → 日付から「年-月」形式の文字列を取得(SQLiteの関数)
  • MySQLの場合は DATE_FORMAT(order_date, '%Y-%m') を使います

Step 2: LAGで前月のデータを取得

※横にスクロールできます

— 月別売上と前月の売上を並べる SELECT order_month, monthly_sales, LAG(monthly_sales) OVER (ORDER BY order_month) as previous_month_sales FROM ( SELECT STRFTIME(‘%Y-%m’, order_date) as order_month, SUM(total_amount) as monthly_sales FROM orders GROUP BY STRFTIME(‘%Y-%m’, order_date) ) AS monthly_totals;
LAG関数の解説:
  • LAG(monthly_sales) → 「1つ前の行」の monthly_sales を取得
  • OVER (ORDER BY order_month) → 月の順番で「前の行」を決定
  • 最初の月は「前月」がないので、NULL になる

Step 3: 完成形(増加月だけを抽出)

※横にスクロールできます

— 完成形:前月より売上が増加した月 SELECT order_month, monthly_sales, previous_month_sales, monthly_sales – previous_month_sales as sales_diff FROM ( SELECT order_month, monthly_sales, LAG(monthly_sales) OVER (ORDER BY order_month) as previous_month_sales FROM ( SELECT STRFTIME(‘%Y-%m’, order_date) as order_month, SUM(total_amount) as monthly_sales FROM orders GROUP BY STRFTIME(‘%Y-%m’, order_date) ) AS monthly_totals ) AS monthly_comparison WHERE previous_month_sales IS NOT NULL — 前月データがある月のみ AND monthly_sales > previous_month_sales — 売上が増加した月 ORDER BY order_month;
実行結果のイメージ:
order_month monthly_sales previous_month_sales sales_diff
2024-03 1,500,000 1,200,000 300,000
2024-05 1,800,000 1,400,000 400,000
📌 サブクエリの入れ子について

この例では、サブクエリの中にさらにサブクエリが入っています(2段階の入れ子)。複雑に見えますが、「内側から順番に実行される」と考えると理解しやすいです。内側のサブクエリの結果が、外側のサブクエリの入力になります。

🔗 4. 複数のサブクエリを結合する

4-1. サブクエリ同士をJOINする

FROM句には、複数のサブクエリを書いて、それらをJOINすることもできます。異なる集計を別々に行い、後で結合するパターンです。

これは「商品の統計」と「売上の統計」を別々に計算して、最後に合わせるような場合に便利です。

4-2. 例1:カテゴリ別の商品情報と売上情報を結合

やりたいこと:カテゴリ別の「商品数・平均価格」と「総売上・販売数量」を1つの表にまとめる

この2つの統計は、別々のテーブルから計算する必要があります。商品情報は products テーブルから、売上情報は order_details テーブルから取得します。

Step 1: カテゴリ別の商品情報を集計

※横にスクロールできます

— サブクエリ1: 商品情報を集計 SELECT category, COUNT(*) as product_count, AVG(price) as avg_price FROM products GROUP BY category;

Step 2: カテゴリ別の売上情報を集計

※横にスクロールできます

— サブクエリ2: 売上情報を集計 SELECT products.category, SUM(order_details.quantity * order_details.unit_price) as total_sales, SUM(order_details.quantity) as total_quantity FROM order_details INNER JOIN products ON order_details.product_id = products.product_id GROUP BY products.category;

Step 3: 2つのサブクエリを結合

※横にスクロールできます

— 完成形:2つのサブクエリをJOINで結合 SELECT product_info.category, product_info.product_count, product_info.avg_price, sales_info.total_sales, sales_info.total_quantity FROM ( — サブクエリ1: 商品情報を集計 SELECT category, COUNT(*) as product_count, AVG(price) as avg_price FROM products GROUP BY category ) AS product_info INNER JOIN ( — サブクエリ2: 売上情報を集計 SELECT products.category, SUM(order_details.quantity * order_details.unit_price) as total_sales, SUM(order_details.quantity) as total_quantity FROM order_details INNER JOIN products ON order_details.product_id = products.product_id GROUP BY products.category ) AS sales_info ON product_info.category = sales_info.category ORDER BY sales_info.total_sales DESC;
クエリの解説:
  • AS product_info → 商品情報のサブクエリに名前をつける
  • AS sales_info → 売上情報のサブクエリに名前をつける
  • INNER JOIN ... ON product_info.category = sales_info.category → カテゴリをキーに結合
  • それぞれのサブクエリの列を product_info.列名sales_info.列名 で参照
実行結果のイメージ:
category product_count avg_price total_sales total_quantity
家電 120 8,500 15,000,000 3,500
食品 250 1,200 12,000,000 8,000

4-3. 例2:都道府県別の顧客数と売上を結合(LEFT JOIN)

やりたいこと:都道府県別の顧客数と、売上を結合して表示する。売上がない都道府県も含める

※横にスクロールできます

SELECT customer_stats.prefecture, customer_stats.customer_count, COALESCE(order_stats.total_sales, 0) as total_sales, COALESCE(order_stats.order_count, 0) as order_count FROM ( — サブクエリ1: 都道府県別の顧客数 SELECT prefecture, COUNT(*) as customer_count FROM customers GROUP BY prefecture ) AS customer_stats LEFT JOIN ( — サブクエリ2: 都道府県別の売上 SELECT customers.prefecture, SUM(orders.total_amount) as total_sales, COUNT(orders.order_id) as order_count FROM orders INNER JOIN customers ON orders.customer_id = customers.customer_id GROUP BY customers.prefecture ) AS order_stats ON customer_stats.prefecture = order_stats.prefecture ORDER BY total_sales DESC;
クエリの解説:
  • LEFT JOIN → 左側(customer_stats)のデータはすべて残す
  • COALESCE(..., 0) → NULLの場合は0に置き換える
  • 注文がない都道府県も、顧客数は表示され、売上は0になる
なぜLEFT JOINを使うのか:

INNER JOINだと、売上がない都道府県は表示されません。「すべての都道府県を表示したい」場合はLEFT JOINを使います。

💡 複数サブクエリを結合するメリット
  • 処理を分離できる:それぞれの集計を独立して書ける
  • デバッグしやすい:各サブクエリを個別にテストできる
  • 柔軟な結合:INNER JOIN、LEFT JOIN、FULL JOINなど選べる
  • 読みやすい:「何を集計しているか」が明確になる

📦 5. サブクエリの入れ子構造

5-1. 入れ子(ネスト)とは

FROM句のサブクエリの中に、さらにサブクエリを入れることができます。これを入れ子(ネスト)と呼びます。

料理で例えると、「野菜を切る → 炒める → 味付けする」のように、段階的に処理を行うイメージです。

入れ子の構造イメージ:

最も内側のサブクエリから順番に実行され、その結果が外側のサブクエリに渡されます。

  1. 内側のサブクエリ(第1段階)が実行される
  2. その結果を使って、外側のサブクエリ(第2段階)が実行される
  3. さらにその結果を使って、メインクエリ(最終段階)が実行される

5-2. 例1:顧客ランク別の統計(2段階の入れ子)

やりたいこと:顧客を購入額でランク分け(VIP/プレミアム/一般)して、ランクごとの人数と平均購入額を表示する

これを実現するには、以下の3段階の処理が必要です。

  1. 顧客ごとの総購入額を計算
  2. 購入額に応じてランクを付ける
  3. ランクごとに集計する

第1段階: 顧客ごとの総購入額を計算

※横にスクロールできます

— 第1段階: 顧客別の購入額を集計 SELECT customers.customer_id, customers.customer_name, COALESCE(SUM(orders.total_amount), 0) as total_spent FROM customers LEFT JOIN orders ON customers.customer_id = orders.customer_id GROUP BY customers.customer_id, customers.customer_name;
クエリの解説:
  • LEFT JOIN → 注文がない顧客も含める
  • COALESCE(..., 0) → 注文がない場合(NULL)は0にする
第1段階の結果イメージ:
customer_id customer_name total_spent
1001 田中太郎 350,000
1002 鈴木花子 75,000
1003 佐藤次郎 5,000

第2段階: 購入額に応じてランクを付ける

※横にスクロールできます

— 第2段階: ランクを付ける SELECT customer_name, total_spent, CASE WHEN total_spent >= 100000 THEN ‘VIP’ WHEN total_spent >= 50000 THEN ‘プレミアム’ ELSE ‘一般’ END as customer_rank FROM ( — 第1段階のサブクエリ SELECT customers.customer_id, customers.customer_name, COALESCE(SUM(orders.total_amount), 0) as total_spent FROM customers LEFT JOIN orders ON customers.customer_id = orders.customer_id GROUP BY customers.customer_id, customers.customer_name ) AS customer_totals;
CASE文の解説:
  • CASE WHEN ... THEN ... END → 条件分岐を行う
  • total_spent >= 100000 → 10万円以上なら「VIP」
  • total_spent >= 50000 → 5万円以上なら「プレミアム」
  • ELSE '一般' → それ以外は「一般」
第2段階の結果イメージ:
customer_name total_spent customer_rank
田中太郎 350,000 VIP
鈴木花子 75,000 プレミアム
佐藤次郎 5,000 一般

第3段階(完成形): ランクごとに集計する

※横にスクロールできます

— 完成形: ランク別の統計 SELECT customer_rank, COUNT(*) as customer_count, ROUND(AVG(total_spent), 2) as avg_spent, ROUND(MIN(total_spent), 2) as min_spent, ROUND(MAX(total_spent), 2) as max_spent FROM ( — 第2段階: ランクを付ける SELECT customer_name, total_spent, CASE WHEN total_spent >= 100000 THEN ‘VIP’ WHEN total_spent >= 50000 THEN ‘プレミアム’ ELSE ‘一般’ END as customer_rank FROM ( — 第1段階: 顧客別の購入額を集計 SELECT customers.customer_id, customers.customer_name, COALESCE(SUM(orders.total_amount), 0) as total_spent FROM customers LEFT JOIN orders ON customers.customer_id = orders.customer_id GROUP BY customers.customer_id, customers.customer_name ) AS customer_totals ) AS ranked_customers GROUP BY customer_rank ORDER BY CASE customer_rank WHEN ‘VIP’ THEN 1 WHEN ‘プレミアム’ THEN 2 ELSE 3 END;
完成形の解説:
  • 最も内側: 顧客ごとの購入額を計算
  • 中間: CASE文でランクを付ける
  • 最も外側: ランクごとにGROUP BYで集計
  • ORDER BY CASE → VIP → プレミアム → 一般 の順で表示
最終結果のイメージ:
customer_rank customer_count avg_spent min_spent max_spent
VIP 15 150,000.50 100,500 350,000
プレミアム 85 72,500.30 50,100 99,800
一般 8,450 2,500.50 0 49,990
⚠️ 入れ子の深さに注意!

入れ子が深くなると、クエリが読みにくくなります。3段階までを目安にしましょう。それ以上深くなる場合は、以下の方法を検討してください:

  • WITH句(CTE)を使う(後のステップで学習)
  • ビュー(VIEW)を作成する
  • 一時テーブルを使う
  • 段階的に別々のクエリとして実行する

📝 6. 可読性を保つコツ

FROM句のサブクエリは強力ですが、複雑になりがちです。読みやすいコードを書くためのコツを学びましょう。

6-1. コツ1:適切にインデントする

インデント(字下げ)を使うと、サブクエリの構造が視覚的に分かりやすくなります。

※横にスクロールできます

— ❌ 悪い例(インデントなし、1行) SELECT * FROM (SELECT category, AVG(price) as avg_price FROM products GROUP BY category) AS stats WHERE avg_price >= 2000; — ✅ 良い例(適切にインデント) SELECT category, avg_price FROM ( SELECT category, AVG(price) as avg_price FROM products GROUP BY category ) AS category_stats WHERE avg_price >= 2000;
インデントのルール:
  • サブクエリの中身は、4スペース(またはタブ1つ)で字下げ
  • SELECT、FROM、WHERE、GROUP BYなどは行頭を揃える
  • 長い行は適切な位置で改行する

6-2. コツ2:分かりやすい別名をつける

サブクエリの別名は、その内容を表す分かりやすい名前にしましょう。

※横にスクロールできます

— ❌ 悪い例(何のデータか不明) SELECT * FROM ( SELECT * FROM products WHERE price > 1000 ) AS t1; SELECT * FROM ( SELECT category, AVG(price) FROM products GROUP BY category ) AS x; — ✅ 良い例(意味が明確) SELECT * FROM ( SELECT * FROM products WHERE price > 1000 ) AS high_price_products; SELECT * FROM ( SELECT category, AVG(price) as avg_price FROM products GROUP BY category ) AS category_price_stats;
💡 良い別名の例:
  • customer_stats → 顧客の統計
  • monthly_sales → 月別売上
  • top_products → 上位商品
  • category_averages → カテゴリ別平均
  • ranked_customers → ランク付けされた顧客

6-3. コツ3:コメントを活用する

複雑なクエリには、コメントで説明を加えましょう。

※横にスクロールできます

— 顧客の購入傾向を分析するクエリ SELECT customer_rank, COUNT(*) as customer_count FROM ( — 購入額でランク分け SELECT customer_name, CASE WHEN total_spent >= 100000 THEN ‘VIP’ WHEN total_spent >= 50000 THEN ‘プレミアム’ ELSE ‘一般’ END as customer_rank FROM ( — 顧客別の総購入額を計算 SELECT customers.customer_name, SUM(orders.total_amount) as total_spent FROM customers INNER JOIN orders ON customers.customer_id = orders.customer_id GROUP BY customers.customer_id, customers.customer_name ) AS customer_spending ) AS ranked_customers GROUP BY customer_rank;
コメントのコツ:
  • サブクエリの目的を簡潔に書く
  • 複雑な計算には何を計算しているかを書く
  • 全体の冒頭にクエリの目的を書く
  • ただし、過度なコメントは逆に読みにくくなるので注意

6-4. コツ4:必要な列だけを選択する

SELECT * は便利ですが、サブクエリでは必要な列だけを選択しましょう。

※横にスクロールできます

— ❌ 悪い例(不要な列も取得) SELECT * FROM ( SELECT * FROM products WHERE price > 1000 ) AS expensive; — ✅ 良い例(必要な列だけ) SELECT product_name, price, category FROM ( SELECT product_name, price, category FROM products WHERE price > 1000 ) AS expensive_products;
必要な列だけを選ぶメリット:
  • クエリが読みやすい:何のデータを使っているか明確
  • パフォーマンス向上:不要なデータを処理しなくて済む
  • エラー防止:列名の重複などの問題を避けられる

6-5. コツ5:複雑すぎる場合は分割を検討

クエリが複雑すぎる場合は、分割することを検討しましょう。

分割の方法:
  • WITH句(CTE):サブクエリに名前をつけて事前に定義(後のステップで学習)
  • ビュー(VIEW):よく使うサブクエリを保存しておく
  • 一時テーブル:中間結果をテーブルとして保存
  • 段階的実行:複数のクエリに分けて順番に実行
💡 可読性チェックリスト

クエリを書いたら、以下をチェックしましょう:

  • □ 適切にインデントされているか?
  • □ サブクエリの別名は分かりやすいか?
  • □ 複雑な部分にはコメントがあるか?
  • □ 必要な列だけを選択しているか?
  • □ 入れ子が深すぎないか(3段階以内)?
  • □ 他の人が読んでも理解できそうか?

📝 練習問題

FROM句のサブクエリを使って、実際に手を動かして確認しましょう!

問題 1基本

高額商品のトップ5

1000円以上の商品から、価格が高い順にトップ5を取得してください。FROM句のサブクエリを使用してください。

※横にスクロールできます

SELECT product_name, price, category FROM ( SELECT product_name, price, category FROM products WHERE price >= 1000 ) AS high_price_products ORDER BY price DESC LIMIT 5;

解説:

  • サブクエリで1000円以上の商品を抽出
  • AS high_price_products で別名をつける(必須)
  • メインクエリで価格順に並べてトップ5を取得

問題 2基本

カテゴリ別の平均価格

カテゴリ別の平均価格を計算し、平均価格が1500円以上のカテゴリだけを表示してください。

※横にスクロールできます

SELECT category, avg_price, product_count FROM ( SELECT category, AVG(price) as avg_price, COUNT(*) as product_count FROM products GROUP BY category ) AS category_stats WHERE avg_price >= 1500 ORDER BY avg_price DESC;

解説:

  • サブクエリでカテゴリ別に集計(AVG, COUNT)
  • メインクエリのWHEREで、集計結果(avg_price)に条件をつける
  • HAVINGでも同じことができるが、FROM句のサブクエリを使うと柔軟性が高い

問題 3応用

VIP顧客の抽出

顧客別の注文件数と総購入額を計算し、注文件数が10件以上かつ総購入額が50000円以上の顧客を抽出してください。

※横にスクロールできます

SELECT customer_name, order_count, total_spent FROM ( SELECT customers.customer_name, COUNT(orders.order_id) as order_count, SUM(orders.total_amount) as total_spent FROM customers LEFT JOIN orders ON customers.customer_id = orders.customer_id GROUP BY customers.customer_id, customers.customer_name ) AS customer_stats WHERE order_count >= 10 AND total_spent >= 50000 ORDER BY total_spent DESC;

解説:

  • サブクエリで顧客ごとにCOUNTとSUMで集計
  • LEFT JOINで注文がない顧客も含める
  • メインクエリで2つの条件(AND)で絞り込み

問題 4応用

利益率の高い商品

各商品の利益率((価格-原価)/原価 * 100)を計算し、利益率が30%以上の商品を抽出してください。

※横にスクロールできます

SELECT product_name, price, cost, profit_rate FROM ( SELECT product_name, price, cost, ROUND((price – cost) * 100.0 / cost, 2) as profit_rate FROM products WHERE cost > 0 ) AS profit_analysis WHERE profit_rate >= 30 ORDER BY profit_rate DESC;

解説:

  • WHERE cost > 0 → 0で割るエラーを防ぐ
  • * 100.0 → 小数点を含む計算にするため(整数同士の割り算を避ける)
  • ROUND(..., 2) → 小数点以下2桁に丸める

問題 5応用

カテゴリ別の商品数と売上

カテゴリ別の商品数と総売上を取得してください。2つのサブクエリを結合して実現してください。

※横にスクロールできます

SELECT product_stats.category, product_stats.product_count, COALESCE(sales_stats.total_sales, 0) as total_sales FROM ( — サブクエリ1: 商品数を集計 SELECT category, COUNT(*) as product_count FROM products GROUP BY category ) AS product_stats LEFT JOIN ( — サブクエリ2: 売上を集計 SELECT products.category, SUM(order_details.quantity * order_details.unit_price) as total_sales FROM order_details INNER JOIN products ON order_details.product_id = products.product_id GROUP BY products.category ) AS sales_stats ON product_stats.category = sales_stats.category ORDER BY total_sales DESC;

解説:

  • 2つのサブクエリで別々に集計(商品数と売上)
  • LEFT JOIN → 売上がないカテゴリも表示
  • COALESCE(..., 0) → NULLを0に置き換え

問題 6チャレンジ

月別売上の前月比

月別の売上を計算し、前月と比較して売上が増加した月だけを表示してください。

※横にスクロールできます

SELECT order_month, monthly_sales, previous_month_sales, monthly_sales – previous_month_sales as sales_diff FROM ( SELECT order_month, monthly_sales, LAG(monthly_sales) OVER (ORDER BY order_month) as previous_month_sales FROM ( SELECT STRFTIME(‘%Y-%m’, order_date) as order_month, SUM(total_amount) as monthly_sales FROM orders GROUP BY STRFTIME(‘%Y-%m’, order_date) ) AS monthly_totals ) AS monthly_comparison WHERE previous_month_sales IS NOT NULL AND monthly_sales > previous_month_sales ORDER BY order_month;

解説:

  • 第1段階: 月別売上を集計
  • 第2段階: LAG関数で前月の売上を取得
  • メインクエリ: 前月より増加した月だけを抽出
  • previous_month_sales IS NOT NULL → 最初の月(前月データなし)を除外

問題 7チャレンジ

顧客ランク別の統計

顧客を総購入額で「VIP(10万円以上)」「プレミアム(5万円以上)」「一般(5万円未満)」にランク分けし、ランクごとの顧客数と平均購入額を表示してください。

※横にスクロールできます

SELECT customer_rank, COUNT(*) as customer_count, ROUND(AVG(total_spent), 2) as avg_spent, ROUND(MIN(total_spent), 2) as min_spent, ROUND(MAX(total_spent), 2) as max_spent FROM ( SELECT customer_name, total_spent, CASE WHEN total_spent >= 100000 THEN ‘VIP’ WHEN total_spent >= 50000 THEN ‘プレミアム’ ELSE ‘一般’ END as customer_rank FROM ( SELECT customers.customer_name, COALESCE(SUM(orders.total_amount), 0) as total_spent FROM customers LEFT JOIN orders ON customers.customer_id = orders.customer_id GROUP BY customers.customer_id, customers.customer_name ) AS customer_totals ) AS ranked_customers GROUP BY customer_rank ORDER BY CASE customer_rank WHEN ‘VIP’ THEN 1 WHEN ‘プレミアム’ THEN 2 ELSE 3 END;

解説:

  • 第1段階: 顧客別の購入額を集計
  • 第2段階: CASE文でランクを付与
  • メインクエリ: ランクでGROUP BY
  • ORDER BY CASE → VIP、プレミアム、一般の順で表示

問題 8チャレンジ

都道府県別の顧客数と売上

都道府県別の顧客数、注文数、総売上を表示してください。売上が多い順に並べてください。

※横にスクロールできます

SELECT customer_stats.prefecture, customer_stats.customer_count, COALESCE(order_stats.order_count, 0) as order_count, COALESCE(order_stats.total_sales, 0) as total_sales FROM ( — 都道府県別の顧客数 SELECT prefecture, COUNT(*) as customer_count FROM customers GROUP BY prefecture ) AS customer_stats LEFT JOIN ( — 都道府県別の注文数と売上 SELECT customers.prefecture, COUNT(orders.order_id) as order_count, SUM(orders.total_amount) as total_sales FROM orders INNER JOIN customers ON orders.customer_id = customers.customer_id GROUP BY customers.prefecture ) AS order_stats ON customer_stats.prefecture = order_stats.prefecture ORDER BY total_sales DESC;

解説:

  • 2つのサブクエリで顧客統計と注文統計を別々に集計
  • LEFT JOINで全都道府県を表示(注文がない地域も含む)
  • COALESCEでNULLを0に変換

📝 Step 3 のまとめ

✅ 学んだこと
  • FROM句のサブクエリ(インラインビュー)を使える
  • サブクエリには必ず別名(AS)をつける
  • 集計結果を再利用して、さらに処理できる
  • 複数のサブクエリをJOINで結合できる
  • サブクエリを入れ子にして多段階の処理ができる
  • 可読性を保つためのコツを理解した
📌 FROM句のサブクエリの基本パターン

※横にスクロールできます

— 基本形 SELECT 列名 FROM ( SELECT 列名 FROM テーブル WHERE 条件 ) AS サブクエリ別名 WHERE 追加条件; — 複数のサブクエリを結合 SELECT * FROM (サブクエリ1) AS 別名1 JOIN (サブクエリ2) AS 別名2 ON 結合条件; — 入れ子(2段階) SELECT * FROM ( SELECT * FROM ( サブクエリ ) AS 内側の別名 ) AS 外側の別名;
💡 実務での使いどころ
  • 集計の集計: カテゴリ別平均の全体平均など
  • 段階的な絞り込み: 複雑な条件を分解して処理
  • 複数統計の結合: 異なる集計結果を組み合わせる
  • 計算列へのフィルタ: 利益率など計算した値で絞り込む
  • ランキング処理: トップNの抽出、ランク別統計
⚠️ 注意点
  • サブクエリには必ず別名をつける(つけないとエラー)
  • 入れ子は3段階までを目安に
  • インデントで構造を分かりやすくする
  • 必要な列だけを選択する
  • 複雑すぎる場合は分割を検討(WITH句、ビューなど)

❓ よくある質問

Q1: サブクエリに別名をつけないとどうなりますか?

エラーになります!「Every derived table must have its own alias」というエラーメッセージが表示されます。FROM句のサブクエリには、必ず AS 別名 をつけてください。

Q2: HAVINGとFROM句のサブクエリの使い分けは?

シンプルな条件ならHAVING複雑な条件や追加の処理が必要ならFROM句のサブクエリを使います。

  • HAVING: HAVING COUNT(*) >= 5 のようなシンプルな条件
  • FROM句のサブクエリ: 計算列を使う、集計結果をさらに集計する、複数のサブクエリを結合する場合

Q3: サブクエリの入れ子はどこまで深くできますか?

技術的には制限はありませんが、読みやすさのため3段階までを推奨します。それ以上深くなる場合は、WITH句(CTE)やビューを使いましょう。

Q4: パフォーマンスは大丈夫ですか?

適切に使えば問題ありません。ただし、以下の点に注意してください:

  • 不要な列を取得しない(SELECT * を避ける)
  • 深い入れ子を避ける
  • 大量データの場合はインデックスを活用する
  • 遅い場合は実行計画を確認する(後のステップで学習)

Q5: WITH句(CTE)とFROM句のサブクエリの違いは?

WITH句(CTE)は、サブクエリに名前をつけて事前に定義できる機能です。

  • FROM句のサブクエリ: 1回だけ使う場合に便利
  • WITH句: 複数箇所で使う場合、複雑なクエリを整理したい場合に便利

WITH句は後のステップで詳しく学びます!

Q6: JOINとFROM句のサブクエリはどう違いますか?

どちらも複数のデータを組み合わせる方法ですが、用途が異なります。

  • JOIN: 既存のテーブル同士を結合
  • FROM句のサブクエリ: 集計結果を一時的なテーブルとして使う
  • 両方を組み合わせることもできます(サブクエリ同士をJOIN)
🎓 次のステップでは

Step 4: 相関サブクエリでは、メインクエリの各行を参照しながら実行されるサブクエリを学びます。「各カテゴリの平均価格より高い商品」のような、行ごとに異なる条件を使った検索ができるようになります!

📝

学習メモ

SQL応用・パフォーマンス最適化 - Step 3

📋 過去のメモ一覧
#artnasekai #学習メモ
LINE