SELECT
product_name,
price,
(SELECT AVG(price) FROM products) AS avg_price,
price – (SELECT AVG(price) FROM products) AS diff_from_avg
FROM products
ORDER BY diff_from_avg DESC
LIMIT 5;
クエリの解説:
(SELECT AVG(price) FROM products) → 平均価格を取得
price - (SELECT AVG(price) FROM products) → 各商品の価格から平均を引く
AS diff_from_avg → 差額に名前をつける
ORDER BY diff_from_avg DESC → 差額が大きい順(平均より高い順)に並べる
SELECT
product_name,
price,
(SELECT MAX(price) FROM products) AS max_price,
(SELECT MIN(price) FROM products) AS min_price,
(SELECT MAX(price) FROM products) – price AS diff_from_max
FROM products
ORDER BY price DESC
LIMIT 5;
SELECT
product_name,
price,
(SELECT SUM(price) FROM products) AS total_price,
ROUND(price * 100.0 / (SELECT SUM(price) FROM products), 2) AS percentage
FROM products
ORDER BY percentage DESC
LIMIT 5;
SELECT
customer_name,
(
SELECT COUNT(*)
FROM orders o
WHERE o.customer_id = customers.customer_id
) AS order_count,
(
SELECT SUM(total_amount)
FROM orders o
WHERE o.customer_id = customers.customer_id
) AS total_spent
FROM customers
ORDER BY total_spent DESC NULLS LAST
LIMIT 10;
クエリの解説:
WHERE o.customer_id = customers.customer_id → 相関部分。外側の顧客と同じcustomer_idの注文を対象にする
COUNT(*) → その顧客の注文件数をカウント
SUM(total_amount) → その顧客の総購入額を計算
NULLS LAST → NULLを最後に表示(注文がない顧客を最後に)
結果イメージ:
customer_name
order_count
total_spent
田中太郎
25
350,000
鈴木花子
18
220,000
佐藤次郎
5
45,000
3-4. 例3:商品ごとの販売実績
やりたいこと:各商品の名前と、その商品の販売回数・総売上を表示する
※横にスクロールできます
SELECT
p.product_name,
p.price,
(
SELECT COUNT(*)
FROM order_details od
WHERE od.product_id = p.product_id
) AS sales_count,
(
SELECT SUM(od.quantity * od.unit_price)
FROM order_details od
WHERE od.product_id = p.product_id
) AS total_sales
FROM products p
ORDER BY total_sales DESC NULLS LAST
LIMIT 10;
クエリの解説:
WHERE od.product_id = p.product_id → 外側の商品と同じproduct_idの注文明細を対象にする
SELECT
c.customer_name,
o.order_date,
o.total_amount,
(
SELECT COUNT(*)
FROM orders o2
WHERE o2.customer_id = c.customer_id
) AS total_order_count,
(
SELECT SUM(total_amount)
FROM orders o3
WHERE o3.customer_id = c.customer_id
) AS total_spent
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
ORDER BY o.order_date DESC
LIMIT 10;
クエリの解説:
INNER JOIN orders o ON ... → customersとordersを結合
各注文行に対して、その顧客の「累計注文回数」と「累計購入額」を追加
結合結果の各行に、スカラーサブクエリで計算した値が追加される
結果イメージ:
customer_name
order_date
total_amount
total_order_count
total_spent
田中太郎
2024-03-15
15,000
25
350,000
鈴木花子
2024-03-14
8,000
18
220,000
田中太郎
2024-03-10
12,000
25
350,000
同じ顧客(田中太郎)の複数の注文が表示されていますが、累計は同じ値になっています。
4-2. 最終購入日を追加
やりたいこと:各顧客の情報と、最終購入日を表示する
※横にスクロールできます
SELECT
customer_name,
email,
(
SELECT MAX(order_date)
FROM orders o
WHERE o.customer_id = customers.customer_id
) AS last_order_date
FROM customers
ORDER BY last_order_date DESC NULLS LAST;
クエリの解説:
MAX(order_date) → その顧客の最新の注文日を取得
注文がない顧客は last_order_date がNULLになる
NULLS LAST → 注文がない顧客を最後に表示
活用シーン:
休眠顧客の特定(最終購入日が古い顧客)
アクティブ顧客のリスト作成
顧客の購買サイクルの分析
4-3. 複数の統計を一度に追加
やりたいこと:各商品の情報と、販売回数、総売上、平均購入額を表示する
※横にスクロールできます
SELECT
p.product_name,
p.category,
p.price,
(
SELECT COUNT(*)
FROM order_details od
WHERE od.product_id = p.product_id
) AS sales_count,
(
SELECT SUM(od.quantity * od.unit_price)
FROM order_details od
WHERE od.product_id = p.product_id
) AS total_sales,
(
SELECT AVG(od.quantity * od.unit_price)
FROM order_details od
WHERE od.product_id = p.product_id
) AS avg_sales_per_order
FROM products p
ORDER BY total_sales DESC NULLS LAST
LIMIT 10;
SELECT
product_name,
price,
(SELECT AVG(price) FROM products) AS avg_price,
CASE
WHEN price >= (SELECT AVG(price) FROM products)
THEN ‘高価格’
ELSE ‘低価格’
END AS price_category
FROM products
ORDER BY price DESC
LIMIT 10;
クエリの解説:
CASE WHEN ... THEN ... ELSE ... END → 条件分岐
WHEN price >= (SELECT AVG(price) FROM products) → 平均価格以上かどうかを判定
THEN '高価格' → 条件を満たす場合は「高価格」
ELSE '低価格' → それ以外は「低価格」
結果イメージ:
product_name
price
avg_price
price_category
高級メロン
5,000
324.5
高価格
りんご
150
324.5
低価格
5-2. 複数段階の分類
やりたいこと:価格を平均の倍率で5段階に分類する
※横にスクロールできます
SELECT
product_name,
price,
CASE
WHEN price >= (SELECT AVG(price) * 2 FROM products)
THEN ‘超高級品’
WHEN price >= (SELECT AVG(price) * 1.5 FROM products)
THEN ‘高級品’
WHEN price >= (SELECT AVG(price) FROM products)
THEN ‘標準より高め’
WHEN price >= (SELECT AVG(price) * 0.5 FROM products)
THEN ‘標準’
ELSE ‘お買い得’
END AS price_rank
FROM products
ORDER BY price DESC
LIMIT 10;
SELECT
customer_name,
(
SELECT SUM(total_amount)
FROM orders o
WHERE o.customer_id = customers.customer_id
) AS total_spent,
CASE
WHEN (
SELECT SUM(total_amount)
FROM orders o
WHERE o.customer_id = customers.customer_id
) >= 100000 THEN ‘プラチナ’
WHEN (
SELECT SUM(total_amount)
FROM orders o
WHERE o.customer_id = customers.customer_id
) >= 50000 THEN ‘ゴールド’
WHEN (
SELECT SUM(total_amount)
FROM orders o
WHERE o.customer_id = customers.customer_id
) >= 10000 THEN ‘シルバー’
ELSE ‘ブロンズ’
END AS customer_rank
FROM customers
ORDER BY total_spent DESC NULLS LAST;
SELECT
customer_name,
(
SELECT MAX(order_date)
FROM orders o
WHERE o.customer_id = customers.customer_id
) AS last_order_date,
CASE
WHEN (
SELECT MAX(order_date)
FROM orders o
WHERE o.customer_id = customers.customer_id
) >= DATE(‘now’, ‘-30 days’) THEN ‘アクティブ’
WHEN (
SELECT MAX(order_date)
FROM orders o
WHERE o.customer_id = customers.customer_id
) >= DATE(‘now’, ‘-90 days’) THEN ‘休眠気味’
WHEN (
SELECT MAX(order_date)
FROM orders o
WHERE o.customer_id = customers.customer_id
) IS NOT NULL THEN ‘休眠中’
ELSE ‘未購入’
END AS activity_status
FROM customers
ORDER BY last_order_date DESC NULLS LAST;
SELECT
customer_name,
(SELECT SUM(total_amount) FROM orders WHERE customer_id = customers.customer_id) AS total,
CASE
WHEN (SELECT SUM(total_amount) FROM orders WHERE customer_id = customers.customer_id) >= 100000 THEN ‘VIP’
WHEN (SELECT SUM(total_amount) FROM orders WHERE customer_id = customers.customer_id) >= 50000 THEN ‘優良’
ELSE ‘一般’
END AS rank
FROM customers;
WITH customer_totals AS (
— 顧客ごとの総購入額を1回だけ計算
SELECT
customer_id,
SUM(total_amount) AS total_spent
FROM orders
GROUP BY customer_id
)
SELECT
c.customer_name,
COALESCE(ct.total_spent, 0) AS total_spent,
CASE
WHEN ct.total_spent >= 100000 THEN ‘VIP’
WHEN ct.total_spent >= 50000 THEN ‘優良’
ELSE ‘一般’
END AS customer_rank
FROM customers c
LEFT JOIN customer_totals ct ON c.customer_id = ct.customer_id
ORDER BY total_spent DESC;
クエリの解説:
WITH customer_totals AS (...) → 一時的なテーブル「customer_totals」を作成
サブクエリの計算は1回だけ実行される
LEFT JOIN customer_totals ct → その結果をJOINで使う
COALESCE(ct.total_spent, 0) → 注文がない顧客は0として扱う
メリット:
同じ計算を1回だけ実行 → 高速
コードが読みやすくなる
同じ結果を複数の場所で使える
6-3. 解決策2:JOINに置き換える
スカラーサブクエリをJOIN + GROUP BYに置き換えると、パフォーマンスが向上することがあります。
❌ 遅い可能性がある書き方(スカラーサブクエリ)
※横にスクロールできます
SELECT
c.customer_name,
(SELECT COUNT(*) FROM orders WHERE customer_id = c.customer_id) AS order_count,
(SELECT SUM(total_amount) FROM orders WHERE customer_id = c.customer_id) AS total
FROM customers c;
✅ 速い書き方(JOIN + GROUP BY)
※横にスクロールできます
SELECT
c.customer_name,
COUNT(o.order_id) AS order_count,
SUM(o.total_amount) AS total
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name
ORDER BY total DESC NULLS LAST;
なぜJOINの方が速いのか:
スカラーサブクエリ: 顧客数 × サブクエリ数 回の計算が実行される
JOIN: 1回のJOINと1回のGROUP BYで完了
データベースの最適化機能が効きやすい
6-4. 解決策3:EXISTSを使う(存在チェック)
「存在するかどうか」だけをチェックしたい場合は、EXISTSが最適です。
※横にスクロールできます
SELECT
customer_name,
CASE
WHEN EXISTS (
SELECT 1 FROM orders
WHERE customer_id = customers.customer_id
) THEN ‘購入あり’
ELSE ‘未購入’
END AS purchase_status
FROM customers;
クエリの解説:
EXISTS (...) → サブクエリの結果が1件でもあればTRUE
1件見つかった時点で処理を終了するので効率的
COUNTで件数を数えるより速い
6-5. 最適化の判断基準
📊 どの方法を使うべきか?
状況
推奨方法
理由
データ量が少ない(数百件)
スカラーサブクエリ
読みやすさを優先
データ量が多い(数万件以上)
JOIN + GROUP BY
パフォーマンス重視
同じ計算を複数回使う
WITH句(CTE)
計算を1回に減らせる
存在チェックのみ
EXISTS
最も効率的
行ごとに異なる計算が必要
相関スカラーサブクエリ
他の方法では難しい
💡 実務でのアドバイス
まずは読みやすさを優先してコードを書く
遅いと感じたら最適化を検討する
EXPLAINで実行計画を確認する(後のステップで学習)
同じ結果を得られる複数の書き方を知っておくことが大切
💼 7. 実務での活用パターン
7-1. ダッシュボード用の顧客統計
実務では、顧客ごとの詳細な統計をダッシュボードに表示することがよくあります。
※横にスクロールできます
— 顧客ダッシュボード用のデータ
SELECT
c.customer_name,
c.prefecture,
(
SELECT COUNT(*)
FROM orders o
WHERE o.customer_id = c.customer_id
) AS order_count,
(
SELECT SUM(total_amount)
FROM orders o
WHERE o.customer_id = c.customer_id
) AS total_spent,
(
SELECT AVG(total_amount)
FROM orders o
WHERE o.customer_id = c.customer_id
) AS avg_order_amount,
(
SELECT MAX(order_date)
FROM orders o
WHERE o.customer_id = c.customer_id
) AS last_order_date
FROM customers c
ORDER BY total_spent DESC NULLS LAST
LIMIT 20;
活用シーン:
顧客管理システムのダッシュボード
営業担当者向けの顧客リスト
マーケティング分析用のデータ抽出
7-2. 商品分析レポート
※横にスクロールできます
— 商品分析レポート
SELECT
p.product_name,
p.category,
p.price,
p.stock,
(
SELECT COUNT(*)
FROM order_details od
WHERE od.product_id = p.product_id
) AS sales_count,
(
SELECT SUM(od.quantity)
FROM order_details od
WHERE od.product_id = p.product_id
) AS total_quantity_sold,
(
SELECT AVG(p2.price)
FROM products p2
WHERE p2.category = p.category
) AS category_avg_price,
CASE
WHEN p.stock = 0 THEN ‘在庫切れ’
WHEN p.stock < 10 THEN '在庫少'
ELSE '在庫あり'
END AS stock_status
FROM products p
ORDER BY sales_count DESC NULLS LAST
LIMIT 20;
活用シーン:
在庫管理レポート
商品の売れ筋分析
発注判断のためのデータ
7-3. 比較分析レポート
※横にスクロールできます
— 全体平均とカテゴリ平均との比較
SELECT
product_name,
category,
price,
(SELECT AVG(price) FROM products) AS overall_avg,
(
SELECT AVG(price)
FROM products p2
WHERE p2.category = products.category
) AS category_avg,
ROUND(
price * 100.0 / (SELECT AVG(price) FROM products),
1
) AS pct_of_overall,
ROUND(
price * 100.0 / (
SELECT AVG(price)
FROM products p2
WHERE p2.category = products.category
),
1
) AS pct_of_category
FROM products
ORDER BY category, price DESC;
クエリの解説:
pct_of_overall → 全体平均に対する割合(%)
pct_of_category → カテゴリ平均に対する割合(%)
100%を超えていれば平均より高い、100%未満なら平均より低い
📝 練習問題
スカラーサブクエリを使いこなせるようになりましょう!
問題 1基本
平均価格の表示
全商品の情報と、全商品の平均価格を同時に表示してください。
※横にスクロールできます
SELECT
product_name,
price,
(SELECT AVG(price) FROM products) AS avg_price
FROM products
LIMIT 10;
解説:
SELECT AVG(price) FROM products → 全商品の平均価格を計算
この値は全ての行に同じ値で表示される
スカラーサブクエリは必ず1つの値を返す
問題 2基本
最高価格との差
各商品の価格と、最高価格との差額を表示してください。
※横にスクロールできます
SELECT
product_name,
price,
(SELECT MAX(price) FROM products) AS max_price,
(SELECT MAX(price) FROM products) – price AS diff_from_max
FROM products
ORDER BY price DESC;
解説:
MAX(price) → 最高価格を取得
MAX(price) - price → 最高価格からその商品の価格を引く
最高価格の商品は差が0になる
問題 3基本
商品の販売回数
各商品の名前と、その商品が注文された回数を表示してください。
※横にスクロールできます
SELECT
product_name,
price,
(
SELECT COUNT(*)
FROM order_details od
WHERE od.product_id = products.product_id
) AS sales_count
FROM products
ORDER BY sales_count DESC;
解説:
WHERE od.product_id = products.product_id → 相関部分
外側の各商品に対して、その商品の注文件数をカウント
商品ごとに異なる値が計算される
問題 4応用
顧客の注文統計
各顧客の名前、注文回数、総購入額を表示してください。
※横にスクロールできます
SELECT
customer_name,
(
SELECT COUNT(*)
FROM orders o
WHERE o.customer_id = customers.customer_id
) AS order_count,
(
SELECT SUM(total_amount)
FROM orders o
WHERE o.customer_id = customers.customer_id
) AS total_spent
FROM customers
ORDER BY total_spent DESC NULLS LAST;
解説:
2つの相関スカラーサブクエリで異なる統計を計算
COUNT(*) → 注文件数
SUM(total_amount) → 総購入額
NULLS LAST → 注文がない顧客を最後に表示
問題 5応用
カテゴリ平均との比較
各商品の価格と、同じカテゴリの平均価格を表示してください。
※横にスクロールできます
SELECT
p1.product_name,
p1.category,
p1.price,
(
SELECT AVG(p2.price)
FROM products p2
WHERE p2.category = p1.category
) AS category_avg_price
FROM products p1
ORDER BY category, price DESC;
解説:
WHERE p2.category = p1.category → 同じカテゴリの商品だけを対象
果物は果物の平均、肉は肉の平均…と、カテゴリごとに異なる平均が表示される
問題 6応用
価格帯の判定
各商品が平均価格以上なら「高価格」、未満なら「低価格」と表示してください。
※横にスクロールできます
SELECT
product_name,
price,
CASE
WHEN price >= (SELECT AVG(price) FROM products)
THEN ‘高価格’
ELSE ‘低価格’
END AS price_category
FROM products
ORDER BY price DESC;
解説:
CASE WHEN ... THEN ... ELSE ... END → 条件分岐
サブクエリの結果(平均価格)と比較して分類
問題 7応用
最終購入日の表示
各顧客の名前と、その顧客の最終購入日を表示してください。
※横にスクロールできます
SELECT
customer_name,
(
SELECT MAX(order_date)
FROM orders o
WHERE o.customer_id = customers.customer_id
) AS last_order_date
FROM customers
ORDER BY last_order_date DESC NULLS LAST;
SELECT
customer_name,
(
SELECT SUM(total_amount)
FROM orders o
WHERE o.customer_id = customers.customer_id
) AS total_spent,
CASE
WHEN (
SELECT SUM(total_amount)
FROM orders o
WHERE o.customer_id = customers.customer_id
) >= 100000 THEN ‘VIP’
WHEN (
SELECT SUM(total_amount)
FROM orders o
WHERE o.customer_id = customers.customer_id
) >= 50000 THEN ‘優良’
ELSE ‘一般’
END AS customer_rank
FROM customers
ORDER BY total_spent DESC NULLS LAST;
解説:
同じサブクエリを複数回書いている(非効率)
実務ではWITH句(CTE)を使って最適化することを推奨
WITH句を使った最適化版:
※横にスクロールできます
WITH customer_totals AS (
SELECT customer_id, SUM(total_amount) AS total_spent
FROM orders
GROUP BY customer_id
)
SELECT
c.customer_name,
COALESCE(ct.total_spent, 0) AS total_spent,
CASE
WHEN ct.total_spent >= 100000 THEN ‘VIP’
WHEN ct.total_spent >= 50000 THEN ‘優良’
ELSE ‘一般’
END AS customer_rank
FROM customers c
LEFT JOIN customer_totals ct ON c.customer_id = ct.customer_id
ORDER BY total_spent DESC;
問題 9チャレンジ
全体比率の計算
各商品の価格が、全商品の平均価格の何%かを表示してください。
※横にスクロールできます
SELECT
product_name,
price,
(SELECT AVG(price) FROM products) AS avg_price,
ROUND(
price * 100.0 / (SELECT AVG(price) FROM products),
1
) AS pct_of_avg
FROM products
ORDER BY pct_of_avg DESC;
解説:
price * 100.0 / ... → パーセンテージを計算
ROUND(..., 1) → 小数点以下1桁に丸める
100%なら平均と同じ、200%なら平均の2倍
問題 10チャレンジ
複合統計レポート
各商品の名前、価格、販売回数、総売上、平均購入額を表示してください。
※横にスクロールできます
SELECT
product_name,
price,
(
SELECT COUNT(*)
FROM order_details od
WHERE od.product_id = products.product_id
) AS sales_count,
(
SELECT SUM(od.quantity * od.unit_price)
FROM order_details od
WHERE od.product_id = products.product_id
) AS total_sales,
(
SELECT AVG(od.quantity * od.unit_price)
FROM order_details od
WHERE od.product_id = products.product_id
) AS avg_sales_per_order
FROM products
ORDER BY total_sales DESC NULLS LAST
LIMIT 10;
解説:
3つの統計を相関スカラーサブクエリで計算
実務でよく使うダッシュボード用のパターン
📝 Step 5 のまとめ
✅ 学んだこと
スカラーサブクエリ: SELECT句でサブクエリを使える
1つの値を返す: 必ず1行1列の結果
相関スカラーサブクエリ: 外側のテーブルを参照して、行ごとに異なる値を計算
CASE式との組み合わせ: 動的な条件分岐が可能
最適化テクニック: WITH句(CTE)やJOINで高速化
実務パターン: ダッシュボードや分析レポートの作成
📌 スカラーサブクエリの基本パターン
※横にスクロールできます
— 基本形(全行に同じ値)
SELECT
列名,
(SELECT 集計関数 FROM テーブル) AS 別名
FROM テーブル;
— 相関スカラーサブクエリ(行ごとに異なる値)
SELECT
t1.列名,
(
SELECT 集計関数
FROM テーブル t2
WHERE t2.列名 = t1.列名
) AS 別名
FROM テーブル t1;
— CASE式との組み合わせ
SELECT
列名,
CASE
WHEN 値 >= (SELECT AVG(列名) FROM テーブル) THEN ‘ラベルA’
ELSE ‘ラベルB’
END AS 分類
FROM テーブル;