🇯🇵 日本語 | 🇺🇸 English | 🇪🇸 Español | 🇵🇹 Português | 🇹🇭 ไทย | 🇨🇳 中文

【SQL中級編】サブクエリを制覇しよう!SELECTの中のSELECTを徹底解説

「全社員の平均給与よりも高い給与をもらっている社員だけを知りたい…」
「各部署で最も給与が高い社員は誰だろう?」

Webクリエイターとしてデータベースを扱う中で、こんな風に「あるクエリの結果を、別のクエリの条件として使いたい」と思ったことはありませんか?一度の問い合わせでは取得できない、少し複雑なデータ抽出。これを華麗に解決してくれるのが、今回ご紹介するサブクエリ(副問い合わせ)です。

サブクエリは、一言で言えば「クエリの中のクエリ」。まるでマトリョーシカ人形のように、SELECT文の中に別のSELECT文を埋め込むテクニックです。これを使いこなせれば、複数ステップに分かれていたデータ抽出を1つのクエリにまとめることができ、あなたのSQLスキルは格段にレベルアップします!

この記事では、初心者の方でもコピペで動かせるサンプルコードを豊富に用意し、「動いた!」という体験を通してサブクエリの強力さと便利さを実感していただきます。


準備:今回の練習用テーブル

サブクエリの威力を最大限に体験するために、今回は「社員(employees)」テーブルと「部署(departments)」テーブルの2つを用意します。以下のSQL文をコピーして、ご自身のデータベース環境で実行してみてください。

もちろん、すぐに試せる環境がなくても大丈夫!記事の後半で、ブラウザだけでSQLを試せる「SQL実行環境」をご用意しています。


-- テーブルが存在すれば削除
DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS departments;

-- 部署テーブルの作成
CREATE TABLE departments (
    id INT PRIMARY KEY,
    department_name VARCHAR(50)
);

-- 部署データの挿入
INSERT INTO departments (id, department_name) VALUES
(1, '営業部'),
(2, '開発部'),
(3, '人事部');

-- 社員テーブルの作成
CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    department_id INT,
    salary INT,
    FOREIGN KEY (department_id) REFERENCES departments(id)
);

-- 社員データの挿入
INSERT INTO employees (id, name, department_id, salary) VALUES
(1, '山田 太郎', 1, 600000), -- 営業部
(2, '鈴木 花子', 2, 550000), -- 開発部
(3, '佐藤 次郎', 1, 400000), -- 営業部
(4, '高橋 三郎', 3, 650000), -- 人事部
(5, '田中 恵子', 2, 500000), -- 開発部
(6, '伊藤 四郎', 1, 700000), -- 営業部
(7, '渡辺 久美', 2, 750000), -- 開発部
(8, '山本 五郎', 3, 380000); -- 人事部
    

準備はOKですか?それでは、サブクエリの強力な世界へ足を踏み入れましょう!


1. 基本のキ:WHERE句で使うサブクエリ

サブクエリの最も基本的で直感的な使い方が、WHERE句の条件として利用するパターンです。外側のクエリ(主クエリ)の絞り込み条件を、内側のクエリ(サブクエリ)で動的に生成するイメージです。

サブクエリは必ず()(括弧)で囲むのがルールです。SQLはまず括弧の中のサブクエリを実行し、その実行結果を主クエリの条件として利用します。

例1:平均給与より高い給与の社員を抽出

冒頭で挙げた「全社員の平均給与よりも高い給与をもらっている社員」を探してみましょう。これを実現するには、2つのステップが必要です。

  1. まず、「全社員の平均給与」を計算する。
  2. 次に、その平均給与を条件に、社員情報を抽出する。

サブクエリを使えば、これを1つのクエリで実行できます。


SELECT *
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
    

どうでしょうか?まず(SELECT AVG(salary) FROM employees)が実行されて平均給与(この例では547500)が計算され、その結果が主クエリのWHERE salary > 547500という条件として使われます。見事に1つのクエリで目的を達成できましたね。


例2:特定の部署に所属する社員を抽出 (IN演算子との連携)

サブクエリは、1つの値だけでなく、複数の値のリストを返すこともできます。そのリストをIN演算子と組み合わせることで、非常に柔軟な条件指定が可能になります。

