SQL LEFT OUTER JOIN とは?図解でわかりやすく解説


SQL LEFT OUTER JOINとは?図解で分かりやすく解説

はじめに:なぜJOINが必要なのか?

リレーショナルデータベースにおいて、データはしばしば複数のテーブルに分けて格納されます。これは、データの重複を避け、整合性を保ち、管理を容易にするためです。例えば、顧客情報と注文情報を考えてみましょう。顧客情報は「顧客テーブル」に、注文情報は「注文テーブル」にそれぞれ格納するのが一般的です。

しかし、私たちがデータを利用する際、これら複数のテーブルにまたがる情報をまとめて見たいことがよくあります。「この顧客はどのような注文をしたか?」や「この注文はどの顧客が行ったか?」を知るには、顧客テーブルと注文テーブルを関連付けてデータを取得する必要があります。

このように、複数のテーブルから関連するデータを結びつけて取得するためのSQLの機能がJOINです。

JOINにはいくつかの種類があります。最も基本的なものはINNER JOINですが、これに加えてOUTER JOINという種類も存在します。OUTER JOINにはさらにLEFT OUTER JOINRIGHT OUTER JOINFULL OUTER JOINがあります。

この記事では、特に実務で頻繁に使用されるLEFT OUTER JOIN(略してLEFT JOIN)に焦点を当て、その仕組み、使い方、そして他のJOINとの違いを図解を交えながら詳しく解説します。

この記事を読めば、あなたは以下のことを理解できるようになります。

  • LEFT OUTER JOINの基本的な考え方
  • LEFT OUTER JOINの構文と書き方
  • LEFT OUTER JOINがどのような結果を返すのかを図で理解する
  • 具体的なビジネスシーンでのLEFT OUTER JOINの使い方
  • LEFT OUTER JOINを使う上での注意点や応用方法
  • INNER JOINなど他のJOINとの違いと使い分け

それでは、SQLの世界で非常に強力なツールであるLEFT OUTER JOINの扉を開けていきましょう。

1. 基本的なJOINの種類のおさらい

LEFT OUTER JOINを理解する前に、まずは基本的なJOINの種類について簡単におさらいしておきましょう。これにより、LEFT OUTER JOINが他のJOINとどう異なるのかが明確になります。

データベースには、例えば以下のような2つのテーブルがあるとします。

  • テーブルA:左側のテーブル
  • テーブルB:右側のテーブル

これらのテーブルを特定の関連するカラム(結合キー)を使って結合することを考えます。

1.1. INNER JOIN

INNER JOINは、最も一般的に使用されるJOINの種類です。これは、結合条件に一致する両方のテーブルの行のみを取得します。どちらかのテーブルに一致する行がない場合は、その行は結果に含まれません。

(図:INNER JOINの概念図 – テーブルAとテーブルBの共通部分だけが結果になるイメージ)

例えば、顧客テーブルと注文テーブルを顧客IDでINNER JOINした場合、注文履歴のある顧客の情報とその注文情報だけが取得されます。注文履歴が全くない顧客の情報は結果に含まれません。

1.2. OUTER JOIN

OUTER JOINは、INNER JOINとは異なり、どちらか一方、あるいは両方のテーブルに一致する行がない場合でも、結果に含めることができるJOINです。一致しない行については、対応するテーブルのカラムにはNULLという特殊な値が格納されます。NULLは「データが存在しない」「不明」といった状態を表します。

OUTER JOINには以下の3つの種類があります。

  • LEFT OUTER JOIN (LEFT JOIN):左側のテーブルの全ての行と、右側のテーブルで一致する行を取得します。右側のテーブルに一致する行がない場合でも、左側の行は結果に含まれ、右側のカラムはNULLになります。
  • RIGHT OUTER JOIN (RIGHT JOIN):右側のテーブルの全ての行と、左側のテーブルで一致する行を取得します。左側のテーブルに一致する行がない場合でも、右側の行は結果に含まれ、左側のカラムはNULLになります。LEFT JOINの左右を反転させたものです。
  • FULL OUTER JOIN (FULL JOIN):左側のテーブルの全ての行と、右側のテーブルの全ての行を取得します。どちらかのテーブルに一致する行がない場合、対応するもう一方のテーブルのカラムはNULLになります。これはLEFT JOINとRIGHT JOINの結果を合わせたようなものです。

この記事の主役はLEFT OUTER JOINですので、ここからはLEFT OUTER JOINに焦点を絞って詳しく見ていきましょう。

2. LEFT OUTER JOINとは? – 定義と目的

あらためて、LEFT OUTER JOINの定義と目的を明確にしましょう。

2.1. 定義

LEFT OUTER JOINは、FROM句で最初に指定されたテーブル(またはLEFT JOINキーワードの左側に記述されたテーブル)を「左側のテーブル」、LEFT JOINキーワードの右側に記述されたテーブルを「右側のテーブル」として扱います。

そして、LEFT OUTER JOINは以下の結果セットを生成します。

  1. 左側のテーブルの全ての行
  2. 左側のテーブルの行と結合条件で一致する右側のテーブルの行
  3. 左側のテーブルの行に対応する右側のテーブルの行が存在しない場合、左側の行は結果に含まれますが、右側のテーブルから取得されるべきカラムの値はすべてNULLになります。

SQLでは、OUTERキーワードは省略可能なので、通常はLEFT JOINと記述されます。

2.2. 目的

