IPA|情報処理技術者試験

INSERT…SELECTからMERGE、EXISTSまで:応用SQL大全(応用情報・DBスペ対応)

SELECT * FROM ... はもう卒業。あなたは、実務の複雑なデータ要求や、応用情報・データベーススペ-シャリスト試験の午後問題で出題されるような、一筋縄ではいかないSQLに頭を悩ませていませんか?基本的なCRUD操作はできても、GROUP BYLEFT JOINを組み合わせるあたりから、SQLの複雑さに壁を感じる方は少なくありません。

「サブクエリが何重にもなって可読性が最悪…」「もっと効率的にデータを更新する方法はないのか?」「ウィンドウ関数って聞いたことはあるけど、どう使えばいいか分からない」。そんな悩みは、多くのデータ活用者が通る道です。

本記事では、基本的なSQLから一歩進み、あなたの「SQLの引き出し」を劇的に増やすための応用的な構文を網羅的に解説します。単なる構文の紹介だけでなく、なぜそれが必要なのか(What/Why)、どのような場面で役立つのか(When/Where)、そして具体的な書き方(How)までを、豊富なコード例や図解を交えながら体系的に掘り下げていきます。

データの挿入・更新を効率化するテクニックから、WITH句(CTE)による可読性の向上、ROW_NUMBER()をはじめとするウィンドウ関数での高度な集計、MERGE文による条件分岐処理、そして試験で狙われやすいEXISTS句やHAVING句の使い分けまで。実務で頻出のケースや、試験の「ひっかけ問題」対策に焦点を当て、一つひとつ丁寧にマスターしていきましょう。

この記事を読み終える頃には、あなたは複雑なSQLを自信を持って組み立てられるようになり、データ活用のレベルが格段に向上しているはずです。それでは、応用SQLの奥深い世界へ一緒に旅立ちましょう。

目次

INSERTの応用テクニック│クエリ結果の挿入からIDの即時取得まで

日々の業務で何気なく使っている INSERT 文。INSERT INTO table (column1, column2) VALUES (value1, value2); という基本形は、SQLを学び始めた最初に覚える構文の一つでしょう。しかし、実務や試験では、この基本形だけでは対応できないシーンが数多く登場します。

例えば、「別のテーブルの集計結果を丸ごと新しいテーブルに移したい」「大量のテストデータを一括で作成したい」「データを登録した瞬間に、自動採番されたIDをすぐさま取得したい」といった要求です。

これらの課題は、これから紹介する3つの応用テクニックを使うことで、スマートかつ効率的に解決できます。

  1. INSERT ... SELECT:クエリの結果をそのまま別のテーブルに挿入する
  2. CTAS / SELECT INTO:テーブル作成とデータ挿入を一度に行う
  3. INSERT ... RETURNING / OUTPUT:挿入したレコードの情報を即座に受け取る

一つずつ、具体的なコード例と業務シーンを交えて見ていきましょう。

クエリ結果を一括登録する INSERT ... SELECT

INSERT ... SELECT は、SELECT 文で取得した結果セットを、そのまま別のテーブルに一括で挿入するための構文です。ループ処理などで1件ずつ INSERT を繰り返すのに比べ、パフォーマンスに優れ、コードも簡潔になるという大きなメリットがあります。

📖 具体例:月次の売上サマリーを作成する

日々の売上データが格納された sales テーブルがあるとします。

-- 挿入先のテーブル(あらかじめ作成しておく)
CREATE TABLE monthly_summary (
    target_month  VARCHAR(7),
    customer_id   INT,
    total_amount  BIGINT
);

-- 2025年9月分のデータを集計し、サマリーテーブルに挿入
INSERT INTO monthly_summary (target_month, customer_id, total_amount)
SELECT
    '2025-09' AS target_month,
    customer_id,
    SUM(amount) AS total_amount
FROM
    sales
WHERE
    sale_date >= '2025-09-01' AND sale_date < '2025-10-01'
GROUP BY
    customer_id;

🏢 こんな仕事で役立つ!

  • 夜間バッチ処理: 日次データを集計して、月次サマリーテーブルや分析用DWH(データウェアハウス)にデータを転送する処理。
  • データ移行: 古いテーブルから新しいスキーマのテーブルへ、データを変換しながら一括で移行する作業。
  • テストデータ作成: 既存の顧客データを元に、個人情報などをマスク処理したテスト用データを大量に作成する場面。

テーブル作成とデータ挿入を同時に行う CTAS / SELECT INTO

CTAS (Create Table As Select) は、その名の通り、SELECT 文の結果を元にして新しいテーブルの作成とデータの挿入を同時に行います。CREATE TABLE して INSERT ... SELECT を行う2ステップを、1つのクエリで完結させられる便利な構文です。

なお、データベース製品によって構文が異なります。

構文 対応する主なDBMS
CREATE TABLE ... AS SELECT Oracle, PostgreSQL, MySQL, BigQuery
SELECT ... INTO ... FROM SQL Server

📖 具体例:分析用の一時テーブルを作成する

PostgreSQL や Oracle の場合 (CTAS)

CREATE TABLE premium_customers AS
SELECT
    customer_id,
    COUNT(*) AS purchase_count,
    SUM(amount) AS total_amount
FROM
    sales
GROUP BY
    customer_id
HAVING
    SUM(amount) >= 100000;

SQL Server の場合 (SELECT INTO)

SELECT
    customer_id,
    COUNT(*) AS purchase_count,
    SUM(amount) AS total_amount
INTO
    premium_customers
FROM
    sales
GROUP BY
    customer_id
HAVING
    SUM(amount) >= 100000;

🏢 こんな仕事で役立つ!

  • データ分析: 元の大規模テーブルに影響を与えず、分析に必要なデータだけを抽出した自分専用の「サンドボックス(砂場)」テーブルを作成する時。
  • バックアップ: テーブルを更新する前に、念のため特定の条件のデータだけを別テーブルに退避させておきたい時。

登録したIDを即座に受け取る INSERT ... RETURNING / OUTPUT

Webアプリケーションなどでデータを新規登録する際、自動採番(SERIALIDENTITY)で生成された主キー(ID)をすぐに知りたいケースは非常に多いです。RETURNING句 (PostgreSQL, Oracle) や OUTPUT句 (SQL Server) を使えば、INSERT を実行したその場で、挿入された行の情報を返すことができます。

