SQLのJOINが苦手という人に向けて

# はじめに
この仕事に携わっていると、結構な経験者でもSQL、特にテーブル同士の結合が苦手という人によく会います。
で、話を聞くと苦手な理由はテーブル同士のリレーションを視覚的にイメージ出来ないことにあるのかと。。

かつて自分もSQLは大の苦手でした。
２、３年目の時に某電力の大規模Javaプロジェクトに放り込まれました。
テーブル同士の多重度が多対多、オンラインPGで1SELECTあたり3000〜5000行という変態的なSQL。
MVCモデルが出始めの頃、Webフレームワークなんて言葉が流行り出す前
バックエンドからフロントエンドまで全てをベタ書き。
期日までに徹夜・休出しながら直したり書かないといけなかったものですから、それはもう大変な苦労と挫折を味わいました。

そんな環境下でテーブル同士のリレーションを図に整理しながら、書いたり、手直ししたりする内に、視覚的にイメージ出来るようになり、SQLへの苦手意識はほとんど無くなりました。

「乗り越えた壁はいつしか自分を守る盾になる」と誰かが言ってたアレです（笑）

SQLが苦手な方向けの説明になってます。
ある程度のSQL経験者には退屈な内容になっておりますので、あしからず。

# 3つのJOIN
`INNER JOIN`・`LEFT (OUTER) JOIN`・`FULL (OUTER) JOIN`の3つのJOINについて説明します。

※他にも`RIGHT (OUTER) JOIN`、`CROSS JOIN`等があります。
ただし、`RIGHT (OUTER) JOIN`は`LEFT (OUTER) JOIN`をただ左右反転させたもの、`CROSS JOIN`はよっぽど切羽詰まった状況使うか、使う時点で処理フローを変えることを検討した方が良い、どちらも実務で使うことは稀なので割愛します。

## INNER JOIN
説明するまでもありませんが、最も一般的でよく使われるJOINですね。
`内部結合`っていわれるやつです。
仮に`A`と`B`というテーブルを`INNER JOIN`した場合の関係性をベン図に表すとこんな感じです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573734/18ef5414-555b-9033-6c29-b3deb62cc879.png" width=50%>
「`テーブルA`にも存在し、かつ、`テーブルB`にも存在するレコードを取ってくる」とゆうJOINです。
真ん中の赤く重なり合ってるところをとります。
テーブルA・Bがこんな状態だったとします。

- テーブルA

| PK_A   | VAL_A |
|-------:|:------|
| 001    | AAA   |
| 002    | BBB   |
| 004    | CCC   |
| 005    | DDD   |
| 007    | EEE   |
| 009    | FFF   |

- テーブルB

| PK_B   | VAL_B |
|-------:|:------|
| 001    | GGG   |
| 003    | HHH   |
| 006    | III   |
| 008    | JJJ   |
| 009    | KKK   |

`PK_A`・`PK_B`がそれぞれの主キーです。

`A`のPK_Aと`B`のPK_BでJOINします。

```
SELECT
    PK_A,
    VAL_A,
    PK_B,
    VAL_B
FROM
    A
    INNER JOIN B ON
        A.PK_A = B.PK_B
```
結果は

| PK_A   | VAL_A | PK_B   | VAL_B |
|-------:|:------|-------:|:------|
| 001    | AAA   | 001    | GGG   |
| 009    | FFF   | 009    | KKK   |

ですね。
`A`・`B`両方に存在する`001`と`009`のみが抽出されます。

## LEFT OUTER JOIN
これもよく使われるJOINです。
`左外部結合`っていわれます。
たまにレガシーなところ行くといまだに`(+)`なんて記述もありますが、アレは古い`Oracle`の方言です。
可読性・保守性が著しく低下するので間違っても`(+)`を記述するのはやめておきましょう。
また、`OUTER`は省略可能ですが、`INNER JOIN`との違いを明確にさせるために、あえて省略しないで説明します。

ベン図に表すとこんな感じです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573734/778f3f79-4f53-15ba-4121-ce266762189e.png" width=50%>
「`テーブルA`の全てプラス、`テーブルA`と重なり合う`テーブルB`のレコードを取ってくる」とゆうJOINです。

先ほどと同じように`A`のPK_Aと`B`のPK_BでJOINします。

```
SELECT
    PK_A,
    VAL_A,
    PK_B,
    VAL_B
FROM
    A
    LEFT OUTER JOIN B ON
        A.PK_A = B.PK_B
```

結果は

| PK_A   | VAL_A | PK_B   | VAL_B |
|-------:|:------|-------:|:------|
| 001    | AAA   | 001    | GGG   |
| 002    | BBB   | null   | null  |
| 004    | CCC   | null   | null  |
| 005    | DDD   | null   | null  |
| 007    | EEE   | null   | null  |
| 009    | FFF   | 009    | KKK   |

`A`の全件と、`A`のキーと一致する`B`が取得されていることがわかります。
`A`のキーと一致しない`B`の**歯抜け**の部分については`null`が埋め込まれていますね。

## FULL OUTER JOIN
`完全外部結合`と言われます。
実務で使われる頻度は少ないですが、覚えておいて損はないです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573734/9c6c8a70-6c26-b20d-f08e-b5791b839833.png" width=50%>
`A`と`B`を重なり合う部分も含めて全件取ってきます。