LEFT JOINの主な目的は、左側のテーブルを基準(基軸)として、それに紐づく右側のテーブルの情報を付加したい場合に使用することです。右側のテーブルに関連情報が存在しない場合でも、左側のテーブルのデータは失いたくない、という状況で非常に役立ちます。

例えば、「全ての顧客の一覧を表示し、もし注文履歴があればその注文情報も一緒に表示したい」といったケースです。INNER JOINでは注文履歴のない顧客は結果から除外されてしまいますが、LEFT JOINを使えば注文履歴のない顧客もリストに含めることができます。その場合、注文に関するカラムはNULLとして表示されます。

このように、左側のテーブルにあるデータを網羅的に表示しつつ、関連する右側のデータがあればそれを補完するという使い方がLEFT JOINの典型的なシナリオです。

3. LEFT OUTER JOINの構文

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

sql
SELECT カラムリスト
FROM 左側のテーブル
LEFT OUTER JOIN 右側のテーブル
ON 結合条件;

または、OUTERを省略して一般的に使われる形:

sql
SELECT カラムリスト
FROM 左側のテーブル
LEFT JOIN 右側のテーブル
ON 結合条件;

  • SELECT カラムリスト: 取得したいカラムを指定します。どのテーブルのどのカラムかを明確にするために、テーブル名.カラム名エイリアス名.カラム名のように記述することが推奨されます。
  • FROM 左側のテーブル: LEFT JOINの基準となる左側のテーブルを指定します。このテーブルの全ての行が結果に含まれます。
  • LEFT [OUTER] JOIN 右側のテーブル: 結合する右側のテーブルを指定します。
  • ON 結合条件: どのように2つのテーブルを結合するかを指定します。通常は、両方のテーブルに存在する関連性の高いカラム(例えば、主キーと外部キーの関係にあるカラム)を指定します。テーブルA.カラムX = テーブルB.カラムY のように記述します。

3.1. USING句を使った結合

もし結合条件に使用するカラム名が両方のテーブルで同じ名前である場合、ON句の代わりにUSING句を使うことができます。

sql
SELECT カラムリスト
FROM 左側のテーブル
LEFT JOIN 右側のテーブル
USING (共通のカラム名);

例えば、CustomersテーブルとOrdersテーブルの両方にcustomer_idというカラムがあり、これを結合キーとする場合、以下のように書けます。

sql
SELECT *
FROM Customers
LEFT JOIN Orders
USING (customer_id);

これは以下のON句を使った書き方と同じ結果になります。

sql
SELECT *
FROM Customers
LEFT JOIN Orders
ON Customers.customer_id = Orders.customer_id;

USING句はコードを簡潔に記述できる場合がありますが、複数のカラムで結合する場合や、カラム名が異なる場合はON句を使う必要があります。また、可読性の観点からON句を好む開発者も多いです。どちらを使うかは、状況やコーディング規約によります。

4. LEFT OUTER JOINの図解と実行結果のイメージ

概念だけでは理解しづらい部分もあるため、具体的なテーブルデータを使ってLEFT JOINの結果を視覚的に理解してみましょう。

以下の2つのシンプルなテーブルを考えます。

テーブルA (Users)

user_id user_name
1 Alice
2 Bob
3 Charlie
4 David

テーブルB (Posts)

post_id user_id post_title
101 1 My first post
102 3 Great article
103 1 Another one
104 5 Hidden post

Usersテーブルはユーザーの情報、Postsテーブルはユーザーが投稿した記事の情報です。user_idが両方のテーブルを関連付けるキーです。

ここで、「全てのユーザーとその投稿一覧を取得したい」という要求があったとします。投稿したことがないユーザーも一覧に含めたい、ということです。これはまさにLEFT JOINを使うべきケースです。

Usersテーブルを左側のテーブル、Postsテーブルを右側のテーブルとして、user_idでLEFT JOINを実行してみましょう。

クエリ:

sql
SELECT
U.user_id,
U.user_name,
P.post_id,
P.post_title
FROM
Users AS U -- テーブルA (左)
LEFT JOIN
Posts AS P -- テーブルB (右)
ON
U.user_id = P.user_id;

(注: AS U, AS P はテーブルエイリアスで、クエリを短く分かりやすくするために使われます)

このクエリがどのように結果を生成するかを見ていきます。

まず、左側のテーブル(Users)の全ての行を取得します。

  • Alice (user_id 1)
  • Bob (user_id 2)
  • Charlie (user_id 3)
  • David (user_id 4)

次に、これらのユーザーそれぞれについて、右側のテーブル(Posts)にuser_idが一致する行があるかを探します。

  • user_id 1 (Alice): Postsテーブルにはuser_idが1の行が2つあります (post_id 101, 103)。これらの行とAliceの行が結合されます。
  • user_id 2 (Bob): Postsテーブルにはuser_idが2の行がありません
  • user_id 3 (Charlie): Postsテーブルにはuser_idが3の行が1つあります (post_id 102)。この行とCharlieの行が結合されます。
  • user_id 4 (David): Postsテーブルにはuser_idが4の行がありません
  • (補足) user_id 5: Postsテーブルにはuser_idが5の行 (post_id 104) がありますが、左側のテーブル(Users)にuser_idが5のユーザーはいないため、この投稿はLEFT JOINの結果には含まれません。(もしFULL JOINなら含まれます)

ここでLEFT JOINの重要な性質です。左側のテーブルの行に対応する右側のテーブルの行がない場合(BobとDavidのケース)、左側のテーブルの行はそのまま結果に含まれますが、右側のテーブルから取得されるはずのカラム(post_id, post_title)にはNULLが設定されます。

