SQLのONとWHEREはなぜ違うのか

LWP | SQLのONとWHEREはなぜ違うのか

SQLのONとWHEREはなぜ違うのか

LEFT JOINにしたのに社員が消えた。残したい行から条件を考えよう。

ストーリー

コイ丸先輩は、社員を全員残すつもりでLEFT JOINを使いました。ところが部門名を「経理」に絞ると、一人しか残りません。アユさんと、社員の一行・部門の一行・部門IDの対応を確かめ、ONとWHEREが決める範囲を見直します。

社員4行・部門3行は架空の説明用データです。社員の部門ID30に対応する部門はなく、高橋さんの部門IDはNULLです。部門IDは部門表で一意という前提です。

1 社員と部門、何が対応する?

社員4行と部門3行を部門IDで対応させる。30・NULL・40には対応する相手がない。

図1|社員4行と部門3行を部門IDで対応させる。30・NULL・40には対応する相手がない。

2 INNER JOINとLEFT JOIN

INNER JOINは対応する田中・鈴木の2行、LEFT JOINは社員側の佐藤・高橋も残す4行となる。

図2|INNER JOINは対応する田中・鈴木の2行、LEFT JOINは社員側の佐藤・高橋も残す4行となる。

3 ONに「経理」を置くと

LEFT JOINのONへ経理条件を加えると、鈴木だけが部門行と対応し、社員4行は残る。

図3|LEFT JOINのONへ経理条件を加えると、鈴木だけが部門行と対応し、社員4行は残る。

4 WHEREに「経理」を置くと

LEFT JOIN後のWHEREはTRUEになる行だけを残す。経理は鈴木1行となり、NULL比較はUNKNOWNになる。

図4|LEFT JOIN後のWHEREはTRUEになる行だけを残す。経理は鈴木1行となり、NULL比較はUNKNOWNになる。

5 INNER JOINでは同じ結果

INNER JOINでは、同じ経理条件をONとWHEREのどちらに置いても、対応する鈴木1行が残る。

図5|INNER JOINでは、同じ経理条件をONとWHEREのどちらに置いても、対応する鈴木1行が残る。

6 4つの結果を並べてみる

INNERはON・WHEREとも鈴木1行。LEFTはONで社員4行、WHEREで鈴木1行となる。

図6|INNERはON・WHEREとも鈴木1行。LEFTはONで社員4行、WHEREで鈴木1行となる。

7 残したい行から、条件を置く

結合する組合せをON、最終結果に残す行をWHEREで考える。図の順番は論理的な説明であり、物理実行順ではない。

図7|結合する組合せをON、最終結果に残す行をWHEREで考える。図の順番は論理的な説明であり、物理実行順ではない。

まとめ

ONは結合する組合せの条件、WHEREは結合結果に残す行の条件です。LEFT JOINで経理条件をONへ置くと社員4行が残り、WHEREへ置くと鈴木さん1行だけが残ります。NULLと「経理」の比較はUNKNOWNになり、WHEREはTRUEの行だけを残します。結合・NULL補完・絞込みの順番は意味を説明するための論理的な整理で、DBの物理的な実行順を強制するものではありません。

今回の仕掛け

同じ社員・部門データを全図で使い、結合の種類と条件の置き場所だけを変えました。結果の違いはPostgreSQL公式の表式・結合の説明にも照合しています。

INNER JOINで結果が同じになる例と、LEFT JOINで結果が変わる例を並べ、要件の「全社員を残す」「経理の社員だけを残す」へ戻りました。条件を置く場所による速度差は測定していません。

今回のソースコードのサンプル

サンプルコード 同じLEFT JOINでONとWHEREを比べる(SQLite)

01

WITH

02

employee(emp_id, emp_name, dept_id) AS (

03

  VALUES (1, '田中', 10), (2, '鈴木', 20),

04

         (3, '佐藤', 30), (4, '高橋', NULL)

05

),

06

department(dept_id, dept_name) AS (

07

  VALUES (10, '営業'), (20, '経理'), (40, '総務')

08

)

09

SELECT 'ON' AS pattern, e.emp_id, e.emp_name, d.dept_name

10

FROM employee e

11

LEFT JOIN department d

12

  ON e.dept_id = d.dept_id AND d.dept_name = '経理'

13

UNION ALL

14

SELECT 'WHERE', e.emp_id, e.emp_name, d.dept_name

15

FROM employee e

16

LEFT JOIN department d ON e.dept_id = d.dept_id

17

WHERE d.dept_name = '経理'

18

ORDER BY pattern, emp_id;

関数の説明

関数を定義する例ではなく、共通の入力をWITHで用意し、二つのLEFT JOINの結果をUNION ALLで並べるSQLです。

コードの説明

patternがONの行は経理条件を結合条件に含め、WHEREの行は部門IDで結合した後の結果を経理条件で絞ります。emp_id・emp_name・dept_idは図の社員ID・氏名・部門IDに対応します。入力が小さいため、比較の結果を手でも確認できます。

入力・概要

社員は田中10・鈴木20・佐藤30・高橋NULLの4行、部門は営業10・経理20・総務40の3行です。WITHのVALUESを使えるSQLite向けの単独クエリです。

結果

ON側は田中NULL・鈴木経理・佐藤NULL・高橋NULLの4行、WHERE側は鈴木経理の1行、合計5行になります。NULLの表示方法は使用するクライアントで変わります。

読むポイント

条件を移す前に、経理以外の社員を残す必要があるかを確かめます。NULLは空文字や0と置き換えません。実行確認は隔離したSQLite上で行い、既存の業務DBは使用していません。

出典メモ

元ネタ:[採用原稿 260917_SQL_ON_WHERE_JOIN_トラブルから理解する.md](<C:/LWP/記事制作/01_記事ネタ/archive/260917_SQL_ON_WHERE_JOIN_トラブルから理解する.md>)。2026年10月4日に原稿全文を確認し、説明単位に沿って再構成しました。元ネタの原文は保全しています。人物・業務データは創作・説明用です。

図の文字・番号・参照・金額を目視確認し、SQLの例は隔離したSQLiteで実行しました。簿記例はこのモデルの計算と借貸一致を確認しています。実業務の帳簿や業務DBを検証した記事ではありません。

Copyright © 2026 LWP 山中 一弘

本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。