SQL ServerのLEFT JOINとOUTER APPLYの違い|最新1件・同順位・性能比較の注意点

この記事のコードはSQL Server/Azure SQLのT-SQLを対象とします。LEFT JOINは一致する右側の行を結合し、OUTER APPLYは左側の値を参照する右側の式やテーブル値関数を使えます。速さだけで選ぶ前に「全注文」か「顧客ごとに最新1件」か、返したい結果を決めます。

SQLのクエリパフォーマンスを最適化するためには、適切な結合方法を選択することが重要です。特にLEFT JOINとOUTER APPLYは、似ているようで異なる特性を持つため、使い分けを理解することが求められます。本記事では、LEFT JOINとOUTER APPLYの基本概念、使い方、それぞれの違いと効率的な使用方法について詳しく説明します。これにより、複雑なクエリのパフォーマンスを向上させ、データベース操作をより効果的に行うための知識を提供します。

目次

LEFT JOINの基本と使い方

LEFT JOINは、二つのテーブルを結合する際に、左側のテーブルのすべての行と、右側のテーブルの一致する行を返します。右側に一致する行がない場合は、右側の列がNULLの行として左側の行を残します。一致する右側の行が複数あれば、左側の行もその件数分現れます。

LEFT JOINの基本構文

基本的なLEFT JOINの構文は以下の通りです:


SELECT 
    A.column1, A.column2, B.column1, B.column2
FROM 
    TableA A
LEFT JOIN 
    TableB B
ON 
    A.key = B.key;

LEFT JOINの使用例

以下は、顧客テーブルと注文テーブルをLEFT JOINで結合する例です。すべての顧客をリストアップし、注文がある場合にはその詳細も表示します:


SELECT 
    Customers.CustomerID, Customers.CustomerName, Orders.OrderID
FROM 
    Customers
LEFT JOIN 
    Orders
ON 
    Customers.CustomerID = Orders.CustomerID;

このクエリにより、顧客情報が全て取得され、各顧客の注文が存在する場合にはその詳細も同時に取得されます。顧客に注文がない場合でも、顧客情報が結果に表示され、注文情報の部分はNULLとなります。

OUTER APPLYの基本と使い方

OUTER APPLYは、テーブルの行ごとに別のテーブルやテーブル値関数の行を評価するために使用されます。これは、特定の行ごとにサブクエリを実行するような動作をします。

OUTER APPLYの基本構文

基本的なOUTER APPLYの構文は以下の通りです:


SELECT 
    A.column1, A.column2, B.column1, B.column2
FROM 
    TableA A
OUTER APPLY 
    (SELECT column1, column2 FROM TableB B WHERE A.key = B.key) AS B;

OUTER APPLYの使用例

以下は、顧客テーブルと注文テーブルをOUTER APPLYで結合する例です。各顧客ごとに最新の注文を表示します:


SELECT 
    Customers.CustomerID, Customers.CustomerName, Orders.OrderID, Orders.OrderDate
FROM 
    Customers
OUTER APPLY 
    (SELECT TOP 1 OrderID, OrderDate 
     FROM Orders 
     WHERE Customers.CustomerID = Orders.CustomerID 
     ORDER BY OrderDate DESC, OrderID DESC) AS Orders;

このクエリでは、各顧客ごとに最新の注文が取得されます。顧客に注文がない場合でも、顧客情報が結果に表示され、注文情報の部分はNULLとなります。OUTER APPLYは特にテーブル値関数やサブクエリを利用して動的にデータを取得する場合に有効です。

LEFT JOINとOUTER APPLYの違い

LEFT JOINとOUTER APPLYは、いずれもテーブルを結合するために使用されますが、動作と適用シナリオにおいていくつかの重要な違いがあります。

基本的な違い

LEFT JOINは、二つのテーブルを結合し、左側のテーブルのすべての行と右側のテーブルの一致する行を返します。OUTER APPLYは、左側のテーブルの各行に対してサブクエリを実行し、その結果を結合します。

LEFT JOINの動作

LEFT JOINは、単純な結合条件に基づいてテーブルを結合します。例として、顧客とそのすべての注文を表示する場合に使用されます。


SELECT 
    Customers.CustomerID, Customers.CustomerName, Orders.OrderID
FROM 
    Customers
LEFT JOIN 
    Orders
ON 
    Customers.CustomerID = Orders.CustomerID;

OUTER APPLYの動作

OUTER APPLYは、各行に対してサブクエリを実行し、その結果を結合します。特定の条件を満たす行を動的に取得する場合に使用されます。


SELECT 
    Customers.CustomerID, Customers.CustomerName, Orders.OrderID, Orders.OrderDate
FROM 
    Customers
OUTER APPLY 
    (SELECT TOP 1 OrderID, OrderDate 
     FROM Orders 
     WHERE Customers.CustomerID = Orders.CustomerID 
     ORDER BY OrderDate DESC, OrderID DESC) AS Orders;

使用シナリオの違い

LEFT JOINは、二つのテーブル間の単純な結合に適しており、特定の条件に基づいて複数の関連行を取得する場合に最適です。OUTER APPLYは、各行に対して複雑なサブクエリを実行し、動的にデータを取得する場合に有効です。

パフォーマンスの違い