📖 具体例:新規ユーザー登録でIDを取得する

PostgreSQL や Oracle の場合 (RETURNING)

INSERT INTO users (name, email)
VALUES ('Taro Yamada', 'taro@example.com')
RETURNING id;

SQL Server の場合 (OUTPUT)

INSERT INTO users (name, email)
OUTPUT inserted.id
VALUES ('Jiro Sato', 'jiro@example.com');

🏢 こんな仕事で役立つ!

  • Webアプリケーション: ユーザーの新規登録、注文の受付、投稿の保存など、登録処理と同時に生成されたIDを使って次の処理へ繋げたいあらゆる場面。
  • 親子関係のあるデータ登録: 親テーブルにデータを INSERT してIDを取得し、即座にそのIDを使って子テーブルにデータを登録する処理。

UPDATE/DELETEの高度な使い方│JOINやサブクエリで対象を一括指定

データの更新や削除を行うUPDATEDELETEWHERE id = 1 のように単一のレコードを対象とする処理は簡単ですが、実務ではもっと複雑な条件指定が求められます。「顧客マスタを参照して、特定の会員ランクのユーザーに関連する売上データのフラグを一括で更新したい」「商品カテゴリが『廃番』になった商品に関連する、すべての在庫レコードを削除したい」といったケースです。

このような「別のテーブルの情報」を条件に含める場合、JOIN やサブクエリを活用することで、複数のクエリを発行することなく、一度の操作で安全かつ効率的に処理を完結させることができます。

別のテーブルを条件にする JOIN を伴うUPDATE/DELETE

UPDATE文やDELETE文の句の中でJOINを使い、他のテーブルと結合することで、そのテーブルのカラムを条件に指定できます。これにより、アプリケーション側でSELECTしてからUPDATEする、といった面倒な手続きが不要になります。

📖 具体例:会員ランクに応じて売上データを更新する

「ブロンズ会員(rank = 'Bronze')の売上レコード全てに、備考欄(notes)へ『キャンペーン割引対象外』と追記する」という要件に対応してみましょう。

PostgreSQL の場合 (UPDATE ... FROM ...)

UPDATE sales s
SET
    notes = 'キャンペーン割引対象外'
FROM
    customers c
WHERE
    s.customer_id = c.customer_id AND c.rank = 'Bronze';

MySQL の場合 (UPDATE ... JOIN ...)

UPDATE sales s
JOIN
    customers c ON s.customer_id = c.customer_id
SET
    s.notes = 'キャンペーン割引対象外'
WHERE
    c.rank = 'Bronze';

🏢 こんな仕事で役立つ!

  • マスタデータ連携: 商品マスタで「販売終了」になった商品について、関連する在庫テーブルのステータスを「販売不可」に一括更新する。
  • データクレンジング: 顧客マスタ上で重複フラグが立てられた顧客の、古い方の行動ログを一括で削除する。

📝 DBMS別 構文の違い

DBMS UPDATE構文 DELETE構文
PostgreSQL UPDATE T1 SET ... FROM T2 WHERE ... DELETE FROM T1 USING T2 WHERE ...
SQL Server UPDATE T1 SET ... FROM T1 JOIN T2 ON ... DELETE T1 FROM T1 JOIN T2 ON ...
MySQL UPDATE T1 JOIN T2 ON ... SET ... DELETE T1 FROM T1 JOIN T2 ON ...
Oracle MERGE文や UPDATE (SELECT ...) を使用 サブクエリ (IN / EXISTS) を使用

サブクエリを利用した条件更新/削除

WHERE句の中にSELECT文(サブクエリ)を記述して、更新・削除の対象を絞り込む方法です。INEXISTS を使うのが一般的で、JOINに比べて構文が直感的で分かりやすい場合があります。

📖 具体例:購入履歴のない顧客ステータスを更新する

「直近1年間に一度も購入履歴のない顧客」のステータスを、「休眠(dormant)」に更新するケースを考えます。NOT EXISTSを使うと、以下のように表現できます。

UPDATE customers c
SET
    status = 'dormant'
WHERE
    NOT EXISTS (
        SELECT 1
        FROM sales s
        WHERE
            s.customer_id = c.customer_id
            AND s.sale_date >= CURRENT_DATE - INTERVAL '1 year'
    );

🏢 こんな仕事で役立つ!

  • マスタの棚卸し: 在庫数が0で、かつ過去3年間一度も注文されていない商品を商品マスタからDELETEする。
  • フラグ付け: 全社員の平均勤続年数をサブクエリで算出し、その平均値よりも勤続年数が長い社員のレコードに「ベテラン」フラグをUPDATEで付与する。

UPSERT/重複時の処理│MERGE文で挿入と更新を一度に

データを登録しようとしたら、主キーやユニークキーの重複でエラーが発生した、という経験は誰にでもあるでしょう。この問題を解決する古典的な方法は、次のような手順を踏むことでした。

  1. SELECT 文でデータが存在するかどうかを確認する。
  2. アプリケーションのコードで、存在しない (NOT EXISTS) なら INSERT を実行。
  3. 存在する場合 (EXISTS) は UPDATE を実行。

しかしこの方法には、①データベースとの通信が2回発生し非効率②確認と実行の間に別の処理が割り込む「競合状態(Race Condition)」のリスクがある、という弱点がありました。

この「もし存在するなら更新、存在しないなら挿入」を、一度の命令で安全(アトミック)に実行する操作を UPSERT (UPdate + inSERT) と呼びます。UPSERTを実現する構文はDBMSによって異なりますが、ここでは代表的な3つを紹介します。

  • MERGE文:SQLの標準規格で定められている多機能な構文
  • ON CONFLICT DO UPDATE:PostgreSQLなどで使われる、INSERT文の拡張構文
  • ON DUPLICATE KEY UPDATE:MySQLで使われる、こちらもINSERT文の拡張構文

SQL標準の MERGE

MERGE文は、更新元データ(ソース)と更新先テーブル(ターゲット)を結合条件で比較し、一致した(MATCHED)場合と、一致しなかった(NOT MATCHED)場合で、それぞれ実行する処理(UPDATE, INSERT, DELETE)を定義できる非常に強力な構文です。