一致する行が複数ある場合(Aliceのケース)、左側の行はその数だけ複製され、それぞれの右側の行と結合されます。

結果セット:

user_id user_name post_id post_title
1 Alice 101 My first post
1 Alice 103 Another one
2 Bob NULL NULL
3 Charlie 102 Great article
4 David NULL NULL

図解:

(図:LEFT JOINの概念図 – 左側のテーブル(Users)の円全体と、右側のテーブル(Posts)との共通部分が含まれるイメージ。共通部分にない左側の部分(Bob, David)は、右側のデータがNULLで補われる形で表現される)

図で表現すると、左側の円全体が選択され、そこに右側の円との重なり部分(一致するデータ)がマージされるイメージです。重なり部分にない左側のデータ(BobとDavid)は、右側の情報が空っぽ(NULL)の状態で残ります。

一方、もしこれをINNER JOINで行っていたら、結果は以下のようになります。

INNER JOINの結果:

user_id user_name post_id post_title
1 Alice 101 My first post
1 Alice 103 Another one
3 Charlie 102 Great article

INNER JOINでは、user_idが両方のテーブルに存在する行(AliceとCharlieの投稿履歴がある行)のみが取得され、BobとDavidは結果に含まれません。

この違いが、LEFT JOINとINNER JOINの最も重要な違いであり、LEFT JOINが「左側のテーブルを基軸とする」という意味です。

5. 具体的な使用例とコード

ここでは、より現実的なシナリオに基づいてLEFT JOINの使い方を見ていきます。

例1:顧客と注文履歴

全ての顧客の一覧と、それぞれの顧客がこれまでに行った注文の情報を表示したい。まだ一度も注文したことのない顧客もリストに含めること。」

テーブル構造:

  • customers テーブル: customer_id (PK), name, email
  • orders テーブル: order_id (PK), customer_id (FK), order_date, amount

データ例:

customers

customer_id name email
1 佐藤 [email protected]
2 田中 [email protected]
3 山田 [email protected]
4 鈴木 [email protected]

orders

order_id customer_id order_date amount
101 1 2023-01-15 3000
102 3 2023-01-20 5000
103 1 2023-02-10 2500
104 5 2023-02-12 8000

クエリ:

sql
SELECT
c.customer_id,
c.name,
c.email,
o.order_id,
o.order_date,
o.amount
FROM
customers AS c -- 左側のテーブル (customers)
LEFT JOIN
orders AS o -- 右側のテーブル (orders)
ON
c.customer_id = o.customer_id; -- 結合条件: customer_id

実行結果:

customer_id name email order_id order_date amount
1 佐藤 [email protected] 101 2023-01-15 3000
1 佐藤 [email protected] 103 2023-02-10 2500
2 田中 [email protected] NULL NULL NULL
3 山田 [email protected] 102 2023-01-20 5000
4 鈴木 [email protected] NULL NULL NULL

解説:

  • customersテーブルが左側のテーブルとして指定されているため、全ての顧客(佐藤, 田中, 山田, 鈴木)が結果に含まれています。
  • 佐藤さん (customer_id 1) は注文が2件あるため、佐藤さんの行が複製されてそれぞれの注文情報と結合されています。
  • 山田さん (customer_id 3) は注文が1件あるため、その注文情報と結合されています。
  • 田中さん (customer_id 2) と鈴木さん (customer_id 4) はordersテーブルに一致するcustomer_idの行がありません。しかし、LEFT JOINなのでcustomersテーブルの行は保持され、ordersテーブル由来のカラム(order_id, order_date, amount)にはNULLが格納されています。
  • ordersテーブルに存在するcustomer_id 5の注文(order_id 104)は、左側のcustomersテーブルに該当する顧客がいないため、結果に含まれていません。

この結果から、「田中さんと鈴木さんはまだ注文したことがない」という情報が明確にわかります。これがINNER JOINでは得られないLEFT JOINのメリットです。

例2:部署と従業員(従業員がいない部署も含む)

全ての部署の一覧と、そこに所属する従業員の情報を表示したい。従業員が一人もいない部署もリストに含めること。」

テーブル構造:

  • departments テーブル: dept_id (PK), dept_name
  • employees テーブル: emp_id (PK), emp_name, dept_id (FK)

データ例:

departments

dept_id dept_name
10 営業部
20 開発部
30 経理部
40 広報部

employees

emp_id emp_name dept_id
1 伊藤 10
2 加藤 20
3 佐々木 10
4 吉田 20
5 中村 50

クエリ:

sql
SELECT
d.dept_id,
d.dept_name,
e.emp_id,
e.emp_name
FROM
departments AS d -- 左側のテーブル (departments)
LEFT JOIN
employees AS e -- 右側のテーブル (employees)
ON
d.dept_id = e.dept_id; -- 結合条件: dept_id

実行結果:

dept_id dept_name emp_id emp_name
10 営業部 1 伊藤
10 営業部 3 佐々木
20 開発部 2 加藤
20 開発部 4 吉田
30 経理部 NULL NULL
40 広報部 NULL NULL

解説:

  • departmentsテーブルが左側のテーブルなので、全ての部署(営業部, 開発部, 経理部, 広報部)が結果に含まれています。
  • 営業部 (dept_id 10) と開発部 (dept_id 20) には複数の従業員がいるため、部署の行が複製されて各従業員情報と結合されています。
  • 経理部 (dept_id 30) と広報部 (dept_id 40) にはemployeesテーブルに一致するdept_idの行がありません。LEFT JOINにより、これらの部署の行は保持され、従業員関連のカラムはNULLになっています。
  • 従業員の中村さん (emp_id 5) は存在しない部署 (dept_id 50) に所属していますが、左側のdepartmentsテーブルに該当する部署がないため、この従業員の行は結果に含まれていません。