```
SELECT
    PK_A,
    VAL_A,
    PK_B,
    VAL_B
FROM
    A
    FULL OUTER JOIN B ON
        A.PK_A = B.PK_B
```
結果は

| PK_A   | VAL_A | PK_B   | VAL_B |
|-------:|:------|-------:|:------|
| 001    | AAA   | 001    | GGG   |
| 002    | BBB   | null   | null  |
| null   | null  | 003    | HHH   |
| 004    | CCC   | null   | null  |
| 005    | DDD   | null   | null  |
| null   | null  | 006    | III   |
| 007    | EEE   | null   | null  |
| null   | null  | 008    | JJJ   |
| 009    | FFF   | 009    | KKK   |

`A`・`B`の全件が取得されていることがわかります。
そしてお互いの**歯抜け**の部分については`null`が埋め込まれていますね。

# こんなんどうでしょう？
ここまでお読み頂き、JOINの視覚的なイメージが着いたでしょうか？

では、こんなのはいかがでしょう？
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573734/2a88ab6b-751e-0a09-76e2-18633e09990f.png" width=50%>

「`テーブルB`と重なり合わない`テーブルA`のレコードを取ってくる」とゆうJOIN、、
もちろん、そんなJOINありません。

ではどうすれば良いか？

```
SELECT
    PK_A,
    VAL_A,
    PK_B,
    VAL_B
FROM
    A
    LEFT OUTER JOIN B ON
        A.PK_A = B.PK_B
WHERE
    B.PK_B IS NULL
```
`A`と`B`を`LEFT OUTER JOIN`させてから、
`WHERE`句で`B`の主キー`PK_B`が`NULL`のレコードを指定しています。

結果は

| PK_A   | VAL_A | PK_B   | VAL_B |
|-------:|:------|-------:|:------|
| 002    | BBB   | null   | null  |
| 004    | CCC   | null   | null  |
| 005    | DDD   | null   | null  |
| 007    | EEE   | null   | null  |

**歯抜け**レコードを利用するのがポイントです。
前述の`LEFT OUTER JOIN`の結果から`B`の**歯抜け**レコードを指定することで
`A`・`B`両方にある`001`・`009`のレコードが除外され、
`B`と重なり合わない`A`のレコードを取得できます。

これが「**歯抜け**」という文字をあえて**太字**にした理由です。


# じゃあ、こんなのも
理屈が分かっちゃえば、こんなのも簡単ですね。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/573734/52862f36-7013-070e-606f-f12553e97c19.png" width=50%>
「重なり合わない部分の`テーブルA`と`テーブルB`のレコードを取ってくる」
同じように**歯抜け**レコードをうまく使いましょう。

```
SELECT
    PK_A,
    VAL_A,
    PK_B,
    VAL_B
FROM
    A
    FULL OUTER JOIN B ON
        A.PK_A = B.PK_B
WHERE
    A.PK_A IS NULL
OR  B.PK_B IS NULL

```
結果は

| PK_A   | VAL_A | PK_B   | VAL_B |
|-------:|:------|-------:|:------|
| 002    | BBB   | null   | null  |
| null   | null  | 003    | HHH   |
| 004    | CCC   | null   | null  |
| 005    | DDD   | null   | null  |
| null   | null  | 006    | III   |
| 007    | EEE   | null   | null  |
| null   | null  | 008    | JJJ   |

前述の`FULL OUTER JOIN`の結果から`A`または`B`の**歯抜け**レコードを指定することによって
`A`・`B`両方にある`001`・`009`のレコードが除外され、
お互い重なり合わない`A`・`B`のレコードを取得できます。

# さいごに
最近主流のアーキテクチャでは（バッチ処理を除いて）複雑なクエリを書くのはアンチパターンです。
なので上記のようなクエリを組む事も稀かと思いますが、テーブル同士を比較してアンマッチリストを抽出したい時や、ゴミデータ・不正データを調査したい時などにJOINを使いこなせると色々と便利です。

あと気をつけなくてならないのは`SELECT`文は`F・W・G・H・S・O`の順番に評価されます。
何かというと`FROM`・`WHERE`・`GROUP BY`・`HAVING`・`SELECT`・`ORDER BY`の頭文字です。
`JOIN`は`FROM`句中で書くため、一番最初に評価されます。
なので大量データを保持したテーブル同士をJOINさせるとパフォーマンスの劣化に繋がったりします。
サブクエリや`WITH`句を使って比較に必要なデータに絞ってからJOINさせる等のパフォーマンスの考慮は必要です。

ただし、最近は`Oracle`・`MySQL`のオプティマイザによって実行計画が最適化されるので、トンチキなSQLを書いても大体は動いてくれます。
自分が走り出しの頃は変なSQL書いたら幾度となく先輩にブッ◯されそうになりました（笑）

で、自分が言いたいのはテーブル同士のリレーションを視覚的にイメージできれば、どんなSQLがやって来ても怖くない、
何千行というSQLも結局はテーブルのJOINの組み合わせに過ぎないということです。

最後までお読み頂きありがとうございました。