【Oracle】LATERAL・CROSS APPLY・OUTER APPLY完全ガイド|Oracle 12c 以降の外部参照サブクエリ・グループ内 Top-N まで解説

通常の SQL サブクエリは、FROM 句に書いたインラインビューから外側のテーブルの列を参照できません。Oracle 12c で導入された LATERAL 句を使うと、インラインビューの中から外側のテーブルの列を参照した相関サブクエリをインラインビューとして書けます。

また CROSS APPLYOUTER APPLY はSQL Server でおなじみの構文ですが、Oracle 12c 以降でも使えます。CROSS APPLY は INNER JOIN + LATERAL に、OUTER APPLY は LEFT OUTER JOIN + LATERAL に相当します。

この記事でわかること

  • LATERAL で FROM 句のインラインビューが外側テーブルの列を参照する方法
  • CROSS APPLY・OUTER APPLY の構文と動作の違い
  • グループ内 Top-N(各部門の給与上位3名)を LATERAL で簡潔に書く方法
  • 従来の RANK() OVER・相関サブクエリとの比較
  • XMLTABLE・DBMS_RANDOM と LATERAL の組み合わせ
スポンサーリンク

LATERAL の基本:インラインビューから外側列を参照する

LATERAL なし(エラー)と LATERAL あり(外側列を参照)の比較
-- 通常のインラインビュー: 外側テーブルの列を参照できない(エラー)
SELECT d.department_name, e.max_salary
FROM departments d,
     (SELECT MAX(salary) AS max_salary
      FROM employees
      WHERE department_id = d.department_id   -- ORA-00904: d.department_id は参照不可
     ) e;

-- LATERAL を付けると外側テーブル(d)の列を参照できる
SELECT d.department_name, e.max_salary
FROM departments d,
     LATERAL (
         SELECT MAX(salary) AS max_salary
         FROM employees
         WHERE department_id = d.department_id   -- d.department_id を参照できる
     ) e;
-- 各部門の最高給与を1行で取得できる

-- INNER JOIN で LATERAL を使う(同等の書き方)
SELECT d.department_name, e.max_salary
FROM departments d
JOIN LATERAL (
    SELECT MAX(salary) AS max_salary, COUNT(*) AS emp_count
    FROM employees
    WHERE department_id = d.department_id
) e ON (1=1);
-- ON (1=1): LATERAL との結合では結合条件を 1=1(常に真)にする

CROSS APPLY と OUTER APPLY

CROSS APPLY は INNER JOIN + LATERAL に相当します。右側のインラインビューが行を返さない場合、その行は結果に含まれません(INNER JOIN の動作)。

OUTER APPLY は LEFT OUTER JOIN + LATERAL に相当します。右側のインラインビューが行を返さない場合でも、左側のテーブルの行が残り、右側の列は NULL になります。

CROSS APPLY と OUTER APPLY の比較
-- CROSS APPLY: インラインビューが行を返す部門のみ結果に含まれる(INNER JOIN 相当)
SELECT d.department_name, top3.last_name, top3.salary
FROM departments d
CROSS APPLY (
    SELECT last_name, salary
    FROM employees
    WHERE department_id = d.department_id
    ORDER BY salary DESC
    FETCH FIRST 3 ROWS ONLY    -- 各部門の給与上位3名
) top3
ORDER BY d.department_name, top3.salary DESC;
-- 社員が1人もいない部門は結果に含まれない

-- OUTER APPLY: 社員がいない部門は NULL として残る(LEFT OUTER JOIN 相当)
SELECT d.department_name,
       top3.last_name,
       NVL(TO_CHAR(top3.salary), '社員なし') AS salary_info
FROM departments d
OUTER APPLY (
    SELECT last_name, salary
    FROM employees
    WHERE department_id = d.department_id
    ORDER BY salary DESC
    FETCH FIRST 3 ROWS ONLY
) top3
ORDER BY d.department_name, top3.salary DESC NULLS LAST;
-- 社員がいない部門は last_name = NULL, salary = NULL として出力される

-- CROSS APPLY は以下の LATERAL + INNER JOIN と同等
SELECT d.department_name, top3.last_name, top3.salary
FROM departments d
JOIN LATERAL (
    SELECT last_name, salary FROM employees
    WHERE department_id = d.department_id
    ORDER BY salary DESC FETCH FIRST 3 ROWS ONLY
) top3 ON (1=1);