例えば、「営業部」または「人事部」に所属するすべての社員を抽出したいとします。部署名から部署IDのリストをサブクエリで取得し、それを主クエリの条件に使いましょう。


SELECT *
FROM employees
WHERE department_id IN (
    SELECT id FROM departments WHERE department_name IN ('営業部', '人事部')
);
    

このクエリでは、まずサブクエリが実行され、'営業部'と'人事部'のIDである(1, 3)というリストが返されます。その後、主クエリはWHERE department_id IN (1, 3)として解釈され、該当する社員のデータを抽出します。部署名が変わってもSQLを修正する必要がなく、とても便利です。


2. 応用編:SELECT句やFROM句で使うサブクエリ

サブクエリはWHERE句以外でも大活躍します。特にSELECT句やFROM句で使うことで、データ分析の幅がぐっと広がります。

SELECT句のサブクエリ (スカラーサブクエリ)

SELECT句の中でサブクエリを使うと、主クエリの各行に対して関連する値を取得し、新しい列として表示できます。ただし、このサブクエリは必ず1行1列の単一の値(スカラー値)を返す必要があります。

例として、各社員の情報に加えて、「その社員が所属する部署の平均給与」を並べて表示してみましょう。


SELECT
    name,
    salary,
    (SELECT department_name FROM departments WHERE id = e.department_id) AS department_name,
    (SELECT AVG(salary) FROM employees WHERE department_id = e.department_id) AS department_avg_salary
FROM
    employees AS e;
    

少し複雑に見えますね。これは相関サブクエリと呼ばれるもので、サブクエリ(内側)が主クエリ(外側)の各行のデータ(この場合はe.department_id)を参照して実行されます。主クエリの行が1行進むごとに、その行の部署IDを使って部署名と部署の平均給与が計算され、結果の列に追加されるのです。


FROM句のサブクエリ (インラインビュー)

FROM句にサブクエリを使うと、そのサブクエリの結果を「一時的な仮想テーブル(インラインビュー)」として扱うことができます。まずサブクエリでデータを集計・加工し、その結果に対してさらにクエリを実行する、という二段構えの処理に便利です。

例として、「部署ごとの平均給与を算出し、その中で平均給与が55万円以上の部署だけ」を表示してみましょう。


SELECT
    d.department_name,
    dept_avg.avg_salary
FROM
    (
        SELECT department_id, AVG(salary) AS avg_salary
        FROM employees
        GROUP BY department_id
    ) AS dept_avg
JOIN
    departments AS d ON dept_avg.department_id = d.id
WHERE
    dept_avg.avg_salary >= 550000;
    

このクエリでは、まずFROM句の中のサブクエリが実行され、department_idごとの平均給与を計算した一時的なテーブルdept_avgが作成されます。その後、主クエリがこのdept_avgテーブルとdepartmentsテーブルを結合し、条件に合う部署名と平均給与を表示しています。GROUP BYの結果にさらにWHERE句を使いたい場合に非常に有効なテクニックです。


実践!ブラウザでSQLを実行してみよう

お待たせしました!ここまで学んだサブクエリを、実際に手を動かして試せる環境です。

