0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【SQL実践ドリル 第4回】サブクエリ ― クエリの中にクエリを書く

0
Posted at

はじめに

【SQL実践ドリル】 シリーズ第4回では、サブクエリ(副問い合わせ) を扱います。サブクエリとは、SQL文の中に埋め込まれた別のSELECT文のことです。「平均より高い給与の社員を抽出したい」「特定の条件を満たす部署だけ取得したい」といった、単純なSELECTでは書けないクエリを実現できます。

このシリーズでは共通のサンプルデータベースを使用します。初回(第1回)でCREATE TABLE文を掲載しています。

サンプルデータの概要

テーブル 件数 主な列
departments 5件 department_id, department_name, location
employees 10件 employee_id, name, department_id, hire_date, salary, manager_id
customers 5件 customer_id, customer_name, email, prefecture, registered_date
products 8件 product_id, product_name, category, price, stock
orders 10件 order_id, customer_id, employee_id, order_date
order_details 20件 detail_id, order_id, product_id, quantity

サブクエリの分類

サブクエリは使い方によっていくつかの種類に分類されます。

1. スカラーサブクエリ

1行1列の値を返すサブクエリです。SELECT句・WHERE句・HAVING句など、単一の値が必要な場所に書けます。

-- 全社員の平均給与を取得するスカラーサブクエリ
SELECT name, salary,
       (SELECT AVG(salary) FROM employees) AS avg_salary
FROM employees;

2. テーブルサブクエリ(FROM句サブクエリ)

FROM句に書いて仮想テーブル(導出テーブル) として使います。MySQLではエイリアスが必須です。

SELECT sub.department_id, sub.avg_salary
FROM (
    SELECT department_id, AVG(salary) AS avg_salary
    FROM employees
    WHERE department_id IS NOT NULL
    GROUP BY department_id
) AS sub
WHERE sub.avg_salary > 330000;

3. 相関サブクエリ

外側のクエリの値を参照するサブクエリです。外側の行ごとにサブクエリが評価されます。

-- 自部署の平均より高い給与の社員
SELECT e1.name, e1.salary
FROM employees e1
WHERE e1.salary > (
    SELECT AVG(e2.salary)
    FROM employees e2
    WHERE e2.department_id = e1.department_id
);

4. EXISTS / NOT EXISTS

サブクエリの結果が存在するかどうかで絞り込みます。相関サブクエリと組み合わせて使うのが一般的です。

-- 注文実績がある顧客
SELECT c.customer_name
FROM customers c
WHERE EXISTS (
    SELECT 1 FROM orders o
    WHERE o.customer_id = c.customer_id
);

5. IN サブクエリ

サブクエリが返す値のリストに含まれるかどうかで絞り込みます。

-- 東京にある部署の社員
SELECT name FROM employees
WHERE department_id IN (
    SELECT department_id FROM departments WHERE location = '東京'
);

MySQL と PostgreSQL の違い

  • 基本的なサブクエリの構文は両DBで共通です。
  • MySQL 8.0以降ではラテラル導出テーブルLATERAL)がサポートされています。PostgreSQL では以前から利用可能です。
  • MySQLのFROM句サブクエリには必ずエイリアスが必要です。PostgreSQLでも付けるのが推奨されます。

問題

問1 ⭐(基本):全社員の平均給与を求める

全社員の平均給与を求めてください。結果の列名は avg_salary としてください。

期待される結果:

avg_salary
338000.0000
模範解答
SELECT AVG(salary) AS avg_salary
FROM employees;

解説:

全10名の給与合計は 350000 + 420000 + 300000 + 380000 + 450000 + 280000 + 330000 + 310000 + 270000 + 290000 = 3380000 です。3380000 / 10 = 338000 となります。この値を次の問題でサブクエリとして利用します。


問2 ⭐(基本):平均給与より高い給与の社員を取得する

スカラーサブクエリを使い、全社員の平均給与(338000)より高い給与の社員の employee_idnamesalary を取得してください。給与の降順で並べてください。

期待される結果:

employee_id name salary
5 伊藤健太 450000
2 佐藤花子 420000
4 高橋美咲 380000
1 田中太郎 350000
模範解答
SELECT employee_id, name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC;

解説:

サブクエリ (SELECT AVG(salary) FROM employees) は 338000 を返します。350000、420000、380000、450000 の4人がこの値を超えています。