グループ内 Top-N を LATERAL で書く

「各部門の給与上位 3 名を取得する」といった「グループ内 Top-N」問題は、LATERAL(または CROSS APPLY)を使うと非常にシンプルに書けます。

グループ内 Top-N:LATERAL vs 従来の方法
-- 方法 1: CROSS APPLY + FETCH FIRST(Oracle 12c 以降、シンプル)
SELECT d.department_name, e.last_name, e.salary
FROM departments d
CROSS APPLY (
    SELECT last_name, salary
    FROM employees
    WHERE department_id = d.department_id
    ORDER BY salary DESC
    FETCH FIRST 3 ROWS ONLY
) e
ORDER BY d.department_name, e.salary DESC;

-- 方法 2: RANK() OVER を使った従来の方法(全バージョンで使える)
SELECT department_name, last_name, salary
FROM (
    SELECT
        d.department_name,
        e.last_name,
        e.salary,
        RANK() OVER (PARTITION BY e.department_id ORDER BY e.salary DESC) AS salary_rank
    FROM employees e
    JOIN departments d ON e.department_id = d.department_id
)
WHERE salary_rank <= 3
ORDER BY department_name, salary DESC;
-- RANK() は同順位が複数いると3位が複数になる場合がある
-- FETCH FIRST ROWS ONLY の方が直感的で Ties の扱いが明確

-- 方法 3: 相関サブクエリによる従来の方法(記述が複雑)
SELECT d.department_name, e.last_name, e.salary
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE (
    SELECT COUNT(*)
    FROM employees e2
    WHERE e2.department_id = e.department_id
      AND e2.salary > e.salary
) < 3
ORDER BY d.department_name, e.salary DESC;

LATERAL の応用:行生成・XMLTABLE との組み合わせ

LATERAL と CONNECT BY / DBMS_RANDOM の組み合わせ
-- LATERAL で CONNECT BY LEVEL を外部テーブルと組み合わせる(行生成)
-- 各ユーザーの最近 N 日分のデータを生成する
SELECT
    u.user_id,
    u.user_name,
    d.day_offset,
    SYSDATE - d.day_offset AS log_date
FROM users u
CROSS APPLY (
    SELECT LEVEL - 1 AS day_offset
    FROM DUAL
    CONNECT BY LEVEL <= u.report_days   -- u.report_days: ユーザーごとの日数(外部参照)
) d
ORDER BY u.user_id, d.day_offset;
-- 通常の CONNECT BY では外部テーブルの列(report_days)を参照できないが LATERAL で可能に

-- LATERAL と XMLTABLE: 各行の XML 列を個別に展開する
-- (XMLTABLE は内部的に LATERAL と同等の動作をするため直接参照可能)
SELECT p.order_id, x.product_id, x.quantity
FROM purchase_orders p,
     XMLTABLE(
         '$doc/PurchaseOrder/Items/Item'
         PASSING p.xml_data AS "doc"
         COLUMNS
             product_id VARCHAR2(20) PATH 'ProductId',
             quantity   NUMBER       PATH 'Quantity'
     ) x;
-- XMLTABLE は暗黙的に LATERAL と同様に外側テーブルの列を参照できる

まとめ

  • LATERAL:FROM 句のインラインビューが外側テーブルの列を参照できる。Oracle 12c 以降で使用可能
  • CROSS APPLY:INNER JOIN + LATERAL に相当。右側が行を返さない場合は結果から除外される
  • OUTER APPLY:LEFT OUTER JOIN + LATERAL に相当。右側が空でも左側の行を NULL で補完して返す
  • グループ内 Top-N:CROSS APPLY + FETCH FIRST を使うと、RANK() OVER や相関サブクエリより直感的に書ける
  • 行生成:CONNECT BY LEVEL を LATERAL に入れると、外側テーブルの列(行数)に応じた可変行数の生成が可能
  • XMLTABLE / UNNEST:Oracle の XMLTABLE や JSON_TABLE は内部的に LATERAL に近い動作をするため、FROM 句で直接使える

グループ内での順位付けには分析関数(RANK / DENSE_RANK / ROW_NUMBER)も有効です。Oracle 分析関数完全ガイドを参照してください。サブクエリの活用パターン全般は Oracle サブクエリ完全ガイドも参照してください。