データベーススペシャリスト試験や応用情報技術者試験の学習で登場する「LATERAL JOIN」や「APPLY 句」。参考書を読んでも「各行に関数のようにサブクエリを適用する」という説明だけでは、具体的なイメージが湧きにくいと感じていませんか?「通常の JOIN や相関サブクエリと何が違うの?」「どんな場面で使うと便利なの?」といった疑問は、多くの学習者が抱える共通の悩みです。
これらの構文は、SQL の表現力を飛躍的に向上させる強力な武器ですが、その概念を掴むには少しコツがいります。本質は「FROM 句で指定したテーブルの行を 1 行ずつ取り出し、その行の値をパラメータとしてサブクエリを実行する」というループ処理のようなイメージです。
この記事では、LATERAL JOIN と APPLY 句が持つこの「ループ処理」のイメージを、豊富な図解と具体的な SQL を使って徹底的に可視化します。読み終える頃には、複雑なデータ抽出要件にもエレガントに対応できる SQL の新たな扉が開かれているはずです。午後試験の難解な問い合わせはもちろん、実務でのデータ分析やレポーティング業務の効率化にも直結するこの強力な機能を、ぜひマスターしましょう。
目次
LATERAL JOIN / APPLY句が必要な理由│相関サブクエリの限界を超える午後試験対策
SQLを学習すると、INNER JOINやLEFT JOIN、そしてGROUP BYといった強力な道具を手にします。しかし、実務やデータベーススペシャリスト試験の午後問題では、これらだけではエレガントに解決できない複雑な要件に遭遇します。それが「グループごとの上位 N 件を取得したい」という典型的なシナリオです。
例えば、「各部署に所属する社員のうち、給与が高いトップ 3 名の情報をすべて取得したい」と考えてみましょう。
通常のJOINとGROUP BYの限界
GROUP BYを使えば、「各部署の最高給与額」を求めることは簡単です。
SELECT
department_id,
MAX(salary) AS max_salary
FROM
employees
GROUP BY
department_id;
しかし、この結果は「誰が」その給与を受け取っているのか分かりません。employee_idやemployee_nameを追加しようとすると、GROUP BY句にも追加する必要があり、意図した結果とは異なってしまいます。JOINを駆使して自己結合を繰り返したり、ウィンドウ関数 (ROW_NUMBER()) を使ったりする方法もありますが、SQLが複雑になりがちです。
相関サブクエリの限界
「じゃあ、行ごとにサブクエリを実行する相関サブクエリならどうだろう?」と考えるかもしれません。確かに相関サブクエリは便利ですが、決定的な制約があります。
- 単一の値しか返せない(スカラ・サブクエリ):
SELECT句に書く相関サブクエリは、結果が 1 行 1 列の値でなければエラーになります。「トップ 3 名」のような複数行の結果を返すことはできません。 - FROM句で使えない: 相関サブクエリは
SELECT句やWHERE句で主問い合わせの列を参照しますが、FROM句の中で主問い合わせの別のテーブルの列を参照することはできません。
このように、既存の機能だけでは「主問い合わせの各行をパラメータとして、別のテーブルや関数を呼び出し、その複数行・複数列の結果セットを結合する」という処理を直感的に書くことが困難でした。
この課題を解決するために登場したのが、LATERAL JOIN(PostgreSQL, Oracleなど)とAPPLY句(SQL Server, Oracleなど)なのです。これらは、FROM句の中で相関サブクエリのように振る舞うことを可能にし、SQLの表現力を大きく広げました。まさに「各行に関数のようにサブクエリを適用する」ための機能と言えるでしょう。
LATERAL JOINの仕組みと構文│図解で学ぶ「行ごとループ処理」のSQL
LATERAL JOINの動きを理解する最良の方法は、プログラミング言語における「For-Each ループ」をイメージすることです。つまり、テーブル A の各行に対して、その行のデータを使ってテーブル B に対する問い合わせ(サブクエリ)を実行し、結果を結合するという処理を繰り返します。
言葉だけでは分かりにくいので、具体的なテーブルと SQL で処理フローを追ってみましょう。
サンプルテーブル
ここでは、シンプルな「部署」テーブルと「従業員」テーブルを使用します。
departments テーブル
| department_id | department_name |
|---|---|
| 1 | 営業部 |
| 2 | 開発部 |
| 3 | 人事部 |
employees テーブル
| employee_id | employee_name | department_id | salary |
|---|---|---|---|
| 101 | 田中 | 1 | 800 |
| 102 | 鈴木 | 1 | 750 |
| 103 | 佐藤 | 1 | 780 |
| 104 | 高橋 | 1 | 700 |
| 201 | 伊藤 | 2 | 900 |
| 202 | 渡辺 | 2 | 850 |
| 203 | 山本 | 2 | 920 |
| 301 | 中村 | 3 | 600 |
「各部署の給与トップ2社員」を取得するSQL
このデータから「各部署の給与トップ 2 名」を取得する SQL (PostgreSQL, Oracle 構文) は以下のようになります。
SELECT
d.department_name,
e_top.employee_name,
e_top.salary
FROM
departments AS d, -- Oracleの場合はカンマ区切りも可
LATERAL (
SELECT
e.employee_name,
e.salary
FROM
employees AS e
WHERE
e.department_id = d.department_id -- ★外側のd.department_idを参照
ORDER BY
e.salary DESC
LIMIT 2 -- PostgreSQLの場合。Oracleでは FETCH FIRST 2 ROWS ONLY
) AS e_top;
※社員が一人もいない部署も表示したい場合は LEFT JOIN LATERAL (...) ON true; とします。
図解:LATERAL JOINの内部的な処理フロー
この SQL がどのように実行されるかを、図と共にステップ・バイ・ステップで見ていきましょう。
- 【ループ開始】 まず、主問い合わせの
FROM departments AS dが評価され、departmentsテーブルの 1行目(department_id=1, 営業部)が取り出されます。 - 【サブクエリ実行】 次に
LATERAL句の中のサブクエリが実行されます。このとき、1.で取り出されたd.department_idの値(1)が、サブクエリ内のWHERE e.department_id = d.department_idのd.department_idに引き渡されます。 - 【結果取得】 サブクエリは実質的に「営業部 (
department_id=1) の社員を給与の高い順に 2 名取得する」という処理になり、田中 (800)と佐藤 (780)の 2 行が結果として返されます。 - 【結果の結合】
departmentsの 1 行目(営業部)と、サブクエリの結果 2 行が結合され、最終的な結果セットに 2 行が追加されます。 - 【次のループへ】
departmentsテーブルの 2行目(department_id=2, 開発部)が取り出され、再び 2.~4. の処理が繰り返されます。開発部の場合は山本 (920)と伊藤 (900)が取得・結合されます。 - 【ループ終了】
departmentsテーブルのすべての行についてこの処理が完了するまで繰り返されます。
このように LATERAL というキーワードは、「(主問い合わせの)横で、行ごとにサブクエリを都度実行する」というイメージを持つと、その強力な機能が理解しやすくなります。
APPLY句の使い方 (SQL Server)│CROSS APPLYとOUTER APPLYを徹底解説
PostgreSQL や Oracle で LATERAL JOIN が導入される前から、SQL Server には同様の機能を実現する APPLY 句が存在しました。APPLY句は、LATERAL JOIN と同じく「行ごとに関数やサブクエリを適用する」という強力な機能を提供します。
APPLY には CROSS APPLY と OUTER APPLY の 2 種類があり、それぞれ通常の JOIN における INNER JOIN と LEFT OUTER JOIN の関係に似ています。
CROSS APPLY:INNER JOINのように振る舞う
CROSS APPLY は、INNER JOIN や JOIN LATERAL と同じように動作します。つまり、左側(主)のテーブルの各行に対して右側のサブクエリを評価し、サブクエリが 1 行以上の結果を返した場合にのみ、その行を結果セットに含めます。
先の「各部署の給与トップ2社員」の例を SQL Server の構文で書くと以下のようになります。
SELECT
d.department_name,
e_top.employee_name,
e_top.salary
FROM
departments AS d
CROSS APPLY (
SELECT
e.employee_name,
e.salary
FROM
employees AS e
WHERE
e.department_id = d.department_id
ORDER BY
e.salary DESC
OFFSET 0 ROWS FETCH NEXT 2 ROWS ONLY -- SQL Server 2012以降
) AS e_top;
もし、所属する社員が一人もいない部署(例えば新設された「総務部」)があった場合、その部署はサブクエリの結果が 0 行となるため、CROSS APPLY では最終的な結果から除外されます。
OUTER APPLY:LEFT JOINのように振る舞う
一方、OUTER APPLY は LEFT OUTER JOIN や LEFT JOIN LATERAL と同じように動作します。CROSS APPLY との決定的な違いは、サブクエリの結果が 0 行だったとしても、左側(主)のテーブルの行は結果セットに残る点です。その際、サブクエリから返されるはずだった列は NULL で補完されます。
先ほどのクエリを OUTER APPLY に変更してみましょう。
SELECT
d.department_name,
e_top.employee_name,
e_top.salary
FROM
departments AS d
OUTER APPLY ( -- ここを変更
SELECT
e.employee_name,
e.salary
FROM
employees AS e
WHERE
e.department_id = d.department_id
ORDER BY
e.salary DESC
OFFSET 0 ROWS FETCH NEXT 2 ROWS ONLY
) AS e_top;
このクエリであれば、社員が一人もいない「総務部」も結果に表示され、employee_name と salary の列には NULL が入ります。これは、「全〇〇の一覧を出しつつ、関連する上位 N 件の情報もあれば表示したい」といった要件で非常に役立ちます。
APPLY と LATERAL JOIN の関係まとめ
これらの関係性を表にまとめると、以下のようになります。
| 構文 | 動作のイメージ | サブクエリから0件の場合の挙動 |
|---|---|---|
JOIN LATERAL |
INNER JOIN 相当 |
主テーブルの行も結果から除外される |
LEFT JOIN LATERAL |
LEFT JOIN 相当 |
主テーブルの行は NULL 埋めで残る |
CROSS APPLY |
INNER JOIN 相当 |
主テーブルの行も結果から除外される |
OUTER APPLY |
LEFT JOIN 相当 |
主テーブルの行は NULL 埋めで残る |
基本的には、標準SQL準拠の LATERAL JOIN と、SQL Server独自の APPLY はほぼ同じ機能を提供していると理解して問題ありません。どちらの構文に触れる機会が多いかは、普段利用しているデータベース製品によります。
実践的な活用シナリオとDBスペシャリスト午後試験対策
LATERAL JOIN や APPLY は、単なるSQLのテクニックに留まらず、実際の業務システムや試験問題で頻出する「トップN問題」をエレガントに解決するための強力な思考ツールです。ここでは、より具体的なシナリオを見ていきましょう。
シナリオ1:ECサイト - カテゴリごとの最新レビューを3件取得
要件: Webサイトに商品カテゴリの一覧を表示し、各カテゴリについてユーザーから投稿された最新のレビューを3件ずつ表示したい。
この要件では、レビューがまだ投稿されていないカテゴリも表示する必要があるため、LEFT JOIN LATERAL や OUTER APPLY が最適です。
-- PostgreSQL / Oracle
SELECT
c.category_name,
r_latest.review_text,
r_latest.created_at
FROM
product_categories AS c
LEFT JOIN LATERAL (
SELECT
r.review_text,
r.created_at
FROM
reviews AS r
WHERE
r.category_id = c.category_id
ORDER BY
r.created_at DESC
FETCH FIRST 3 ROWS ONLY -- Oracle構文
) AS r_latest ON true;
このSQLにより、カテゴリごとに最新のレビューを取得しつつ、レビューが0件のカテゴリもリストから漏れることがありません。
シナリオ2:金融システム - 各顧客の直近の取引履歴を5件取得
要件: 顧客管理画面で、特定の顧客情報と共に、その顧客が行った直近5回の取引履歴(入出金など)を素早く表示したい。
この場合も、新規登録したばかりで取引履歴がまだない顧客を考慮して OUTER APPLY を使うのが堅牢な設計と言えます。
-- SQL Server
SELECT
c.customer_name,
t_recent.transaction_date,
t_recent.amount
FROM
customers AS c
OUTER APPLY (
SELECT
t.transaction_date,
t.amount
FROM
transactions AS t
WHERE
t.customer_id = c.customer_id
ORDER BY
t.transaction_date DESC
OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY
) AS t_recent;
データベーススペシャリスト午後試験での出題イメージ
データベーススペシャリスト試験、特に午後Ⅰ・午後Ⅱでは、LATERAL JOIN や APPLY の知識が直接問われるSQL記述問題が頻出します。
設問例:
「各支店(支店マスタ)について、その支店で受注した最新3件の受注情報(受注伝票)を、支店名、受注日、受注金額の形式で取得するSELECT文を答えよ。なお、受注が1件もない支店も支店名を表示すること。」
解答への思考プロセス:
- 主テーブルの特定: まず、「各支店について」とあるので、
FROM句の主役は支店マスタだと判断します。 - 行ごと処理の発想: 「各支店ごとに」「最新3件」というキーワードから、
GROUP BYでは解けないトップN問題であると見抜きます。ここで行ごとのループ処理が可能なLATERAL JOIN/APPLYを使う方針を立てます。 - 外部結合の選択: 「受注が1件もない支店も表示」という要件から、内部結合系の
CROSS APPLYやJOIN LATERALではなく、外部結合系のOUTER APPLYまたはLEFT JOIN LATERALを選択します。 - サブクエリの構築:
APPLY句(またはLATERAL句)の中で、受注伝票テーブルを対象とし、WHERE句で支店マスタ.支店コード = 受注伝票.支店コードという相関条件を記述します。 - 順序と件数:
ORDER BY 受注日 DESCで新しい順に並べ替え、FETCH FIRST 3 ROWS ONLYなどで件数を絞り込みます。
この思考プロセスを身につけることで、複雑に見える問題文も構造的に分解し、正解のSQLをスムーズに導き出せるようになります。
まとめ│「行ごとのループ処理」という武器を手に入れる
本記事では、LATERAL JOIN と APPLY 句について、その必要性から具体的な処理フロー、そして実践的な活用シナリオまでを解説しました。最後に、重要なポイントを振り返りましょう。
- 核心的なイメージ:
LATERAL JOINとAPPLYの本質は、「主テーブルの各行をループの起点とし、その行の値をパラメータとしてサブクエリを実行する」という考え方です。この「行ごとのループ処理」のイメージが理解の鍵となります。 - 存在理由: 従来の
JOINやGROUP BY、相関サブクエリでは困難だった「グループごとのトップN件」のような複雑な問い合わせを、直感的かつシンプルに記述するために生まれました。 - 構文の使い分け:
LATERAL JOIN(PostgreSQL, Oracle等): 標準SQLに近い構文です。APPLY(SQL Server等):CROSS APPLYとOUTER APPLYがあります。
- 結合方法の選択:
- サブクエリから対応するデータが見つからない行を除外したい場合 →
JOIN LATERALまたはCROSS APPLY(INNER JOIN相当) - データが見つからなくても主テーブルの行は残したい場合 →
LEFT JOIN LATERALまたはOUTER APPLY(LEFT JOIN相当)
- サブクエリから対応するデータが見つからない行を除外したい場合 →
データベーススペシャリスト試験の午後問題や、実務で複雑なデータを扱う際、「〇〇ごとに最新の△△を N 件…」といった要件が出てきたら、真っ先に LATERAL JOIN や APPLY を思い出せるように訓練しておくことが、合格への、そしてスキルアップへの近道です。
この強力な機能を使いこなし、SQLによるデータ操作の可能性をさらに広げていきましょう。