📖 具体例:日々の入荷データで在庫マスタを更新する

product_idP002 の「みかん」は在庫がすでにあるので更新 (UPDATE)P003 の新商品は在庫がないので新規登録 (INSERT) したいです。MERGE文を使うと、この処理を1クエリで記述できます。

-- SQL Server, Oracle などで利用可能
MERGE INTO product_stock AS target
USING daily_arrivals AS source
ON (target.product_id = source.product_id)
WHEN MATCHED THEN
    -- 一致した場合:在庫数を加算して更新
    UPDATE SET target.quantity = target.quantity + source.arrival_quantity
WHEN NOT MATCHED THEN
    -- 一致しなかった場合:新しい商品として挿入
    INSERT (product_id, product_name, quantity)
    VALUES (source.product_id, '(商品名未登録)', source.arrival_quantity);

🏢 こんな仕事で役立つ!

  • ETL/バッチ処理: 夜間バッチで、基幹システムから抽出した差分データをDWH(データウェアハウス)に反映させる処理。
  • データ同期: 複数のデータベース間でマスタデータを同期する場面。

PostgreSQLの ON CONFLICT DO UPDATE

PostgreSQLでは、INSERT文にON CONFLICT句を追加することで、より直感的にUPSERTを記述できます。

-- PostgreSQL, SQLite などで利用可能
INSERT INTO product_stock (product_id, quantity)
VALUES ('P002', 50), ('P003', 80)
ON CONFLICT (product_id) DO UPDATE SET
    quantity = product_stock.quantity + excluded.arrival_quantity;

MySQLの ON DUPLICATE KEY UPDATE

MySQLにも同様の構文があります。主キーまたはユニークキーが重複した場合の動作をON DUPLICATE KEY UPDATE句で指定します。

-- MySQL, MariaDB などで利用可能
INSERT INTO product_stock (product_id, quantity)
VALUES ('P002', 50), ('P003', 80)
ON DUPLICATE KEY UPDATE
    quantity = quantity + VALUES(quantity);

📝 DBMS別 UPSERT構文のまとめ

構文 対応DBMS 特徴
MERGE SQL Server, Oracle, BigQuery SQL標準。構文はやや複雑だが、DELETEも指定できるなど多機能。
ON CONFLICT DO UPDATE PostgreSQL, SQLite INSERT文の拡張で直感的。ユニーク制約違反がトリガーとなる。
ON DUPLICATE KEY UPDATE MySQL, MariaDB こちらもINSERT文の拡張。主キー or ユニーク制約違反がトリガー。

CTE(WITH句)の活用法│複雑なSQLを部品化して可読性を劇的に向上

サブクエリが何重にもネスト(入れ子に)され、インデントが深くなりすぎて、もはや解読不能なSQL文、通称「秘伝のタレ」化したクエリに遭遇したことはありませんか?この問題を解決する強力な武器が CTE (Common Table Expression)、通称 WITH です。

WITH句を使うと、複雑なSQLを意味のある単位で部品化し、それぞれに名前を付けることができます。結果として、SQLの可読性メンテナンス性が劇的に向上します。

基本のWITH句:サブクエリを撲滅し、可読性を手に入れる

WITH句の基本的な使い方は、SELECT文の前に WITH 名前 AS (SELECT ...) という形で、一時的な結果セットを定義することです。

🍳 身近な例え:料理の下ごしらえ

WITH句は料理の「下ごしらえ」に似ています。

  • サブクエリだらけのSQL: 全ての食材と調理工程を一つの鍋に一度に放り込むようなもの。
  • WITH句を使ったSQL:
    1. 野菜を切ってボウルに入れておく (WITH veges AS (...))
    2. ソースを別の器で混ぜておく (WITH sauce AS (...))
    3. 最後にフライパンで1と2を炒め合わせる (メインのSELECT)

📖 具体例:部門別の売上と従業員数を算出する

After: WITH句で書き換えた例

-- 処理の流れが上から下へ、直線的で分かりやすい
WITH
  -- 部品1: 部門ごとの売上合計を計算
  dept_sales AS (
    SELECT
      dept_id,
      SUM(amount) AS total_amount
    FROM sales
    GROUP BY dept_id
  ),
  -- 部品2: 部門ごとの従業員数を計算
  dept_employees AS (
    SELECT
      dept_id,
      COUNT(*) AS num_employees
    FROM employees
    GROUP BY dept_id
  )
-- 部品1と部品2を結合して最終結果を生成
SELECT
    d.dept_name,
    ds.total_amount,
    de.num_employees
FROM
    departments d
JOIN dept_sales ds ON d.dept_id = ds.dept_id
JOIN dept_employees de ON d.dept_id = de.dept_id;

再帰CTE:組織図などの階層構造データを自在に扱う

WITH句のもう一つの強力な機能が「再帰」です。これは、深さが不定な階層構造(ツリー構造)のデータを扱う際に絶大な威力を発揮します。

📖 具体例:組織図の階層を展開する

-- '田中部長' (id=10) 配下の全組織を取得する
WITH RECURSIVE subordinates AS (
  -- 1. アンカーメンバー:再帰の起点
  SELECT
    id, name, manager_id, 1 AS depth
  FROM employees
  WHERE id = 10

  UNION ALL

  -- 2. 再帰メンバー:アンカーメンバーの結果と自己結合
  SELECT
    e.id, e.name, e.manager_id, s.depth + 1
  FROM employees e
  JOIN subordinates s ON e.manager_id = s.id
)
SELECT * FROM subordinates;

🏢 こんな仕事で役立つ!

  • 組織図: 特定の部署配下の全従業員をリストアップする。
  • 部品表 (BOM): ある製品を組み立てるのに必要な全パーツを展開する。
  • Webサイトのカテゴリ: パンくずリストを生成する。

ウィンドウ関数を使いこなす│SQLで行内比較や順位付けを自在に

GROUP BY を使った集計はSQLの基本ですが、集計すると個々の行が持つ詳細情報が失われてしまう、という大きな制約があります。例えば、「各従業員の給与と、その従業員が所属する部門の平均給与を並べて表示したい」という要求に、GROUP BYだけで応えるのは困難です。

この課題をエレガントに解決するのがウィンドウ関数です。

