【SQL】内部結合・外部結合入門 ~ INNER JOIN・LEFT JOIN・FULL OUTER JOIN ~

# はじめに

私は業務で Ruby on Rails を使用しています。
Rails の Active Record がとても優秀なのは周知の事実。
そのため発行される SQL スクリプトを意識しないと、システムのボトルネックが次々と生み出されるのもこれまた周知の事実。
ということで、SQL をガシガシ勉強しています。
今回は外部結合に関して、書いていきたいと思います。

# TL;DR

**INNER JOIN**
```sql
SELECT * FROM GroupA INNER JOIN GroupB ON GroupA.id = GroupB.id;
```

**LEFT JOIN**
```sql
SELECT * LEFT JOIN GroupB ON GroupA.id = GroupB.id;
```

**RIGHT JOIN**
```sql
SELECT * FROM GroupA RIGHT JOIN GroupB ON GroupA.id = GroupB.id;
```

**LEFT JOIN EXCLUDING INNER JOIN**
```sql
SELECT * FROM GroupA LEFT JOIN GroupB ON GroupA.id = GroupB.id
    WHERE GroupB.id IS NULL;
```

**RIGHT JOIN EXCLUDING INNER JOIN**
```sql
SELECT * FROM GroupA RIGHT JOIN GroupB ON GroupA.id = GroupB.id
    WHERE GroupA.id IS NULL;
```

**FULL OUTER JOIN**
```sql
SELECT * FROM GroupA LEFT JOIN GroupB ON GroupA.id = GroupB.id
UNION
SELECT * FROM GroupA RIGHT JOIN GroupB ON GroupA.id = GroupB.id;
```

**FULL OUTER JOIN EXCLUDING INNER JOIN**
```sql
SELECT * FROM GroupA LEFT JOIN GroupB ON GroupA.id = GroupB.id
    WHERE GroupB.id IS NULL
UNION
SELECT * FROM GroupA RIGHT JOIN GroupB ON GroupA.id = GroupB.id
    WHERE GroupA.id IS NULL;
```


# そもそものお話

この章では、
**そもそも SQL のような宣言型言語では手続き型言語を扱っている時とは違う思考をする必要がある**
ということを述べていきます。

宣言型と手続き型の違いに関して stack overflow の記事で良いものがありました。

>Declarative programming is where you say what you want without having to say how to do it.
> With procedural programming, you have to specify exact steps to get the result.
>
> For example, SQL is more declarative than procedural, because the queries don't specify steps to produce the result.

【引用: [*What is the difference between declarative and procedural programming paradigms?*](https://stackoverflow.com/questions/1619834/what-is-the-difference-between-declarative-and-procedural-programming-paradigms#:~:text=Declarative%20programming%20is%20where%20you,steps%20to%20produce%20the%20result.)】

これを日本語に訳すと以下のようなニュアンスだと思います。

> 宣言型プログラミングはどのように実現するかを言うのではなく、欲しいものが何であるかを記述する。
> 手続き型プログラミングでは、結果を得るために正確なステップを明記する必要がある。
>
> 例えば SQL は手続き的というよりも宣言的である。
> それはクエリーは結果を生み出すためにステップを明記しないからである。

つまり、宣言型言語の入力はその名の通り欲しいものが何かを宣言するだけで良いということです。
入力の仕方が違うため、宣言型言語を扱う時には手続き型言語とは異なる思考の方法に切り替える必要があります。
このことは　SQL のような宣言型言語を最初に学んだわけでも主戦場としているわけでもない私にとって目から鱗でした。
どのような思考の違いがあるのか、それは以下の画像がよく解説してくれています。

![db_cm_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/89a85bab-3f15-161d-93f7-01c77c3a2d0a.gif)
【引用: [*手続き型から宣言型・集合指向に頭を切り替えるための7箇条*](https://mickindex.sakura.ne.jp/database/db_laws.html)】

**四角を描くな、円を描け**

ということです。


# 結合と集合

前章で SQL の思考に切り替わったかと思うので、ここからは本題の結合について述べていきます。

まず、結合は過程でありその成果物が集合であると私は認識しています。
そのため SQL スクリプトによる結合の方法を知っていても、目的とする集合がどのような集合であるかを捉えることができなければ結合の使いようがありません。
あるいは、知っている結合を総当たりで試すという非効率的なことをしなければなりません。
従って、結合操作とそれによって得られる集合を対応づけて覚える必要があります。
その時に具体的なサンプルデータで覚えるよりも絵で考える方が覚えやすいと個人的に思っています。
ここで、良い画像を見つけたのでドーンと載せちゃいます。
以降の章からはこれらの集合を得るための操作や考え方について述べていきます。

![1_GigXPhr4Ue2zbrgIIoB8Lw.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/668a2647-a073-dd07-760d-0fefbace0d36.png)
【引用: [*Ace the Coding Interview: SQL— Inner Join*](https://medium.com/@iammanolov98/mastering-sql-joins-coding-interview-preparation-innerjoin-e96bef58afc2)】

# 結合操作

今回使用するのは MySQL です。
また、バージョンは `8.0.34` を使用します。

```bash
$ mysql --version
mysql  Ver 8.0.34 for macos13.3 on arm64 (Homebrew)
$ 
```

この章では以下のようなサンプルデータを用いてそれぞれの結合操作を行います。
```SQL
CREATE TABLE GroupA (
  id    INTEGER    NOT NULL,
  name  VARCHAR(8) NOT NULL
);

INSERT INTO GroupA VALUES
  (1, '斉藤'),
  (2, '島田'),
  (3, '須藤');

CREATE TABLE GroupB (
  id    INTEGER    NOT NULL,
  name  VARCHAR(8) NOT NULL
);

INSERT INTO GroupB VALUES
  (1, '斉藤'),
  (2, '島田'),
  (4, '瀬古');
```

## INNER JOIN

![スクリーンショット 2023-10-15 3.57.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/8e67ab07-995e-1cb1-aff5-488afef85f0b.png)

以下スクリプトは　`GroupA` に対して `GroupB` を INNER JOIN した操作です。

```sql
SELECT GroupA.id, GroupA.name AS A_name, GroupB.name AS B_name
    FROM GroupA INNER JOIN GroupB ON GroupA.id = GroupB.id;
```

<details>
<summary>INNER JOIN 操作結果</summary>
<div>

```bash
+----+--------+--------+
| id | A_name | B_name |
+----+--------+--------+
|  1 | 斉藤   | 斉藤   |
|  2 | 島田   | 島田   |
+----+--------+--------+
```
</div>
</details>

この結合操作は以下のようなイメージです。
![スクリーンショット 2023-10-15 4.10.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/6173e3d9-45c9-420f-46ab-5de734029c28.png)


なお、`GroupB` に対して `GroupA` を INNER JOIN してもその性質上同様の結果が得られます。

```sql
SELECT GroupB.id, GroupA.name AS A_name, GroupB.name AS B_name
    FROM GroupB INNER JOIN GroupA ON GroupA.id = GroupB.id;
```

<details>
<summary>INNER JOIN 操作結果</summary>
<div>

```bash
+----+--------+--------+
| id | A_name | B_name |
+----+--------+--------+
|  1 | 斉藤   | 斉藤   |
|  2 | 島田   | 島田   |
+----+--------+--------+
```
</div>
</details>

## LEFT JOIN / RIGHT JOIN

![スクリーンショット 2023-10-14 18.43.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/dbf0a862-ebd4-9557-7422-66e941ee3d76.png)

以下スクリプトは `GroupA` に対して `GroupB` を LEFT JOIN した操作です。

```sql
SELECT GroupA.id, GroupA.name AS A_name, GroupB.name AS B_name
    FROM GroupA LEFT JOIN GroupB ON GroupA.id = GroupB.id;
```

<details>
<summary>LEFT JOIN 操作結果</summary>
<div>

```bash
+----+--------+--------+
| id | A_name | B_name |
+----+--------+--------+
|  1 | 斉藤   | 斉藤   |
|  2 | 島田   | 島田   |
|  3 | 須藤   | NULL   |
+----+--------+--------+
```
</div>
</details>

この結合操作は以下のようなイメージです。
LEFT JOIN では基準は FROM 句の集合となります。(今回のケースでは `GroupA`)
![スクリーンショット 2023-10-14 19.54.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/151645af-95d5-f771-eecb-ca6cffe8a6df.png)




次に、`GroupA` に対して `GroupB` を RIGHT JOIN します。

```sql
SELECT GroupB.id, GroupA.name AS A_name, GroupB.name AS B_name
    FROM GroupA RIGHT JOIN GroupB ON GroupA.id = GroupB.id;
```


<details>
<summary>RIGHT JOIN 操作結果</summary>
<div>

```bash
+----+--------+--------+
| id | A_name | B_name |
+----+--------+--------+
|  1 | 斉藤   | 斉藤   |
|  2 | 島田   | 島田   |
|  4 | NULL   | 瀬古   |
+----+--------+--------+
```
</div>
</details>

RIGHT JOIN では基準は RIGHT JOIN 句の集合です。(今回のケースでは `GroupB`)
![スクリーンショット 2023-10-14 19.55.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/c7da0f3a-4f46-24e3-6056-af8d73c2b862.png)

以上のように、LEFT JOIN と RIGHT JOIN は操作します。
LEFT JOIN と RIGHT JOIN の違いは左右どちらの集合を基準にするかという点にあるので、等価的に操作することができます。

## INNER JOIN を除いた LEFT JOIN / RIGHT JOIN

![スクリーンショット 2023-10-15 4.25.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/258accec-6ba0-9325-424e-98a94c5f18b8.png)


INNER JOIN を除いた LEFT JOIN / RIGHT JOIN は 前節の LEFT JOIN / RIGHT JOIN の操作に加えて、 WHERE 句によって基準ではない方のテーブルの id が NULL であるものを絞り込むことで得ることができます。

以下スクリプトは `GroupA` に対して `GroupB` を LEFT JOIN して、INNER JOIN を除いた操作です。

```sql
SELECT GroupA.id, GroupA.name AS A_name, GroupB.name AS B_name
    FROM GroupA LEFT JOIN GroupB ON GroupA.id = GroupB.id
    WHERE GroupB.id IS NULL;
```

<details>
<summary>INNER JOIN を除いた LEFT JOIN 操作結果</summary>
<div>

```bash
+----+--------+--------+
| id | A_name | B_name |
+----+--------+--------+
|  3 | 須藤   | NULL   |
+----+--------+--------+
```
</div>
</details>

この結合操作は以下のようなイメージです。
![スクリーンショット 2023-10-15 4.43.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/cb0dffc9-3de5-a406-3466-3911a41a7063.png)



次に、`GroupA` に対して `GroupB` を RIGHT JOIN し、INNER JOIN を除いた操作を示します。

```sql
SELECT GroupB.id, GroupA.name AS A_name, GroupB.name AS B_name
    FROM GroupA RIGHT JOIN GroupB ON GroupA.id = GroupB.id
    WHERE GroupA.id IS NULL;
```

<details>
<summary>INNER JOIN を除いた RIGHT JOIN 操作結果</summary>
<div>

```bash
+----+--------+--------+
| id | A_name | B_name |
+----+--------+--------+
|  4 | NULL   | 瀬古   |
+----+--------+--------+
```
</div>
</details>

## FULL OUTER JOIN

![スクリーンショット 2023-10-15 4.48.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/5c49b083-cf1d-4384-3b6c-2457fb138ad2.png)

MySQL では現在 FULL OUTER JOIN をサポートしていないため UNION 句を使用します。
UNION 句については、公式では以下のように説明されています。

> UNION は、複数の SELECT ステートメントの結果を単一の結果セットに結合します。
> (略)
> デフォルトでは、重複行は UNION の結果から削除されます。

【引用: [*MySQL 8.0 リファレンスマニュアル UNION 句*](https://dev.mysql.com/doc/refman/8.0/ja/union.html)】

この UNION 句の性質によって、FULL OUTER JOIN を実現することができます。
具体的には、LEFT JOIN の SELECT 文と RIGHT JOIN の SELECT 文を UNION によって結合します。
INNER JOIN の部分が重複しますが、説明の通り重複行は UNION の結果から削除されるため心配事ではありません。
以下スクリプトは擬似的に `GroupA` と `GroupB` を FULL OUTER JOIN した操作です。

```sql
SELECT GroupA.id, GroupA.name AS A_name, GroupB.name AS B_name
    FROM GroupA LEFT JOIN GroupB ON GroupA.id = GroupB.id
UNION
SELECT GroupB.id, GroupA.name AS A_name, GroupB.name AS B_name
    FROM GroupA RIGHT JOIN GroupB ON GroupA.id = GroupB.id;
```

<details>
<summary>FULL OUTER JOIN 操作結果</summary>
<div>

```bash
+----+--------+--------+
| id | A_name | B_name |
+----+--------+--------+
|  1 | 斉藤   | 斉藤   |
|  2 | 島田   | 島田   |
|  3 | 須藤   | NULL   |
|  4 | NULL   | 瀬古   |
+----+--------+--------+
```
</div>
</details>

以下は FULL OUTER JOIN の結合イメージです。
(※ UNION を使用した上記のスクリプトの結合操作イメージではありません)

![スクリーンショット 2023-10-15 5.09.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/f94513eb-0457-2c70-5024-87b6cecc6771.png)


## INNER JOIN を除いた FULL OUTER JOIN

![スクリーンショット 2023-10-15 6.02.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1913290/75b5c8cb-8664-d73a-efa9-a1adc1817906.png)


INNER JOIN を除いた FULL OUTER JOIN を MySQL を使用する場合、前々節「INNER JOIN を除いた LEFT JOIN / RIGHT JOIN」を利用することで実現することができます。
具体的には、**INNER JOIN を除いた LEFT JOIN** と **INNER JOIN を除いた RIGHT JOIN** を UNION によって結合することで、INNER JOIN を除いた FULL OUTER JOIN を実現できます。
つまり、発想は前節と全く同じです。
以下スクリプトは擬似的に `GroupA` と `GroupB` を FULL OUTER JOIN して、INNER JOIN を除いた操作です。

```sql
SELECT GroupA.id, GroupA.name AS A_name, GroupB.name AS B_name
    FROM GroupA LEFT JOIN GroupB ON GroupA.id = GroupB.id
    WHERE GroupB.id IS NULL
UNION
SELECT GroupB.id, GroupA.name AS A_name, GroupB.name AS B_name
    FROM GroupA RIGHT JOIN GroupB ON GroupA.id = GroupB.id
    WHERE GroupA.id IS NULL;
```

<details>
<summary>FULL OUTER JOIN 操作結果</summary>
<div>

```bash
+----+--------+--------+
| id | A_name | B_name |
+----+--------+--------+
|  3 | 須藤   | NULL   |
|  4 | NULL   | 瀬古   |
+----+--------+--------+
```
</div>
</details>

# おわりに

今回一気に七種類の結合操作を見ました。
これら全てを覚えるのは大変ですよね。
しかし、実際は INNER JOIN, LEFT JOIN, FULL OUTER JOIN の三種類さえ抑えれば、あとは派生的に覚えることができると思います。

今回 SQL の内部結合・外部結合を抑えることができたので、次回は Rails の ActiveRecord とこれら結合を絡めた記事を書く予定です。