この結果から、経理部と広報部には現在従業員がいないことが分かります。組織図や部署の状況を把握する際に有用な情報です。

例3:商品とレビュー(レビューがない商品も含む)

全ての商品の一覧と、その商品に付けられたレビューの情報を表示したい。まだレビューが一つも付けられていない商品もリストに含めること。」

テーブル構造:

  • products テーブル: product_id (PK), product_name, price
  • reviews テーブル: review_id (PK), product_id (FK), rating, comment, review_date

データ例:

products

product_id product_name price
P001 ノートPC 120000
P002 スマートフォン 80000
P003 タブレット 50000
P004 キーボード 10000

reviews

review_id product_id rating comment review_date
R1 P001 5 最高です! 2023-03-01
R2 P003 4 使いやすい 2023-03-05
R3 P001 4 デザインが良い 2023-03-10
R4 P005 3 よく分からない 2023-03-12

クエリ:

sql
SELECT
p.product_id,
p.product_name,
p.price,
r.review_id,
r.rating,
r.comment
FROM
products AS p -- 左側のテーブル (products)
LEFT JOIN
reviews AS r -- 右側のテーブル (reviews)
ON
p.product_id = r.product_id; -- 結合条件: product_id

実行結果:

product_id product_name price review_id rating comment
P001 ノートPC 120000 R1 5 最高です!
P001 ノートPC 120000 R3 4 デザインが良い
P002 スマートフォン 80000 NULL NULL NULL
P003 タブレット 50000 R2 4 使いやすい
P004 キーボード 10000 NULL NULL NULL

解説:

  • productsテーブルが左側のテーブルなので、全ての製品(ノートPC, スマートフォン, タブレット, キーボード)が結果に含まれています。
  • ノートPC (P001) には2件のレビューがあるため、製品の行が複製されて各レビュー情報と結合されています。
  • タブレット (P003) には1件のレビューがあるため、そのレビュー情報と結合されています。
  • スマートフォン (P002) とキーボード (P004) にはreviewsテーブルに一致するproduct_idの行がありません。LEFT JOINにより、これらの製品の行は保持され、レビュー関連のカラムはNULLになっています。
  • レビューテーブルに存在するP005のレビュー(R4)は、左側のproductsテーブルに該当する製品がないため、結果に含まれていません。

この結果から、スマートフォンとキーボードにはまだレビューが一つも付いていないことが分かります。これは、レビュー促進の施策を検討する上で有用な情報となり得ます。

これらの例からわかるように、LEFT JOINはマスターデータ(顧客、部署、商品など)を基軸として、それに関連するトランザクションデータや属性情報(注文、従業員、レビューなど)を、関連性の有無にかかわらずまとめて取得したい場合に非常に強力なツールとなります。

6. LEFT OUTER JOINの応用

LEFT JOINは基本的な結合だけでなく、他のSQL句と組み合わせることでさらに様々なデータ分析や抽出が可能になります。

6.1. WHERE句によるフィルタリング

結合後の結果セットに対して、さらに条件を加えて絞り込むことができます。ただし、WHERE句を右側のテーブルのカラムに対して使用する際には注意が必要です。

例:特定の期間の注文情報を持つ顧客一覧 (注文がない顧客も含む)

「全ての顧客をリストアップし、もし2023年1月に行われた注文があればその情報を表示したい。2023年1月以外の注文は表示しないが、注文が全くない顧客はリストに残す。」

クエリ:

sql
SELECT
c.customer_id,
c.name,
o.order_id,
o.order_date,
o.amount
FROM
customers AS c
LEFT JOIN
orders AS o
ON
c.customer_id = o.customer_id
WHERE
o.order_date >= '2023-01-01' AND o.order_date < '2023-02-01'
OR o.order_id IS NULL; -- ここがポイント!

解説:

このクエリのWHERE句は、以下の2つの条件のどちらかを満たす行を選択します。

  1. o.order_date >= '2023-01-01' AND o.order_date < '2023-02-01': 注文日が2023年1月中の行。これは、2023年1月中に注文した顧客のデータ(結合されたordersテーブルのデータが存在する行)を抽出します。
  2. o.order_id IS NULL: ordersテーブルのorder_idNULLの行。これは、LEFT JOINによってordersテーブル側に一致する行がなかったためNULLとなった行、つまり「注文履歴がない」または「2023年1月中の注文履歴がない」顧客の行を抽出します。

注意点:
もしWHERE o.order_date >= '2023-01-01' AND o.order_date < '2023-02-01' だけにすると、o.order_dateNULLの行(注文がなかった顧客)はWHERE句の条件を満たさないため結果から除外されてしまいます。これは実質的に「2023年1月中に注文した顧客のみ」を抽出することになり、LEFT JOINを使ったにも関わらず注文のない顧客が消えてしまいます。

つまり、LEFT JOINの結果に対して右側のテーブルのカラムでフィルタリングする場合、NULLになる行を残したいのか、それとも除外したいのかを明確にし、WHERE句の条件を適切に設定する必要があります。NULLの行を残したい場合は、上記のように OR カラム名 IS NULL という条件を追加する必要があります。