問3 ⭐(基本):INサブクエリ ― 東京にある部署の社員

INサブクエリを使い、所在地が「東京」の部署に所属する社員の employee_idnamedepartment_id を取得してください。employee_id の昇順で並べてください。

期待される結果:

employee_id name department_id
1 田中太郎 1
3 鈴木一郎 1
4 高橋美咲 3
8 中村真理 1
9 小林誠 3
模範解答
SELECT employee_id, name, department_id
FROM employees
WHERE department_id IN (
    SELECT department_id
    FROM departments
    WHERE location = '東京'
)
ORDER BY employee_id;

解説:

東京にある部署は営業部(department_id = 1)と人事部(department_id = 3)です。これらの部署に所属する社員は5名です。加藤優子(employee_id = 10)は department_id が NULL のため、IN の結果に含まれません(NULLはINで一致しません)。


問4 ⭐⭐(応用):各社員の給与と所属部署の平均給与を比較する

相関サブクエリを使い、全社員について namesalarydepartment_name、所属部署の平均給与(dept_avg_salary)を取得してください。employee_id の昇順で並べてください。department_id が NULL の社員の department_namedept_avg_salary は NULL としてください。

期待される結果:

name salary department_name dept_avg_salary
田中太郎 350000 営業部 320000.0000
佐藤花子 420000 開発部 383333.3333
鈴木一郎 300000 営業部 320000.0000
高橋美咲 380000 人事部 325000.0000
伊藤健太 450000 開発部 383333.3333
渡辺さくら 280000 開発部 383333.3333
山本大輔 330000 経理部 330000.0000
中村真理 310000 営業部 320000.0000
小林誠 270000 人事部 325000.0000
加藤優子 290000 NULL NULL
模範解答
SELECT
    e.name,
    e.salary,
    d.department_name,
    (SELECT AVG(e2.salary)
     FROM employees e2
     WHERE e2.department_id = e.department_id) AS dept_avg_salary
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
ORDER BY e.employee_id;

解説:

相関サブクエリは外側の e.department_id を参照し、同じ部署の平均給与を計算します。各部署の平均給与は以下のとおりです。

  • 営業部(dept 1): (350000 + 300000 + 310000) / 3 = 320000
  • 開発部(dept 2): (420000 + 450000 + 280000) / 3 ≒ 383333.3333
  • 人事部(dept 3): (380000 + 270000) / 2 = 325000
  • 経理部(dept 4): 330000 / 1 = 330000

加藤優子は department_id が NULL なので、サブクエリの WHERE e2.department_id = NULL は常に偽となり、AVG は NULL を返します。


問5 ⭐⭐(応用):各部署で最も給与が高い社員を取得する

相関サブクエリを使い、各部署で給与が最も高い社員の namesalarydepartment_name を取得してください。department_id の昇順で並べてください(department_id が NULL の社員は除外します)。

期待される結果:

name salary department_name
田中太郎 350000 営業部
伊藤健太 450000 開発部
高橋美咲 380000 人事部
山本大輔 330000 経理部
模範解答
SELECT e.name, e.salary, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.salary = (
    SELECT MAX(e2.salary)
    FROM employees e2
    WHERE e2.department_id = e.department_id
)
ORDER BY e.department_id;

解説:

相関サブクエリで各部署の最大給与を取得し、それに一致する社員を抽出します。JOIN を使用しているため department_id が NULL の加藤優子は除外されます。各部署の最大給与は、営業部: 350000(田中太郎)、開発部: 450000(伊藤健太)、人事部: 380000(高橋美咲)、経理部: 330000(山本大輔)です。


問6 ⭐⭐(応用):平均注文金額を超える注文を取得する

FROM句サブクエリを使い、注文ごとの合計金額が全注文の平均合計金額を超える注文の order_idorder_datetotal_amount を取得してください。total_amount の降順で並べてください。

期待される結果:

order_id order_date total_amount
1 2024-07-01 413500
10 2024-11-01 295000
8 2024-10-01 288000
4 2024-08-01 224000
7 2024-09-10 202000
模範解答
SELECT o.order_id, o.order_date, order_totals.total_amount
FROM orders o
JOIN (
    SELECT od.order_id, SUM(p.price * od.quantity) AS total_amount
    FROM order_details od
    JOIN products p ON od.product_id = p.product_id
    GROUP BY od.order_id
) AS order_totals ON o.order_id = order_totals.order_id
WHERE order_totals.total_amount > (
    SELECT AVG(sub.total_amount)
    FROM (
        SELECT od.order_id, SUM(p.price * od.quantity) AS total_amount
        FROM order_details od
        JOIN products p ON od.product_id = p.product_id
        GROUP BY od.order_id
    ) AS sub
)
ORDER BY order_totals.total_amount DESC;