APPLYの行ごとの説明は論理的な意味です。物理的な実行方法や性能はオプティマイザーの計画、索引、統計、件数に依存します。LEFT JOINなら大規模データで必ず速い、OUTER APPLYなら必ず遅いとは判断できません。

効率的なクエリの書き方

LEFT JOINとOUTER APPLYを効率的に使用するためには、それぞれの特性を理解し、適切なシナリオで選択することが重要です。以下に、それぞれの方法を効率的に使用するためのベストプラクティスを紹介します。

LEFT JOINのベストプラクティス

インデックスの使用

LEFT JOINを使用する際には、結合に使用されるカラムにインデックスを設定することが重要です。これにより、結合操作のパフォーマンスが向上します。


CREATE INDEX idx_customer_id ON Orders(CustomerID);

必要なカラムのみを選択

クエリのパフォーマンスを向上させるためには、SELECT文で必要なカラムのみを指定します。これにより、無駄なデータの転送を防ぎます。


SELECT 
    Customers.CustomerID, Customers.CustomerName, Orders.OrderID
FROM 
    Customers
LEFT JOIN 
    Orders
ON 
    Customers.CustomerID = Orders.CustomerID;

OUTER APPLYのベストプラクティス

サブクエリの最適化

OUTER APPLYを使用する際には、サブクエリを最適化し、必要最低限のデータを取得するようにします。最新1件など必要な結果に合う場合だけTOP句を使います。単に高速化するためにTOPを付けると、必要な行を失います。


SELECT 
    Customers.CustomerID, Customers.CustomerName, Orders.OrderID, Orders.OrderDate
FROM 
    Customers
OUTER APPLY 
    (SELECT TOP 1 OrderID, OrderDate 
     FROM Orders 
     WHERE Customers.CustomerID = Orders.CustomerID 
     ORDER BY OrderDate DESC, OrderID DESC) AS Orders;

インデックスと統計情報の管理

OUTER APPLYを使用する際には、関連するテーブルのカラムにインデックスを作成し、最新の統計情報を保持することが重要です。これにより、クエリプランナーが最適な実行プランを選択できるようになります。


CREATE INDEX idx_customer_id_order_date ON Orders(CustomerID, OrderDate);
UPDATE STATISTICS Orders;

同じ結果でパフォーマンスを比較する

全注文を返すLEFT JOINと、最新1件だけ返すOUTER APPLYでは結果行数も処理内容も違います。そのまま時間を測って優劣を決められません。以下は「注文がない顧客も残し、注文があれば最新1件」という同じ目的の2案です。OrderIDは一意なキー、同じOrderDateなら大きいOrderIDを選ぶルールとします。実環境での実測結果ではなく比較用のコード例です。

案1:OUTER APPLYとTOP (1)

SELECT c.CustomerID, c.CustomerName, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
OUTER APPLY (
    SELECT TOP (1) x.OrderID, x.OrderDate
    FROM dbo.Orders AS x
    WHERE x.CustomerID = c.CustomerID
    ORDER BY x.OrderDate DESC, x.OrderID DESC
) AS o;

案2:ROW_NUMBERとLEFT JOIN

;WITH ranked AS (
    SELECT CustomerID, OrderID, OrderDate,
           ROW_NUMBER() OVER (
               PARTITION BY CustomerID
               ORDER BY OrderDate DESC, OrderID DESC
           ) AS rn
    FROM dbo.Orders
)
SELECT c.CustomerID, c.CustomerName, r.OrderID, r.OrderDate
FROM dbo.Customers AS c
LEFT JOIN ranked AS r
  ON r.CustomerID = c.CustomerID
 AND r.rn = 1;

rn = 1をWHEREに置くと、注文がなく右側がNULLの顧客が除外されます。この例ではONに置きます。OrderDateだけで同順位が生じる場合の選択を安定させるため、一意キーOrderIDも並び順に加えています。同日時の注文をすべて返したいなら、それは別の要件です。

まず両案の行数と各列の値が一致することを、注文なし・複数注文・同日時の注文を含む検証データで確認します。最終結果の表示順が必要なら外側にもORDER BYを追加します。

測る項目と索引の考え方

SET STATISTICS IO, TIME ON;
-- 上の比較対象クエリを同じ条件で実行
SET STATISTICS IO, TIME OFF;

検証環境で実際の実行プラン、論理読み取り数、CPU時間、経過時間、実際と推定の行数を確認します。対象顧客数や1顧客あたりの注文数、キャッシュや同時実行の違いをそろえて比較し、一回の時間だけで判断しません。本番でキャッシュを強制消去する操作は不要です。

最新注文を探す処理では、OrdersのCustomerID・OrderDate・OrderIDの並びを考慮した索引が候補になります。ただし既存索引、更新負荷、容量、他クエリへの影響を確認してから検証します。索引の作成や統計更新は読み取り専用の操作ではなく、むやみに本番へ追加しません。

公式資料:FROM・JOIN・APPLY、ROW_NUMBERの順序条件、実際の実行プランの表示。

まとめ

LEFT JOINとOUTER APPLYの選択は、必要な結果と相関参照の有無から考えます。全件と最新1件を混ぜて性能比較せず、同順位の扱い、NULL行の保持、重複を確認してから、同じ結果のクエリを実行プランとI/Oで評価してください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次