6.2. 注文履歴がない顧客のみを抽出する

LEFT JOINで生成されるNULLを利用して、「右側のテーブルに全く一致する行がなかった」行だけを抽出することができます。これは、「〜がない」「〜に該当しない」エンティティを特定したい場合に非常に有用です。

例:一度も注文したことがない顧客をリストアップする

「顧客テーブルには登録されているが、ordersテーブルには一度も登場しない(注文履歴がない)顧客を知りたい。」

クエリ:

sql
SELECT
c.customer_id,
c.name,
c.email
FROM
customers AS c
LEFT JOIN
orders AS o
ON
c.customer_id = o.customer_id
WHERE
o.order_id IS NULL; -- 注文がない行は o.order_id が NULL になることを利用

実行結果 (例1のデータを使用):

customer_id name email
2 田中 [email protected]
4 鈴木 [email protected]

解説:

まずcustomersordersをLEFT JOINします。結果には全ての顧客が含まれ、注文がない顧客のordersテーブル側のカラムはNULLになります。
次に、WHERE o.order_id IS NULL という条件で絞り込みます。これにより、LEFT JOINの結果のうち、ordersテーブル側に一致する行がなかった(つまり注文履歴がなかった)行だけが抽出されます。order_idordersテーブルの主キーでありNOT NULL制約を持つカラムであることが多いので、このようにIS NULLで判定するのが確実です。

この手法は、「会員登録したがログインしていないユーザー」「商品登録したが在庫がない商品」「部署はあるが従業員がいない部署」など、様々な「存在しない関連データ」を持つエンティティを特定するのに応用できます。

6.3. 複数のLEFT JOIN

3つ以上のテーブルをLEFT JOINで結合することも可能です。この場合、結合は左から順に行われます。

例:顧客、注文、商品情報を結合(注文がない顧客も含む)

「全ての顧客をリストアップし、もし注文があればその注文情報と、注文した商品の情報を表示したい。注文がない顧客も、注文はあるが商品情報がないケース(まれですが)もリストに含める。」

テーブル構造:

  • customers: customer_id, name
  • orders: order_id, customer_id, order_date
  • order_items: order_item_id, order_id, product_id, quantity (注文明細テーブル)
  • products: product_id, product_name, price

今回は、注文テーブルと商品テーブルの間に中間テーブルorder_itemsを挟むよくあるケースで考えます。

データ例:

customers
| customer_id | name |
| :———- | :—— |
| 1 | 佐藤 |
| 2 | 田中 |

orders
| order_id | customer_id | order_date |
| :——- | :———- | :——— |
| 101 | 1 | 2023-01-15 |
| 102 | 1 | 2023-02-10 |

order_items
| order_item_id | order_id | product_id | quantity |
| :———— | :——- | :——— | :——- |
| 1 | 101 | P001 | 1 |
| 2 | 101 | P002 | 2 |
| 3 | 102 | P001 | 1 |
| 4 | 103 | P999 | 1 | (order_id 103 は orders にない, product_id P999 は products にない)

products
| product_id | product_name |
| :——— | :———– |
| P001 | ノートPC |
| P002 | スマートフォン |

クエリ:

sql
SELECT
c.customer_id,
c.name AS customer_name,
o.order_id,
o.order_date,
oi.order_item_id,
oi.quantity,
p.product_id,
p.product_name
FROM
customers AS c -- 左1
LEFT JOIN
orders AS o ON c.customer_id = o.customer_id -- 左2: customers と orders を結合
LEFT JOIN
order_items AS oi ON o.order_id = oi.order_id -- 左3: 上の結果と order_items を結合
LEFT JOIN
products AS p ON oi.product_id = p.product_id; -- 左4: 上の結果と products を結合

実行結果:

customer_id customer_name order_id order_date order_item_id quantity product_id product_name
1 佐藤 101 2023-01-15 1 1 P001 ノートPC
1 佐藤 101 2023-01-15 2 2 P002 スマートフォン
1 佐藤 102 2023-02-10 3 1 P001 ノートPC
2 田中 NULL NULL NULL NULL NULL NULL

解説:

  • 最初のLEFT JOINcustomersordersを結合します。田中さん(customer_id 2)は注文がないため、この段階でorders関連のカラムはNULLになります。
  • 次に、その結果とorder_itemsorder_idLEFT JOINします。田中さんの行は既にorders側がNULLなので、order_items側もNULLになります。佐藤さんの注文には対応するorder_itemsがあるため、結合されます。
  • 最後に、その結果とproductsproduct_idLEFT JOINします。order_itemsに該当するproduct_idがproductsテーブルにある場合は結合、ない場合はNULLになります。今回は全てのproduct_idがproductsテーブルにあるため、すべて結合されています。もし、order_itemsテーブルに存在するproduct_idproductsテーブルにない場合、その行のproducts関連カラムはNULLになります。また、order_itemsに存在しないorder_idを持つ行(order_id 103の注文明細)は、前の結合でordersテーブルに該当がないため、結果に含まれません。

このように、LEFT JOINを連鎖させることで、最初のテーブル(左端のテーブル)を基軸として、関連する全ての情報を紐付け、どの段階でデータが存在しない場合でも最初のテーブルのデータが失われないように結果を構築することができます。結合の順序が重要であることに注意してください。左側に記述したテーブルから順に、結果セットが「広がっていく」イメージです。

6.4. LEFT JOIN と GROUP BY / 集計関数

LEFT JOINの結果に対して集計関数(COUNT, SUM, AVGなど)やGROUP BY句を使用することもよくあります。