ウィンドウ関数は、GROUP BYのように行を一つにまとめることなく、「現在の行に関連する行の集まり(=ウィンドウ)」に対して計算を行います。結果として、個々の行の情報を保持したまま、集計や順位付けの結果を新しい列として追加できます。

身近な例で言えば、GROUP BYが「3年1組のテストの平均点は80点」という要約を1行だけ出すのに対し、ウィンドウ関数はクラス名簿の生徒一人ひとりの横に「あなたの点数:85点、クラス平均:80点」と書き込んでくれるようなものです。

ウィンドウ関数の基本構文: OVER()

ウィンドウ関数は、必ず OVER() という句を伴って使用されます。この OVER() の中に、どの範囲(ウィンドウ)で計算を行うかを指定します。

関数名(引数) OVER (
    [PARTITION BY 列名...]  -- ウィンドウを分割する基準(どのグループで?)
    [ORDER BY 列名...]     -- ウィンドウ内の順序を定義(どの順番で?)
)
  • PARTITION BY: ウィンドウを区切るための列を指定します。「部門ごと」「商品カテゴリごと」といったグループを定義する、ウィンドウ関数版のGROUP BYです。
  • ORDER BY: PARTITION BYで区切られたウィンドウの中で、どのような順序で行を並べるかを定義します。特に順位付けや累計を計算する際に必須です。

代表的なウィンドウ関数と具体例

1. 順位付け:ROW_NUMBER, RANK, DENSE_RANK

  • ROW_NUMBER(): 同点でも重複しない一意の連番を振る (1, 2, 3, 4)
  • RANK(): 同点には同じ順位を付け、次の順位は飛ばす (1, 2, 2, 4)
  • DENSE_RANK(): 同点には同じ順位を付け、次の順位は飛ばさない (1, 2, 2, 3)

📖 具体例:部門ごとに給与の高い従業員を順位付けする

SELECT
    name, department, salary,
    ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC) AS row_num,
    RANK()       OVER(PARTITION BY department ORDER BY salary DESC) AS rank_num,
    DENSE_RANK() OVER(PARTITION BY department ORDER BY salary DESC) AS dense_rank_num
FROM
    employees;

2. 集計:SUM, AVG, COUNT など

📖 具体例:従業員の給与と部門平均給与を比較する

SELECT
    name, department, salary,
    AVG(salary) OVER(PARTITION BY department) AS avg_salary_in_dept
FROM
    employees;

3. ROW_NUMBERによる重複排除

📖 具体例:メールアドレスが重複しているユーザーのうち、最新の1件だけを残す

WITH RankedUsers AS (
    SELECT
        user_id, email, created_at,
        ROW_NUMBER() OVER(PARTITION BY email ORDER BY created_at DESC) AS rn
    FROM
        users
)
DELETE FROM users
WHERE user_id IN (SELECT user_id FROM RankedUsers WHERE rn > 1);

QUALIFY句:ウィンドウ関数の結果で絞り込む

QUALIFY句は、ウィンドウ関数のためのHAVINGと考えることができます。

📖 具体例:QUALIFYで部門ごとトップ2の従業員を抽出する

-- BigQuery, Snowflakeなどで利用可能
SELECT
    name, department, salary
FROM
    employees
QUALIFY
    RANK() OVER(PARTITION BY department ORDER BY salary DESC) <= 2;

サブクエリの応用:EXISTSによる存在チェックと相関サブクエリの仕組み

ここでは、INよりも効率的な場合が多い EXISTS句と、サブクエリを理解する上で避けては通れない「相関サブクエリ」の概念について掘り下げます。この違いを理解することは、応用情報・DBスペシャリスト試験の長文SQLを読解する上で極めて重要です。

EXISTS / NOT EXISTS:効率的な存在チェック

EXISTSは、サブクエリが1行でも結果を返せば真(TRUE)1行も返さなければ偽(FALSE)を返す演算子です。データベースは条件に合う行を1行見つけた時点でサブクエリの実行を打ち切れるため、パフォーマンス上有利になることがあります。

📖 具体例:一度でも商品を購入したことがある顧客を抽出する

SELECT
    c.customer_id, c.name
FROM
    customers c
WHERE
    EXISTS (
        -- c.customer_idに紐づく注文が1件でもあれば、このサブクエリはTRUEを返す
        SELECT 1
        FROM orders o
        WHERE o.customer_id = c.customer_id
    );

相関サブクエリと非相関サブクエリの違い

1. 非相関サブクエリ (Non-correlated Subquery)

サブクエリが外側のクエリとは独立して単体で実行できるものです。先にサブクエリが一度だけ実行され、その結果を使って外側のクエリが処理されます。

2. 相関サブクエリ (Correlated Subquery)

サブクエリが外側のクエリの各行の値を参照しているため、単体では実行できないものです。外側のクエリで処理される各行に対して、サブクエリが繰り返し実行されます。

📝 2種類のサブクエリの比較

非相関サブクエリ (Non-correlated) 相関サブクエリ (Correlated)
実行タイミング 外側のクエリより先に一度だけ実行 外側のクエリの各行ごとに繰り返し実行
依存関係 独立しており、単体で実行可能 外側のクエリに依存し、単体では実行不可
主な用途 全体集計値との比較 (IN, >, <) 行ごとの存在チェック (EXISTS)
パフォーマンス 一般的に高速 外側の行数が多いと低速になりがち(インデックスが重要)

JOIN句の使い分け完全ガイド│INNER, LEFT JOINとON, WHEREの違い

リレーショナルデータベースの心臓部とも言えるJOIN句。多くの開発者がINNER JOINLEFT JOINを日常的に使っていますが、その挙動の違いや、条件をON句に書くかWHERE句に書くかで結果が根本的に変わるケースについて、自信を持って説明できるでしょうか?

このONWHEREの違いは、応用情報技術者試験やデータベーススペシャリスト試験の午後問題で頻出の「ひっかけ問題」であると同時に、実務で意図しないバグを生み出す原因にもなります。

このセクションでは、基本的なJOINの種類をおさらいしつつ、最重要ポイントであるON句とWHERE句の使い分けについて徹底的に解説します。

JOINの種類とそれぞれの役割

まずは主要なJOINの種類を、ベン図のイメージと共に確認しましょう。

