SQL LEFT OUTER JOINとは?図解で分かりやすく解説
はじめに:なぜJOINが必要なのか?
リレーショナルデータベースにおいて、データはしばしば複数のテーブルに分けて格納されます。これは、データの重複を避け、整合性を保ち、管理を容易にするためです。例えば、顧客情報と注文情報を考えてみましょう。顧客情報は「顧客テーブル」に、注文情報は「注文テーブル」にそれぞれ格納するのが一般的です。
しかし、私たちがデータを利用する際、これら複数のテーブルにまたがる情報をまとめて見たいことがよくあります。「この顧客はどのような注文をしたか?」や「この注文はどの顧客が行ったか?」を知るには、顧客テーブルと注文テーブルを関連付けてデータを取得する必要があります。
このように、複数のテーブルから関連するデータを結びつけて取得するためのSQLの機能がJOINです。
JOINにはいくつかの種類があります。最も基本的なものはINNER JOINですが、これに加えてOUTER JOINという種類も存在します。OUTER JOINにはさらにLEFT OUTER JOIN、RIGHT OUTER JOIN、FULL 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は以下の結果セットを生成します。
- 左側のテーブルの全ての行。
- 左側のテーブルの行と結合条件で一致する右側のテーブルの行。
- 左側のテーブルの行に対応する右側のテーブルの行が存在しない場合、左側の行は結果に含まれますが、右側のテーブルから取得されるべきカラムの値はすべて
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 | |
---|---|---|
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 | 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つの条件のどちらかを満たす行を選択します。
o.order_date >= '2023-01-01' AND o.order_date < '2023-02-01'
: 注文日が2023年1月中の行。これは、2023年1月中に注文した顧客のデータ(結合されたordersテーブルのデータが存在する行)を抽出します。o.order_id IS NULL
:orders
テーブルのorder_id
がNULL
の行。これは、LEFT JOINによってordersテーブル側に一致する行がなかったためNULL
となった行、つまり「注文履歴がない」または「2023年1月中の注文履歴がない」顧客の行を抽出します。
注意点:
もしWHERE o.order_date >= '2023-01-01' AND o.order_date < '2023-02-01'
だけにすると、o.order_date
がNULL
の行(注文がなかった顧客)は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 | |
---|---|---|
2 | 田中 | [email protected] |
4 | 鈴木 | [email protected] |
解説:
まずcustomers
とorders
をLEFT JOINします。結果には全ての顧客が含まれ、注文がない顧客のorders
テーブル側のカラムはNULL
になります。
次に、WHERE o.order_id IS NULL
という条件で絞り込みます。これにより、LEFT JOINの結果のうち、orders
テーブル側に一致する行がなかった(つまり注文履歴がなかった)行だけが抽出されます。order_id
はorders
テーブルの主キーであり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 JOIN
でcustomers
とorders
を結合します。田中さん(customer_id 2)は注文がないため、この段階でorders関連のカラムはNULLになります。 - 次に、その結果と
order_items
をorder_id
でLEFT JOIN
します。田中さんの行は既にorders側がNULLなので、order_items側もNULLになります。佐藤さんの注文には対応するorder_itemsがあるため、結合されます。 - 最後に、その結果と
products
をproduct_id
でLEFT JOIN
します。order_itemsに該当するproduct_idがproductsテーブルにある場合は結合、ない場合はNULLになります。今回は全てのproduct_idがproductsテーブルにあるため、すべて結合されています。もし、order_items
テーブルに存在するproduct_id
がproducts
テーブルにない場合、その行の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 |
解説:
- まず
customers
とorders
をLEFT JOINします。注文がない顧客(田中、鈴木)のorders
側のカラムはNULL
になります。 - 次に、
GROUP BY c.customer_id, c.name
によって、顧客ごとにグループ化します。 COUNT(o.order_id)
は、各グループ内でorder_id
がNULL
ではない行の数を数えます。注文がある顧客のグループでは実際の注文数、注文がない顧客のグループでは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_amount
が0
になります。
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が含まれる可能性のあるカラムを集計する際は、
COALESCE
やIFNULL
でNULLを適切な値(0など)に置換することを検討する。 - NULLが含まれる可能性のあるカラムを条件とする場合は、
IS NULL
やIS 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スキルをさらに向上させ、日々のデータ操作や分析に役立てていただければ幸いです。