下のコードをまるごとコピーして、subquery_practice.htmlのようなファイル名で保存し、ブラウザで開いてみてください。この記事で紹介した色々なサブクエリを試して、その動きを体感してみてください!


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>SQLサブクエリ練習場</title>
    <script src="https://cdn.jsdelivr.net/npm/alasql@4"></script>
    <style>
        body { font-family: sans-serif; padding: 2rem; background-color: #f9f9f9; }
        .container { max-width: 800px; margin: auto; background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
        h1 { color: #333; }
        textarea { width: 100%; height: 180px; font-family: monospace; font-size: 16px; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; margin-bottom: 1rem; }
        button { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
        button:hover { background-color: #0056b3; }
        #result-area { margin-top: 2rem; }
        table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #f2f2f2; }
        .error { color: red; font-weight: bold; }
    </style>
</head>
<body>
    <div class="container">
        <h1>サブクエリを試してみよう!</h1>
        <p>下のテキストエリアにSQL文を入力して「実行」ボタンを押してください。</p>
        <textarea id="sql-input">-- この中にSQLを書いてください
SELECT
    d.department_name,
    dept_avg.avg_salary
FROM
    (
        SELECT department_id, AVG(salary) AS avg_salary
        FROM employees
        GROUP BY department_id
    ) AS dept_avg
JOIN
    departments AS d ON dept_avg.department_id = d.id
WHERE
    dept_avg.avg_salary >= 550000;</textarea>
        <button onclick="executeSQL()">SQLを実行</button>
        <div id="result-area"></div>
    </div>

    <script>
        // データベースの初期化とデータ準備
        const db = new alasql.Database();
        db.exec(`
            CREATE TABLE departments (id INT PRIMARY KEY, department_name VARCHAR(50));
            INSERT INTO departments VALUES (1, '営業部'), (2, '開発部'), (3, '人事部');

            CREATE TABLE employees (id INT PRIMARY KEY, name VARCHAR(50), department_id INT, salary INT);
            INSERT INTO employees VALUES
            (1, '山田 太郎', 1, 600000), (2, '鈴木 花子', 2, 550000), (3, '佐藤 次郎', 1, 400000),
            (4, '高橋 三郎', 3, 650000), (5, '田中 恵子', 2, 500000), (6, '伊藤 四郎', 1, 700000),
            (7, '渡辺 久美', 2, 750000), (8, '山本 五郎', 3, 380000);
        `);

        function executeSQL() {
            const sql = document.getElementById('sql-input').value;
            const resultArea = document.getElementById('result-area');
            resultArea.innerHTML = '';

            try {
                const result = db.exec(sql);
                if (result.length > 0) {
                    resultArea.appendChild(createTable(result));
                } else {
                    resultArea.innerHTML = '<p>結果は0件でした。</p>';
                }
            } catch (e) {
                resultArea.innerHTML = `<p class="error">エラー: ${e.message}</p>`;
            }
        }

        function createTable(data) {
            const table = document.createElement('table');
            const thead = table.createTHead();
            const tbody = table.createTBody();
            const headerRow = thead.insertRow();
            for (const key in data[0]) {
                const th = document.createElement('th');
                th.textContent = key;
                headerRow.appendChild(th);
            }
            data.forEach(rowData => {
                const row = tbody.insertRow();
                for (const key in rowData) {
                    const cell = row.insertCell();
                    cell.textContent = rowData[key];
                }
            });
            return table;
        }

        // 初期表示
        executeSQL();
    </script>
</body>
</html>
    

気をつけるべき点と関連テクニック

サブクエリは非常に強力ですが、いくつか注意点があります。これらを知っておくことで、より洗練されたクエリを書けるようになります。

パフォーマンスの問題

サブクエリ、特に先ほど紹介した相関サブクエリは、主クエリの行ごとに実行されるため、データ量が多くなるとパフォーマンスが著しく低下することがあります。多くの場合、サブクエリはJOINを使って書き換えることができ、そちらの方が高速に動作します。

サブクエリの戻り値

サブクエリが返すデータの形にはルールがあります。

これらのルールを破るとエラーが発生するため、サブクエリがどのような結果を返すのかを常に意識することが重要です。


関連テクニック:JOINによる書き換え

パフォーマンスの観点から、多くのサブクエリはJOINを使った形に書き換えることが推奨されます。先ほどの「特定の部署に所属する社員を抽出」するクエリをJOINで書いてみましょう。


SELECT
    e.*
FROM
    employees AS e
JOIN
    departments AS d ON e.department_id = d.id
WHERE
    d.department_name IN ('営業部', '人事部');
    

このJOINを使ったクエリは、サブクエリを使った場合と同じ結果を返します。一般的に、データベースのオプティマイザ(クエリを最適化する機能)はJOINをより効率的に処理できるため、特にデータ量が多い場合はJOINの使用を検討しましょう。


まとめ

お疲れ様でした!今回は、SQLの強力なテクニックであるサブクエリについて、その使い方を深く掘り下げてきました。

サブクエリを使いこなせると、これまで複数回に分けて行っていたデータ取得が一度で済むようになり、作業効率が飛躍的に向上します。ぜひ、今回紹介したSQL実行環境で色々なパターンを試し、サブクエリを自分のものにしてください!