JOINの種類 役割 イメージ
INNER JOIN 内部結合 2つのテーブルで、結合キーが一致する行だけを返す。
LEFT JOIN 左外部結合 左テーブルの全行と、それに一致する右テーブルの行を返す。一致しない場合は右側がNULLになる。
RIGHT JOIN 右外部結合 右テーブルの全行と、それに一致する左テーブルの行を返す。LEFT JOINの左右逆バージョン。
FULL OUTER JOIN 完全外部結合 両方のテーブルの全行を返す。どちらか一方にしか存在しない行もNULLで補完して表示する。
SELF JOIN 自己結合 1つのテーブルを、別名(エイリアス)を付けて自分自身と結合するテクニック。組織図などで使う。

LEFT JOINは「主テーブルのデータは全部見たいが、関連するデータがあればそれも欲しい」という場面で非常に役立ちます。

最重要ポイント:ON句とWHERE句の条件の違い

ON句とWHERE句はどちらも「条件」を指定しますが、その役割と実行タイミングが全く異なります。

  • ON: テーブルを結合するための条件。どの行とどの行を紐付けるかを定義します。
  • WHERE: 結合が完了した後の仮想的な巨大テーブルから、最終的に表示する行を絞り込むための条件

LEFT JOINの場合:ONWHEREで結果が全く異なる!

📖 具体例:「全部署」と「所属する"正社員"」を一覧表示したい

1. 条件をON句に書いた場合

SELECT d.dept_name, e.name, e.status
FROM departments d
LEFT JOIN employees e
    ON d.dept_id = e.dept_id AND e.status = '正社員'; -- ON句で絞り込み

結果: 全部署が表示される。部署に正社員がいない場合、従業員名はNULLとなる。「全部署を基軸に、もし正社員がいれば表示する」という要件通りの結果になります。

2. 条件をWHERE句に書いた場合

SELECT d.dept_name, e.name, e.status
FROM departments d
LEFT JOIN employees e
    ON d.dept_id = e.dept_id
WHERE e.status = '正社員'; -- WHERE句で絞り込み

結果: `LEFT JOIN`によって生成されたNULLの行がWHERE句で除外されてしまい、INNER JOINと同じ結果になります。「正社員がいる部署」だけが表示されます。

INNER JOINでの動作 LEFT JOINでの動作
ON句の条件 結合する行を定義。(結果はWHEREと同じ) 先に右テーブルを絞り込んでから結合。左テーブルの行は全て残る。
WHERE句の条件 結合の結果セットを絞り込む。 結合の結果セットを絞り込む。右テーブルの条件で絞るとINNER JOINのようになる。

LATERAL JOINとAPPLY句│各行に関数のようにサブクエリを適用する

通常のJOINには、「FROM句に書かれたテーブルは、それより前に書かれたテーブルの列を参照できない」という制約があります。この問題を解決するのが、LATERAL JOIN (PostgreSQL, Oracle) と APPLY句 (SQL Server) です。

これらの構文を使うと、左側のテーブルの各行に対して、その行の値をパラメータとして右側のサブクエリを繰り返し実行できます。まるでプログラミングのfor-eachループのように動作します。

LATERAL JOIN (PostgreSQL / Oracle)

📖 具体例:各部署で給与が高い上位2名の従業員を取得する

SELECT
    d.dept_name,
    top_e.name,
    top_e.salary
FROM
    departments d,
LATERAL (
    SELECT e.name, e.salary
    FROM employees e
    WHERE e.dept_id = d.dept_id -- dの各行の値を参照
    ORDER BY e.salary DESC
    LIMIT 2
) AS top_e;

CROSS APPLY / OUTER APPLY (SQL Server)

SQL Serverでは、APPLY句が同様の機能を提供します。

  • CROSS APPLY: INNER JOINのように動作します。
  • OUTER APPLY: LEFT JOINのように動作します。

📖 具体例:CROSS APPLYで部署ごとのトップ2名を取得

SELECT
    d.dept_name,
    top_e.name,
    top_e.salary
FROM
    departments d
CROSS APPLY (
    SELECT e.name, e.salary
    FROM employees e
    WHERE e.dept_id = d.dept_id
    ORDER BY e.salary DESC
    OFFSET 0 ROWS FETCH NEXT 2 ROWS ONLY
) AS top_e;
構文 対応DBMS 動作のイメージ
LATERAL JOIN PostgreSQL, Oracle INNER JOINのように振る舞う。
LEFT JOIN LATERAL ... ON TRUE PostgreSQL, Oracle LEFT JOINのように振る舞う。
CROSS APPLY SQL Server INNER JOINLATERALの組み合わせに相当。
OUTER APPLY SQL Server LEFT JOINLATERALの組み合わせに相当。

GROUPING SETS/ROLLUP/CUBE入門│一度に多様な集計を実現するSQL

データ分析レポートでは、「地域ごとの売上」「商品カテゴリごとの売上」「全社合計の売上」といった様々な粒度での集計値が求められます。通常、これらを取得するには、それぞれ別のGROUP BYクエリを書いてUNION ALLで結合する必要がありました。この問題を解決するのが、GROUP BY句の拡張であるGROUPING SETS, ROLLUP, CUBEです。

GROUPING SETS:自由な組み合わせで集計

GROUPING SETSは、集計したいグループの組み合わせを自由に、明示的に指定できる最も柔軟な方法です。

📖 具体例:「地域別合計」「カテゴリ別合計」「総合計」を一度に取得

SELECT region, category, SUM(sales) AS total_sales
FROM sales_data
GROUP BY GROUPING SETS (
    (region),   -- 地域ごとの集計
    (category), -- カテゴリごとの集計
    ()          -- 総合計(空のタプルで指定)
);

ROLLUP:階層的な小計と合計を一度に

ROLLUPは、階層構造を持つデータの集計に特化したショートカットです。指定した列の組み合わせから、より上位の階層の小計、そして総計へと段階的に集計(ロールアップ)した結果を生成します。

📖 具体例:「都市別」「地域ごとの小計」「総合計」を一度に

SELECT region, city, SUM(sales) AS total_sales
FROM sales_data
GROUP BY ROLLUP(region, city);

CUBE:全パターンの組み合わせで集計

CUBEは、指定した列の考えられるすべての組み合わせについて集計を行います。多次元分析でクロス集計表を作成するような場合に便利です。