例:顧客ごとの注文回数と合計注文金額(注文がない顧客も含む)

「全ての顧客に対し、これまでの注文回数と合計注文金額を集計したい。注文が全くない顧客は、回数0、金額0として表示したい。」

クエリ:

sql
SELECT
c.customer_id,
c.name,
COUNT(o.order_id) AS order_count, -- 注文IDの数を数える
SUM(o.amount) AS total_amount -- 注文金額の合計を計算
FROM
customers AS c -- 左側のテーブル
LEFT JOIN
orders AS o -- 右側のテーブル
ON
c.customer_id = o.customer_id
GROUP BY
c.customer_id,
c.name -- 集計キー
ORDER BY
c.customer_id;

実行結果 (例1のデータを使用):

customer_id name order_count total_amount
1 佐藤 2 5500
2 田中 0 NULL
3 山田 1 5000
4 鈴木 0 NULL

解説:

  • まずcustomersordersをLEFT JOINします。注文がない顧客(田中、鈴木)のorders側のカラムはNULLになります。
  • 次に、GROUP BY c.customer_id, c.name によって、顧客ごとにグループ化します。
  • COUNT(o.order_id)は、各グループ内でorder_idNULLではない行の数を数えます。注文がある顧客のグループでは実際の注文数、注文がない顧客のグループではorder_idが全てNULLなので結果は0になります。
  • SUM(o.amount)は、各グループ内でamountカラムの合計を計算します。注文がある顧客のグループでは合計金額、注文がない顧客のグループではamountが全てNULLなので結果はNULLになります。

NULLを0として表示したい場合:

集計関数がNULLを含む行に対して計算を行った結果、NULLになる場合があります(SUMなど)。これを0として表示したい場合は、COALESCE関数(またはMySQLのIFNULL関数)を使用します。

sql
SELECT
c.customer_id,
c.name,
COUNT(o.order_id) AS order_count,
COALESCE(SUM(o.amount), 0) AS total_amount -- SUMの結果がNULLなら0に変換
FROM
customers AS c
LEFT JOIN
orders AS o
ON
c.customer_id = o.customer_id
GROUP BY
c.customer_id,
c.name
ORDER BY
c.customer_id;

COALESCE関数:
COALESCE(value1, value2, ...) は、引数のリストの中で最初に見つかった非NULLの値を返します。COALESCE(SUM(o.amount), 0) は、SUM(o.amount)の結果がNULLでなければその値を、NULLであれば0を返します。

このクエリの実行結果は、田中さんと鈴木さんのtotal_amount0になります。

customer_id name order_count total_amount
1 佐藤 2 5500
2 田中 0 0
3 山田 1 5000
4 鈴木 0 0

このように、LEFT JOINと集計関数、GROUP BYを組み合わせることで、「全て」のエンティティに対する集計結果を、関連データがない場合も考慮して得ることができます。

7. LEFT OUTER JOINを使う際の注意点

LEFT JOINは非常に便利ですが、使用する上でいくつか注意すべき点があります。

7.1. NULLの扱い

LEFT JOINの結果にはNULLが含まれる可能性があります。NULLはゼロや空文字とは異なる特殊な値です。NULLを含むカラムに対して算術演算を行うと結果がNULLになったり、=などの比較演算子でNULLを比較すると意図した結果が得られないことがあります(NULL = NULL は真ではなくUNKNOWNとなります)。

  • NULLが含まれる可能性のあるカラムを集計する際は、COALESCEIFNULLでNULLを適切な値(0など)に置換することを検討する。
  • NULLが含まれる可能性のあるカラムを条件とする場合は、IS NULLIS NOT NULLを使用する。

7.2. WHERE句とON句の違い(LEFT JOINの場合)

INNER JOINの場合はON句とWHERE句のどちらで結合条件やフィルタ条件を記述しても、結果セット自体は同じになることが多いです(パフォーマンスは異なる場合があります)。しかし、LEFT JOINの場合はON句とWHERE句に記述する条件によって結果が大きく変わることがあります。

  • ON句の条件: LEFT JOINを実行するに適用されます。ON句の条件を満たさない右側のテーブルの行は結合対象になりませんが、左側のテーブルの行は保持され、右側のカラムはNULLになります。
  • WHERE句の条件: LEFT JOINによる結合が完了し、結果セットが生成されたに適用されます。WHERE句の条件を満たさない行全体が結果セットから除外されます。

例:ON句とWHERE句での右側テーブルへの条件指定の違い