解説:

各注文の合計金額は以下のとおりです。

order_id 内訳 total_amount
1 198000×2 + 3500×5 413500
2 45000×1 + 12000×3 81000
3 89000×1 + 4800×2 98600
4 198000×1 + 6500×4 224000
5 3500×10 + 12000×2 59000
6 65000×1 + 3500×3 75500
7 89000×2 + 4800×5 202000
8 198000×1 + 45000×2 288000
9 12000×1 + 6500×3 31500
10 89000×3 + 3500×8 295000

合計: 1768100、平均: 176810。この平均を超える注文は5件です。

MySQLのFROM句サブクエリにはエイリアス(AS order_totalsAS sub)が必須です。


問7 ⭐⭐(応用):EXISTS ― パソコンを注文したことがある顧客

EXISTSを使い、カテゴリが「パソコン」の商品を1回でも注文したことがある顧客の customer_idcustomer_name を取得してください。customer_id の昇順で並べてください。

期待される結果:

customer_id customer_name
1 株式会社ABC
3 GHI商事
4 JKLテクノロジー
5 MNOサービス
模範解答
SELECT c.customer_id, c.customer_name
FROM customers c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    JOIN order_details od ON o.order_id = od.order_id
    JOIN products p ON od.product_id = p.product_id
    WHERE o.customer_id = c.customer_id
      AND p.category = 'パソコン'
)
ORDER BY c.customer_id;

解説:

パソコンカテゴリの商品は「ノートPC Pro」(product_id = 1)と「ノートPC Light」(product_id = 6)です。

  • 株式会社ABC(cust 1): 注文1でproduct 1、注文3でproduct 6、注文8でproduct 1 → 該当
  • GHI商事(cust 3): 注文4でproduct 1 → 該当
  • JKLテクノロジー(cust 4): 注文10でproduct 6 → 該当
  • MNOサービス(cust 5): 注文7でproduct 6 → 該当
  • DEFコーポレーション(cust 2): 注文2でproduct 4,5、注文6でproduct 8,2 → パソコンなし

EXISTS は条件を満たす行が1行でも見つかればTRUEを返すため、IN より効率的な場合があります。


問8 ⭐⭐(応用):NOT EXISTS ― モニターを注文したことがない顧客

NOT EXISTSを使い、カテゴリが「モニター」の商品を一度も注文したことがない顧客の customer_idcustomer_name を取得してください。customer_id の昇順で並べてください。

期待される結果:

customer_id customer_name
3 GHI商事
4 JKLテクノロジー
5 MNOサービス
模範解答
SELECT c.customer_id, c.customer_name
FROM customers c
WHERE NOT EXISTS (
    SELECT 1
    FROM orders o
    JOIN order_details od ON o.order_id = od.order_id
    JOIN products p ON od.product_id = p.product_id
    WHERE o.customer_id = c.customer_id
      AND p.category = 'モニター'
)
ORDER BY c.customer_id;

解説:

モニターカテゴリの商品は「4Kモニター 27インチ」(product_id = 4)と「ゲーミングモニター」(product_id = 8)です。

  • 株式会社ABC(cust 1): 注文8でproduct 4(4Kモニター)→ モニター注文あり
  • DEFコーポレーション(cust 2): 注文2でproduct 4、注文6でproduct 8 → モニター注文あり
  • GHI商事(cust 3): 注文4でproduct 1,7、注文9でproduct 5,7 → モニターなし
  • JKLテクノロジー(cust 4): 注文5でproduct 2,5、注文10でproduct 6,2 → モニターなし
  • MNOサービス(cust 5): 注文7でproduct 6,3 → モニターなし

問9 ⭐⭐⭐(チャレンジ):2人以上の部下を持つマネージャー

相関サブクエリを使い、部下(manager_id が自分の employee_id である社員)を2人以上持つマネージャーの employee_idnamesubordinate_count(部下数)を取得してください。subordinate_count の降順で並べてください。

期待される結果:

employee_id name subordinate_count
1 田中太郎 3
2 佐藤花子 2
4 高橋美咲 2
模範解答
SELECT
    e.employee_id,
    e.name,
    (SELECT COUNT(*)
     FROM employees e2
     WHERE e2.manager_id = e.employee_id) AS subordinate_count
FROM employees e
WHERE (
    SELECT COUNT(*)
    FROM employees e2
    WHERE e2.manager_id = e.employee_id
) >= 2
ORDER BY subordinate_count DESC;

解説:

各社員の部下数は以下のとおりです。

  • 田中太郎(emp 1): manager_id = 1 の社員 → 佐藤花子、鈴木一郎、中村真理 = 3人
  • 佐藤花子(emp 2): manager_id = 2 の社員 → 伊藤健太、渡辺さくら = 2人
  • 高橋美咲(emp 4): manager_id = 4 の社員 → 山本大輔、小林誠 = 2人

HAVING句を使う代替解法もあります。

-- 代替解法:JOINとGROUP BYを使う方法
SELECT e.employee_id, e.name, COUNT(e2.employee_id) AS subordinate_count
FROM employees e
JOIN employees e2 ON e2.manager_id = e.employee_id
GROUP BY e.employee_id, e.name
HAVING COUNT(e2.employee_id) >= 2
ORDER BY subordinate_count DESC;

問10 ⭐⭐⭐(チャレンジ):顧客別の注文回数と合計金額

相関サブクエリを使い、全顧客の customer_nameorder_count(注文回数)、total_amount(合計金額)を取得してください。注文がない顧客は order_count = 0、total_amount = 0 としてください。total_amount の降順で並べてください。

期待される結果:

customer_name order_count total_amount
株式会社ABC 3 800100
JKLテクノロジー 2 354000
GHI商事 2 255500
MNOサービス 1 202000
DEFコーポレーション 2 156500
模範解答
SELECT
    c.customer_name,
    (SELECT COUNT(*)
     FROM orders o
     WHERE o.customer_id = c.customer_id) AS order_count,
    (SELECT COALESCE(SUM(p.price * od.quantity), 0)
     FROM orders o
     JOIN order_details od ON o.order_id = od.order_id
     JOIN products p ON od.product_id = p.product_id
     WHERE o.customer_id = c.customer_id) AS total_amount
FROM customers c
ORDER BY total_amount DESC;

解説:

各顧客の注文内容と合計金額は以下のとおりです。

  • 株式会社ABC(cust 1): 注文1(413500)+ 注文3(98600)+ 注文8(288000)= 800100
  • JKLテクノロジー(cust 4): 注文5(59000)+ 注文10(295000)= 354000
  • GHI商事(cust 3): 注文4(224000)+ 注文9(31500)= 255500
  • MNOサービス(cust 5): 注文7(202000)= 202000
  • DEFコーポレーション(cust 2): 注文2(81000)+ 注文6(75500)= 156500

COALESCE を使うことで、注文がない場合に NULL ではなく 0 を返します(今回のデータでは全顧客に注文があります)。

代替解法(FROM句サブクエリを使う方法):

SELECT
    c.customer_name,
    COALESCE(sub.order_count, 0) AS order_count,
    COALESCE(sub.total_amount, 0) AS total_amount
FROM customers c
LEFT JOIN (
    SELECT
        o.customer_id,
        COUNT(DISTINCT o.order_id) AS order_count,
        SUM(p.price * od.quantity) AS total_amount
    FROM orders o
    JOIN order_details od ON o.order_id = od.order_id
    JOIN products p ON od.product_id = p.product_id
    GROUP BY o.customer_id
) AS sub ON c.customer_id = sub.customer_id
ORDER BY total_amount DESC;

まとめ

サブクエリの種類 特徴 主な用途
スカラーサブクエリ 1行1列を返す SELECT句・WHERE句での値比較
テーブルサブクエリ 表形式の結果を返す FROM句で仮想テーブルとして利用
相関サブクエリ 外側のクエリの値を参照 行ごとの条件評価・グループ内比較
EXISTS / NOT EXISTS 存在チェック 関連データの有無で絞り込み
IN サブクエリ 値リストに含まれるか 別テーブルの条件による絞り込み

サブクエリは強力ですが、相関サブクエリは行数が多いとパフォーマンスに影響します。JOINやウィンドウ関数で書き換えられる場合は、実行計画(EXPLAIN)を確認して最適な方法を選びましょう。


参考


@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?