GROUPING()関数:集計行のNULLを判定する

集計によって生成されたNULLなのか、元々のデータがNULLなのかを区別するためにGROUPING()関数を使います。GROUPING(列名)は、その行が指定した列で集計されている場合は1を、そうでない場合は0を返します。

SELECT
    CASE WHEN GROUPING(region) = 1 THEN '総合計' ELSE region END AS region,
    CASE WHEN GROUPING(city) = 1 THEN '地域合計' ELSE city END AS city,
    SUM(sales) AS total_sales
FROM
    sales_data
GROUP BY ROLLUP(region, city);

WHERE句とHAVING句の違い│集計前後のフィルタリングを徹底解説

WHEREHAVINGは、どちらもデータを「絞り込む」ための句ですが、その役割と実行されるタイミングが根本的に異なります。

  • WHERE: GROUP BYで集計されるに、個々のを絞り込む。
  • HAVING: GROUP BYで集計されたに、その結果のグループを絞り込む。

🏫 学校のイベントで例えるWHEREHAVING`

WHERE句の役割は「参加資格」です。大会が始まるに、生徒一人ひとり(=)をチェックして参加者を絞り込みます。

HAVING`句の役割は「表彰条件」です。大会が終わったの集計された結果(=グループ)を見て、条件を満たすクラスを表彰します。

WHERE`句:`GROUP BY`前の「行」に対する条件

WHERE`句は、集計関数 (`SUM()`, `COUNT()`など) が計算されるよりも前に処理されるため、条件式の中で集計関数を使うことはできません

SELECT product_category, SUM(sales) AS total_sales
FROM sales_data
WHERE region = '関東' -- まず個々の行を「関東」に絞り込む
GROUP BY product_category;

HAVING`句:`GROUP BY`後の「グループ」に対する条件

HAVING`句は、GROUP BY`によって集計・グループ化された結果セットに対して絞り込みを行うため、集計関数を使うのが一般的です。

SELECT product_category, SUM(sales) AS total_sales
FROM sales_data
WHERE region = '関東'
GROUP BY product_category
HAVING SUM(sales) > 1000000; -- 集計後の結果(グループ)を絞り込む

試験で狙われやすい誤解パターンとまとめ

SQLの内部的な実行順序は FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY です。

WHERE HAVING
処理タイミング GROUP BYの前 GROUP BYの後
対象 個々の (Rows) 集約されたグループ (Groups)
集計関数の使用 不可 可能
GROUP BYの要否 不要 原則として必要

PIVOT/UNPIVOTによる行列変換│SQLのCASE式を使った書き方も解説

データベースではデータを「縦持ち」(1行1ファクト)で保持するのが一般的ですが、レポートなどでは「横持ち」(クロス集計表)の方が見やすい場合があります。この「縦持ち」と「横持ち」を相互に変換する操作がPIVOTUNPIVOTです。

PIVOT:縦持ちデータを横持ちデータに変換

PIVOTは、特定の列の値を新しい列名として展開し、指定した集計を行うことで、データを縦から横へ回転させます。

書き方1:PIVOT演算子を使う(SQL Server / Oracleなど)

SELECT year, [1] AS Jan, [2] AS Feb, [3] AS Mar
FROM
    monthly_sales
PIVOT (
    SUM(sales)          -- 何を集計するか
    FOR month           -- どの列を新しい列名にするか
    IN ([1], [2], [3])  -- 新しい列名になる値のリスト
) AS PivotTable;

書き方2:CASE式を使う(標準SQL)

PIVOTをサポートしないDBMSでは、GROUP BYCASE式を組み合わせることで同じ結果を得られます。

SELECT
    year,
    SUM(CASE WHEN month = 1 THEN sales ELSE 0 END) AS Jan,
    SUM(CASE WHEN month = 2 THEN sales ELSE 0 END) AS Feb,
    SUM(CASE WHEN month = 3 THEN sales ELSE 0 END) AS Mar
FROM
    monthly_sales
GROUP BY
    year;

UNPIVOT:横持ちデータを縦持ちデータに変換

UNPIVOTPIVOTの逆で、複数の列を行に変換します。

書き方1:UNPIVOT`演算子を使う(SQL Server / Oracleなど)

SELECT student_name, subject, score
FROM
    grades_wide
UNPIVOT (
    score                -- 値が入る列
    FOR subject          -- カテゴリが入る列
    IN (kokugo, sugaku)  -- 縦に展開したい元の列リスト
) AS UnpivotTable;

書き方2:UNION ALL`を使う(標準SQL)

SELECT student_name, '国語' AS subject, kokugo AS score FROM grades_wide
UNION ALL
SELECT student_name, '数学' AS subject, sugaku AS score FROM grades_wide;

集合演算子UNION, INTERSECT, EXCEPTの使い分け│JOINとの違いも解説

JOINがテーブルを横に(列を)結合するのに対し、集合演算子は複数のSELECT文の結果セットを縦に(行を)結合したり、比較したりします。操作の前提として、各SELECT文の列の数と、対応する列のデータ型が一致している必要があります。

UNION / UNION ALL:和集合(結果セットの足し算)

  • UNION: 重複する行を排除して結果を返します。
  • UNION ALL: 重複する行をそのまま残して結果を返します。こちらの方が高速です。
SELECT product_id, amount FROM sales_2024
UNION ALL
SELECT product_id, amount FROM sales_2023;

INTERSECT:積集合(共通部分の抽出)

2つのSELECT文の結果セットに共通して存在する行だけを返します。

SELECT user_id FROM premium_users
INTERSECT
SELECT user_id FROM newsletter_subscribers;

EXCEPT` / `MINUS`:差集合(片方だけに存在する部分の抽出)

最初のSELECT`文の結果から、2番目のSELECT`文の結果に含まれる行を取り除いた結果を返します。Oracleでは`MINUS`が使われます。

SELECT employee_id FROM employees
EXCEPT
SELECT manager_id FROM departments;