「全ての顧客のうち、2023年1月中の注文を持つ顧客とその注文情報を表示したい。ただし、2023年1月以外の注文は表示せず、注文が全くない顧客もリストに含める。」

  • ON句で条件を指定:

    sql
    SELECT
    c.customer_id,
    c.name,
    o.order_id,
    o.order_date,
    o.amount
    FROM
    customers AS c
    LEFT JOIN
    orders AS o
    ON
    c.customer_id = o.customer_id
    AND o.order_date >= '2023-01-01' -- ON句に右側テーブルの条件を追加
    AND o.order_date < '2023-02-01';

    結果イメージ: 顧客全員がリストアップされ、2023年1月の注文のみが表示されます。1月以外の注文は結合されず、その顧客のその注文情報行は結果に含まれません。注文が全くない顧客や1月以外の注文しかない顧客は、orders関連カラムがNULLになります。

    • 佐藤さん (customer_id 1): 1月と2月に注文がある。1月の注文(101)はON句の条件を満たすので結合される。2月の注文(103)はON句の条件を満たさないため結合されない。結果には1月の注文のみが結合された行と、注文がないものとしてorders側がNULLになる行(ただし1月の注文があるのでNULL行は出ない)・・・ではなく、LEFT JOINの性質上、ON句で右側の条件を満たさない行があっても、左側の行自体は保持されるので、結果は 佐藤(101), 佐藤(NULL) のような形になるか、多くのRDBMSではON句で右側の条件を満たさない行とは結合せず、そのLEFT JOINの対象行はNULLになる形で扱われる。
    • 田中さん (customer_id 2): 注文なし。ON句の条件も満たさない。orders側がNULLになる。
    • 山田さん (customer_id 3): 1月以外の注文(102)がある。ON句の条件を満たさないため結合されない。orders側がNULLになる。
    • 鈴木さん (customer_id 4): 注文なし。ON句の条件も満たさない。orders側がNULLになる。

    正確な結果イメージ:
    | customer_id | name | order_id | order_date | amount |
    | :———- | :—— | :—— | :——— | :—– |
    | 1 | 佐藤 | 101 | 2023-01-15 | 3000 |
    | 2 | 田中 | NULL | NULL | NULL |
    | 3 | 山田 | NULL | NULL | NULL |
    | 4 | 鈴木 | NULL | NULL | NULL |
    この結果は、「顧客全員をリストアップし、2023年1月中の注文があればそれを表示する」という意図に合致します。

  • WHERE句で条件を指定: (前述の「特定の条件でNULLを抽出」の例を再掲し、違いを説明)

    sql
    SELECT
    c.customer_id,
    c.name,
    o.order_id,
    o.order_date,
    o.amount
    FROM
    customers AS c
    LEFT JOIN
    orders AS o
    ON
    c.customer_id = o.customer_id -- 結合条件のみ
    WHERE
    o.order_date >= '2023-01-01' AND o.order_date < '2023-02-01'
    OR o.order_id IS NULL; -- フィルタ条件

    結果イメージ: このクエリは「2023年1月中に注文した顧客」と「全く注文がない顧客」をリストアップします。山田さん(customer_id 3)のように2023年1月以外の注文しか持たない顧客は、LEFT JOINの結果ではorders側がNULLになりませんが、WHERE句の条件 (o.order_date >= ... OR o.order_id IS NULL) のどちらも満たさないため結果から除外されます

    正確な結果イメージ (例1データ、ただし山田の注文は1月以外の注文):
    | customer_id | name | order_id | order_date | amount |
    | :———- | :—— | :—— | :——— | :—– |
    | 1 | 佐藤 | 101 | 2023-01-15 | 3000 |
    | 2 | 田中 | NULL | NULL | NULL |
    | 4 | 鈴木 | NULL | NULL | NULL |
    この結果は、「2023年1月中に注文したことがあるか、または全く注文がない顧客のみをリストアップする」という意図に合致します。

このように、LEFT JOINにおいて右側テーブルのカラムに対する条件をON句に書くかWHERE句に書くかで、NULLになる行が残るか残らないかが変わる場合があるため、注意が必要です。一般的に、「結合自体を絞り込む」ならON句、「結合後の結果を絞り込む」ならWHERE句と考えると分かりやすいですが、特にLEFT JOINにおいては、右側テーブルの条件をON句に入れることで、左側テーブルの全ての行を残しつつ、右側をより厳密に紐付ける(一致しないものはNULLにする)という効果が得られます。

7.3. パフォーマンス

大きなテーブルに対してJOINを実行する場合、パフォーマンスが問題となることがあります。特にLEFT JOINは左側のテーブルの全ての行を処理する必要があるため、その影響が大きくなる可能性があります。

  • インデックス: 結合条件に使用するカラム(ON句やUSING句で指定するカラム)には、必ずインデックスを作成しておきましょう。これにより、データベースは効率的に対応する行を見つけることができます。特に、左側テーブルの結合キーや、右側テーブルの結合キーにはインデックスが必須と言えます。
  • 不要なJOINを避ける: 必要なテーブルだけをJOINする。
  • 取得カラムの最小化: SELECT *ではなく、必要なカラムだけを選択する。
  • フィルタリングのタイミング: 可能な限り、JOINの前に(またはON句で)不要な行を絞り込むことで、JOIN対象の行数を減らす。ただし、LEFT JOINの場合は前述の通りWHERE句でのフィルタリングが結果に影響を与えるため、意図に応じて使い分ける必要があります。

7.4. テーブルエイリアスの使用

複数のテーブルをJOINする場合、どのカラムがどのテーブルに属するかを明確にするために、テーブルエイリアス(別名)を使用することを強く推奨します。

sql
FROM customers AS c
LEFT JOIN orders AS o ON c.customer_id = o.customer_id
SELECT c.name, o.order_date -- どのテーブルのカラムか明確

これにより、クエリの可読性が向上し、あいまいさ(Ambiguous column name)によるエラーを防ぐことができます。

8. 他のJOINとの比較と使い分け

LEFT JOINを他のJOINと区別し、適切に使い分けるための比較を見ていきましょう。

8.1. INNER JOIN vs LEFT OUTER JOIN

特徴 INNER JOIN LEFT OUTER JOIN
結果に含まれる行 両方のテーブルで結合条件に一致する行のみ 左側のテーブルの全ての行 + 右側で一致する行
一致しない行 結果に含まれない 左側の行は含まれ、右側のカラムはNULLになる
主な目的 両方のテーブルに存在する関連データのみ取得 左側のデータを基軸に、関連データを付加
使用例 注文履歴のある顧客とその注文情報を表示 全ての顧客と、あれば注文情報を表示
図解イメージ 2つの円の重なり部分のみ 左側の円全体 + 重なり部分