JOIN`との決定的な違い

集合演算子 (Set Operators) JOIN
結合方向 縦 (Vertical) - 行を追加する 横 (Horizontal) - 列を追加する
対象 2つ以上のSELECT文の結果セット 2つ以上のテーブル
前提条件 列の数とデータ型が一致する必要がある 関連するキー列で結合条件を定義する

SQLでのJSON処理とテーブル化│JSON_TABLE/OPENJSONで非構造化データを扱う

最近の主要なデータベースは、SQLで直接JSONを操作するための強力な関数を備えています。これにより、SQLのパワー(JOINや集計など)をJSONデータに対しても活用できます。

JSONデータからの値の抽出

まずは基本として、JSONオブジェクトから特定のキーの値を取り出す方法です。$.キー名のような「JSONパス式」を使って、値の場所を指定します。

📖 具体例:ユーザープロファイル(JSON)から名前と都市を抽出

MySQL / MariaDB の場合

SELECT
    profile ->> '$.name' AS name, -- `->>` はテキストとして抽出
    profile ->> '$.address.city' AS city
FROM users;

SQL Server の場合

SELECT
    JSON_VALUE(profile, '$.name') AS name,
    JSON_VALUE(profile, '$.address.city') AS city
FROM users;

JSON配列のテーブル化

JSON関数の真価が発揮されるのが、JSON配列を行に展開する「テーブル化」です。

📖 具体例:注文JSONデータから注文明細をテーブル形式で取り出す

JSON_TABLE (Oracle / MySQL 8.0以降)

SELECT
    o.order_id,
    i.*
FROM
    orders o,
    JSON_TABLE(
        o.order_data,
        '$.items[*]' -- [*]ですべての配列要素を展開
        COLUMNS (
            product_id VARCHAR(10) PATH '$.prod_id',
            quantity INT PATH '$.qty'
        )
    ) AS i;

OPENJSON (SQL Server)

SELECT
    o.order_id,
    i.*
FROM
    orders o
CROSS APPLY OPENJSON(o.order_data, '$.items')
    WITH (
        product_id VARCHAR(10) '$.prod_id',
        quantity INT '$.qty'
    ) AS i;

OFFSET/FETCH/LIMITによるページング処理と安定ソートの重要性

Webサイトの検索結果のように、結果を分割して表示する機能をページングと呼びます。データベースから指定した範囲のデータだけを取得するためにOFFSET, FETCH, LIMIT句が使われます。

ページングの基本:OFFSET / FETCH / LIMIT

  • OFFSET: 先頭から指定した行数をスキップします。
  • FETCH / LIMIT: スキップした後の位置から、指定した行数だけを取得します。

📖 具体例:商品一覧を1ページ10件として、3ページ目のデータを取得する

MySQL / PostgreSQL の場合 (LIMIT ... OFFSET ...)

SELECT product_id, product_name, price
FROM products
ORDER BY created_at DESC
LIMIT 10 OFFSET 20;

SQL Standard / Oracle / SQL Server の場合 (OFFSET ... FETCH ...)

SELECT product_id, product_name, price
FROM products
ORDER BY created_at DESC
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;

安定ソートの重要性と鍵付きページング

OFFSETを使ったページングには、ORDER BYのキーに重複があった場合の順序が保証されないという落とし穴があります。これにより、ページ間で表示が重複・欠落する可能性があります。

解決策1:安定ソート (Stable Sort)

解決策は、ORDER BY句の最後に主キーなどのユニークな列を必ず追加し、並び順が一意に決まるようにすることです。

ORDER BY price DESC, product_id ASC;

解決策2:鍵付きページング (Keyset Paging)

OFFSETは件数が増えるとパフォーマンスが劣化します。その問題を解決するのが鍵付きページングです。「N件スキップする」のではなく、「最後に見た行の次の行からN件取得する」という考え方で、非常に高速に動作します。

SELECT product_id, product_name, price
FROM products
WHERE
    (price < 5000) OR (price = 5000 AND product_id > 123)
ORDER BY
    price DESC, product_id ASC
LIMIT 10;

実務で役立つSQL応用Tips│パフォーマンス・ロック競合・データ整合性の考慮点

実務の現場では、単に「正しい結果が返ってくる」だけでは不十分です。「いかに速く、安全に、そしてデータの一貫性を保ちながら」実行できるかが問われます。

パフォーマンスと実行計画

同じ結果を返すSQLでも、書き方によって実行速度が大きく変わります。その鍵を握るのが実行計画です。

  • EXPLAIN: このコマンドでデータベースが立てた実行計画を確認し、パフォーマンスチューニングの第一歩とします。
  • フルテーブルスキャンを避ける: 実行計画で「Full Table Scan」と表示された場合、インデックスが使えていないサインです。WHERE句の条件がインデックスの効く書き方(SARGable)になっているか確認しましょう。
  • インデックスの貼りすぎに注意: インデックスはSELECTを高速化しますが、INSERTUPDATEの性能を低下させる原因にもなります。

ロック競合を避ける工夫

複数人が同時にデータを更新するシステムでは、データの整合性を保つためにロックが働きます。このロックの待ち時間がロック競合です。

  • トランザクションは短く: BEGINからCOMMITまでの間は、本当に必要なデータベース操作だけに絞りましょう。
  • リソースへのアクセス順を統一する: アプリケーション全体でテーブルや行をロックする順番を統一すると、デッドロックの発生を減らせます。

データ整合性を守る制約

データの品質と一貫性を保証するため、データベース自身の制約(Constraints)機能を活用します。

  • PRIMARY KEY / UNIQUE`: 行の重複を防ぎます。
  • NOT NULL`: 必須項目を保証します。
  • CHECK`: 列が満たすべき条件を定義します (例: CHECK (price >= 0))。
  • FOREIGN KEY`(外部キー): テーブル間の関連性を保証し、「孤児レコード」の発生を防ぐ最も重要な制約です。

応用情報・DBスペシャリスト試験頻出の応用SQL句│ウィンドウ関数からCTEまで徹底解説

基本情報技術者試験で学ぶSELECT, WHERE, GROUP BYといった基本的なSQL句だけでは、応用情報やデータベーススペシャリストの午後問題、特に複雑なデータ分析や集計が求められる設問には対応しきれません。ここでは、実務でも頻繁に利用され、試験でも合否を分けるポイントとなる応用的なSQL句を解説します。

ウィンドウ関数:RANK/ROW_NUMBER/SUM/AVGによる高度な集計

ウィンドウ関数は、GROUP BYのように行を集約せず、元の行を残したままで集計や順位付けができる強力な機能です。OVER句を使って計算対象となる行の範囲(ウィンドウ)を動的に指定します。

  • 順位付け: RANK()(同順位を考慮)、DENSE_RANK()(同順位を考慮し、次の順位は連番)、ROW_NUMBER()(同順位でも一意の連番)を使い分け、売上ランキングや成績順リストを作成する問題で問われます。
  • 累積計算: SUM() OVER (ORDER BY ...)のように使い、年度ごとの累積売上や、時系列での在庫数の推移などを計算できます。
  • 移動平均: AVG() OVER (ORDER BY ... ROWS BETWEEN ...)を使うことで、直近3ヶ月の平均売上のような、一定範囲での平均値を算出する問題に対応できます。

集合演算子:UNION, INTERSECT, EXCEPT の違い

複数のSELECT文の結果を一つにまとめる際に使うのが集合演算子です。それぞれの役割を正確に理解しておく必要があります。

  • UNION: 2つの結果セットの和集合を返します(重複行は排除)。東京本社の顧客リストと大阪支社の顧客リストを名寄せして1つのリストにする、といったケースで使われます。
  • UNION ALL: UNIONと似ていますが、重複行を排除せずそのまま結合します。単純に結果を縦に連結したい場合や、パフォーマンスが重視される場面で有効です。
  • INTERSECT: 2つの結果セットの積集合を返します。つまり、両方に共通して存在する行のみを抽出します。「セミナーAにもセミナーBにも参加した顧客」を抽出する、といった設問で役立ちます。
  • EXCEPT: 2つの結果セットの差集合を返します。1つ目の結果セットに存在し、2つ目の結果セットには存在しない行を抽出します。「商品を購入したが、まだレビューを投稿していないユーザー」を探すようなケースで利用します。

GROUP BY の拡張:GROUPING SETS, ROLLUP, CUBE

GROUP BY句を拡張し、小計や総計といった複数の粒度の集計を一度のクエリで取得するための機能です。

  • ROLLUP: 指定した列の組み合わせで、階層的な小計と総計を算出します。「ROLLUP(年度, 四半期)」と指定すれば、「年度ごと・四半期ごとの集計」「年度ごとの小計」「全体の総計」が一度に得られます。
  • CUBE: 指定した列の考えうる全ての組み合わせで集計結果を算出します。「CUBE(商品カテゴリ, 地域)」とすれば、カテゴリ×地域の集計に加え、カテゴリごとの小計、地域ごとの小計、そして総計まで網羅的に出力できます。
  • GROUPING SETS: ROLLUPCUBEと異なり、開発者が必要な集計の組み合わせだけを明示的に指定できます。より柔軟な集計が可能です。

EXISTS と IN の使い分け│相関サブクエリの代表格

EXISTSは、サブクエリ(副問い合わせ)の結果セットに行が存在するかどうかを判定し、TRUEまたはFALSEを返す述語です。

  • EXISTSの強み: 「~という条件に合致するデータが一件でも存在すれば良い」という判定に使います。サブクエリ内で条件に合う行が1行見つかった時点で評価が終了するため、IN句よりも高速に動作することがあります。特に「一度でも商品Aを購入したことがある顧客」を抽出するような場合に有効です。
  • INとの違い: INはサブクエリの結果リストに「値が含まれているか」をチェックするのに対し、EXISTSは「行が存在するか」という存在有無そのものをチェックします。この違いがパフォーマンスやクエリの書き方に影響します。
  • NOT EXISTS: EXISTSの逆で、一件も行が存在しない場合にTRUEとなります。「まだ一度も購入履歴のない商品」を探す、といった問題で頻出です。

CTE(共通テーブル式):WITH句によるSQLの可読性向上

CTE (Common Table Expression) は、WITH句を使って、複雑なSQL文を一時的な名前付きの結果セット(テーブル)に分割して記述する機能です。

  • メリット:
    • 可読性の向上: 長大でネストが深いSQLを、処理のステップごとに部品化できるため、コードが非常に読みやすくなります。
    • 再利用性: 一度定義したCTEを、同じクエリ内で何度も参照できます。
  • 再帰CTE: WITH RECURSIVEを使うことで、CTEが自分自身を参照する「再帰クエリ」を記述できます。これは、組織図の階層をたどったり、部品表(BOM)の親子関係を展開したりするような、階層構造データを扱う問題で必須のテクニックです。

MERGE(UPSERT)文による条件付きINSERT/UPDATE

MERGE文は、対象のテーブルにデータが存在するかどうかを条件に、UPDATE処理とINSERT処理を自動で振り分ける機能です。UPSERT(UPDATE or INSERT)とも呼ばれます。

  • 利用シーン: 日次バッチ処理でマスタデータを更新する際、「社員番号が既に存在すれば情報を更新(UPDATE)し、存在しなければ新規追加(INSERT)する」といった処理を、1つのSQL文で完結させることができます。これにより、処理の記述がシンプルになり、パフォーマンス向上も期待できます。

WHERE と HAVING の決定的違い│絞り込みのタイミング

どちらも行を絞り込むための句ですが、その処理タイミングが決定的に異なります。

  • WHERE: GROUP BYで集約を行うに、元のテーブルの各行に対して絞り込みを行います。
  • HAVING: GROUP BYで集約を行ったの結果セットに対して、絞り込みを行います。

このため、SUM()AVG()といった集計関数を条件に使えるのはHAVING句だけです。「合計金額が10,000円以上の顧客グループのみを抽出する」といった条件は、集約後でないと評価できないためHAVING句で記述します。

午後試験の罠:JOIN と UNION の混同に注意

最後に、午後試験で受験者が陥りやすいひっかけポイントです。

  • JOIN: テーブルを横方向(列方向)に連結します。顧客テーブルと注文テーブルを顧客IDで紐づけて、顧客名と注文日を並べて表示するようなケースで使います。
  • UNION: 結果セットを縦方向(行方向)に連結します。アクティブ会員テーブルと休眠会員テーブルを縦に繋いで、全会員リストを作成するようなケースで使います。

問題文をよく読み、「複数のテーブルから関連情報を紐づけて列を増やしたいのか」、それとも「構造が同じ複数の結果を一つにまとめて行を増やしたいのか」を正確に見極めることが、正解への鍵となります。

-IPA|情報処理技術者試験