使い分け:

  • INNER JOIN: データの関連性が必須である場合。例えば、「実際に注文があった商品のリスト」や「部署に所属している従業員のリスト」のように、両方のデータが存在しないと意味をなさない情報を取得する場合。
  • LEFT JOIN: 左側のテーブルのデータが重要であり、それに付随する情報があれば表示したい場合。例えば、「全ての顧客リスト(注文の有無にかかわらず)」や「全ての製品リスト(レビューの有無にかかわらず)」のように、マスターデータを網羅的に表示したい場合。

8.2. LEFT OUTER JOIN vs RIGHT OUTER JOIN

特徴 LEFT OUTER JOIN RIGHT OUTER JOIN
結果に含まれる行 左側のテーブルの全ての行 + 右側で一致する行 右側のテーブルの全ての行 + 左側で一致する行
一致しない行 左側の行は含まれ、右側のカラムはNULLになる 右側の行は含まれ、左側のカラムはNULLになる
主な目的 左側のデータを基軸に、関連データを付加 右側のデータを基軸に、関連データを付加
使用例 全ての顧客と、あれば注文情報を表示 全ての注文と、あれば顧客情報を表示
図解イメージ 左側の円全体 + 重なり部分 右側の円全体 + 重なり部分

使い分け:

機能的にはLEFT JOINとRIGHT JOINは対称的です。テーブルA LEFT JOIN テーブルBテーブルB RIGHT JOIN テーブルA とほぼ同じ結果になります(カラムの並び順は異なる場合があります)。

一般的には、LEFT JOINの方がより直感的でよく使用されます。クエリを読む際に、FROM テーブルA LEFT JOIN テーブルB と書かれていれば、「Aが主で、Bの情報を付け加えているんだな」と理解しやすいからです。RIGHT JOINを使用する代わりに、テーブルの順序を入れ替えてLEFT JOINを使用することが推奨されることが多いです。例えば、customers RIGHT JOIN orders と書く代わりに orders LEFT JOIN customers と書く方が一般的です。

8.3. LEFT OUTER JOIN vs FULL OUTER JOIN

特徴 LEFT OUTER JOIN FULL OUTER JOIN
結果に含まれる行 左側のテーブルの全ての行 + 右側で一致する行 両方のテーブルの全ての行
一致しない行 左側の行は含まれ、右側のカラムはNULLになる 一致しない左側の行は右側がNULL、一致しない右側の行は左側がNULL
主な目的 左側のデータを基軸に、関連データを付加 両方のテーブルのデータを網羅的に結合
使用例 全ての顧客と、あれば注文情報を表示 全ての顧客と全ての注文(関連データがあれば結合、なければNULL)を表示
図解イメージ 左側の円全体 + 重なり部分 2つの円の和集合全体

使い分け:

  • LEFT JOIN: 左側のテーブルのデータが全て必要で、右側のデータはあれば表示したい場合。最も頻繁に使われるパターンです。
  • FULL JOIN: 左側にも右側にも存在しないデータを全て含めて、両方のデータを網羅的に比較・分析したい場合。例えば、「全ての従業員」と「全ての部署」を結合して、部署に所属しない従業員と、従業員がいない部署の両方を一度にリストアップしたい場合などに使用します。ただし、FULL JOINをサポートしていないデータベースシステムもあります。

9. まとめ

この記事では、SQLのLEFT OUTER JOINについて、その基本的な定義から具体的な使用例、応用方法、そして注意点までを詳しく解説しました。

  • LEFT OUTER JOIN(またはLEFT JOIN)は、左側のテーブルの全ての行を結果に含め、それに対応する右側のテーブルの行があれば結合し、なければ右側のカラムをNULLとする結合方法です。
  • 構文は FROM テーブルA LEFT JOIN テーブルB ON 結合条件 です。
  • 主に、左側のテーブル(マスターデータなど)を基軸として、それに紐づく右側のテーブル(トランザクションデータなど)の情報を補足的に表示したい場合に利用されます。関連データが存在しない場合でも、基軸となるデータは失われません。
  • WHERE句で右側のテーブルのカラムに対して条件を指定する場合、NULLの行を残すためにはOR カラム名 IS NULLのような条件を付け加える必要があるなど、フィルタリングの挙動に注意が必要です。
  • 複数のテーブルをLEFT JOINで結合したり、GROUP BY句や集計関数と組み合わせて使うことで、より複雑なレポートや分析が可能になります。
  • パフォーマンスを考慮し、結合キーには適切にインデックスを設定することが重要です。
  • INNER JOINは両方に一致する行のみ、RIGHT JOINは右側全て、FULL JOINは両方全てを結果に含める点で、LEFT JOINとは異なります。実務ではLEFT JOINが最も頻繁に使用されるOUTER JOINの種類と言えます。

LEFT JOINはデータベースから有用な情報を引き出すための非常に強力かつ不可欠なツールです。マスターデータとトランザクションデータを組み合わせたり、「存在しない」関連データを特定したりする際に、その威力を発揮します。

この記事を通じて、LEFT JOINの仕組みと使い方をしっかり理解し、あなたのSQLスキルをさらに向上させ、日々のデータ操作や分析に役立てていただければ幸いです。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール