LEFT JOINを使ったのに、右側に一致するデータがない行が消える場合は、結合条件だけでなく、結合後のWHEREを確認します。右側の値へ条件を付けると、NULLで補われた行が残らず、期待した一覧にならないことがあります。
ここではPostgreSQLのSELECTを例に、「全顧客を残し、支払済み注文があれば付ける」要件を確認します。入力は架空の3顧客・3注文です。説明用の期待結果を整理したもので、PostgreSQLやAIへ実行して得た出力ではありません。AIにはSQLの修正案と、どの行が残るかを説明させます。
目次
最初に、何を一行として残したいか決める
「顧客と注文を結合する」だけでは、欲しい結果が決まりません。すべての顧客を残すのか、支払済み注文のある顧客だけなのかを区別します。また、顧客に注文が複数あるとき、注文ごとに複数行を返すのか、顧客ごとに集計するのかも決めてください。
この例のcustomersはcustomer_idが一意の顧客表、ordersはorder_idが一意かつNULLでない注文表です。注文表のcustomer_idで顧客に対応させ、statusがpaidなら支払済みとして扱います。実際の業務で状態名や対象条件が違う場合は、その定義へ置き換えます。
表は横にスクロールできます
| customer_id | 顧客 |
|---|---|
| 1 | A社 |
| 2 | B社 |
| 3 | C社 |
表は横にスクロールできます
| order_id | customer_id | status |
|---|---|---|
| 101 | 1 | paid |
| 102 | 1 | cancelled |
| 103 | 2 | cancelled |
B社には注文がありますが、支払済み注文はありません。C社には注文自体がありません。この違いが、修正案を確認するときの要点です。
WHEREの条件で、結合後の行を落としていないか見る
次のSQLは、顧客IDでLEFT JOINしたあとに、支払済みの行へ絞ります。
SELECT c.customer_id, o.order_id, o.status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.status = 'paid'
ORDER BY c.customer_id, o.order_id;
結合した段階では、A社に101・102、B社に103が対応し、C社には右側をNULLにした行が付きます。その後のWHEREでは、101だけが条件に合います。C社のNULLはpaidに等しいとは判定されないため、この結果には残りません。
PostgreSQLの公式資料は、LEFT OUTER JOINで右側に一致しない左の行をNULL付きで残すことと、ONの制約とWHEREの制約では結果が変わる例を示しています。「LEFT JOINだから、後の条件にかかわらず左の全行が最後まで残る」とは考えないでください。PostgreSQL公式:結合と条件(新しいタブで開く)
支払済み注文のある行だけが欲しいなら、この絞り込みが要件に合う場合もあります。行が少ないという理由だけで条件を移さず、まず欲しい結果へ戻ります。
全顧客を残す要件なら、右側の対象条件をONへ置く
今回の要件は「全顧客を残し、支払済み注文があれば付ける」です。その場合の修正案は、支払済みの条件を結合条件へ含める形です。
SELECT c.customer_id, o.order_id, o.status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
AND o.status = 'paid'
ORDER BY c.customer_id, o.order_id;
A社は注文101と一致します。B社の注文103はpaidではないため今回の結合には一致せず、B社には右側がNULLの行が付きます。C社にも一致する注文がないため、右側がNULLの行が付きます。
表は横にスクロールできます
| customer_id | order_id | status |
|---|---|---|
| 1 | 101 | paid |
| 2 | NULL | NULL |
| 3 | NULL | NULL |
このNULLは「この条件に合う注文を結合できなかった」ことを表します。「B社には注文が一件も存在しない」とは言えません。元の入力にはcancelledの注文103があります。
すべてのWHERE条件をONへ移すわけでもありません。例えば対象の顧客自体を限定する条件と、付けたい注文を限定する条件は役割が違います。要件に沿って、どの表のどの集合を絞るのかを説明してください。
ORでNULLを残す修正は、同じ結果とは限らない
AIが次のような修正を提案する場合も、B社が残るかを確かめます。
SELECT c.customer_id, o.order_id, o.status
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.customer_id
WHERE o.status = 'paid' OR o.order_id IS NULL
ORDER BY c.customer_id, o.order_id;
C社には結合後にorder_idがNULLの行があるため残ります。一方、B社には注文103が結合済みで、order_idはNULLではありません。statusもpaidではないため、B社は落ちます。この例ではA社とC社の2行で、全顧客を残す要件には合いません。
「右側に一件もない」と「右側にはあるが、指定条件に合うものがない」を別の確認例にすると、この差を見つけられます。NULLを残す一節を追加しただけで、ONへ条件を置いたSQLと同じ意味になるとは扱わないでください。
行数が増える場合は、顧客数と注文数を分ける
A社へもう一つ支払済み注文104を追加したとします。ONにpaid条件を入れたSQLは、A社に101と104の2行、B社とC社に各1行で、計4行になります。LEFT JOINは、左側一行につき必ず結果も一行にする機能ではありません。
顧客ごとに一行が必要なら、注文をどうまとめるかを別に決めます。最新一件、支払済み注文数、合計金額などでは異なるSQLになります。行数を減らすためだけにDISTINCTを加えると、必要な注文の違いを隠す可能性があります。
件数を数える場合も対象を明示します。公式の集計関数の説明では、COUNT()は入力行数、COUNT(式)は式がNULLでない入力行数を数えます。今回のON条件の結果を顧客ごとに数えるなら、B社のCOUNT()はNULL付きの一行を数えて1、COUNT(o.order_id)は0です。これは注文IDがNULLでないという入力条件を前提にしています。PostgreSQL公式:集計関数(新しいタブで開く)
AIへは、残す行と失敗例を一緒に渡す
PostgreSQLのSELECT修正案を作ってください。DBへの接続や実行、データ変更はしないでください。
要件:全顧客を残し、paidの注文があれば付ける。複数あれば注文ごとに返す。
テーブル:customers(customer_id一意)、orders(order_id一意かつNULL不可、customer_id、status)。
現在のSQL:[SQL]。
架空入力と期待結果:[表]。
右側に注文がない顧客、cancelledだけの顧客、paidが複数の顧客を分けて説明してください。
ONとWHEREの条件が、それぞれどの行へ影響するか示してください。
OR order_id IS NULLを加える案と同じ結果になるか、反例を確認してください。
COUNT(*)、COUNT(order_id)、異なる顧客数を混同しないでください。
出力:修正SQL/期待する行ID/件数/前提/未確認事項。
実際の状態名、NULL可否、結合キーが不明なら、推測せず確認事項へ残してください。
出力後は、SQLの文章説明だけでなく、期待する顧客IDと注文IDの組を比べます。全顧客が残ったか、条件外の注文が混ざらないか、複数一致が消されていないかを確認します。SQLが実行できることと、要件の行集合になっていることは別です。
実際の環境で試す段階では、許可された読み取り用の検証環境と対象データを使います。今回の机上例だけでは、実際のスキーマ、型、権限、実行計画、性能を確認していません。大きな表での処理量や、別のSQL方言へそのまま使えるかも別に検証します。
SQLを生成する環境の選び方は、SQLを書くAIの選び方を参考にしてください。結合結果をアプリで利用するなら、SQLの行集合が正しくなった後に、APIや画面でも期待する件数が扱われているかを確かめます。
参考資料
公式資料の確認日:2026年10月20日。確認時のTable ExpressionsはPostgreSQL 18の資料です。表と期待結果は架空例です。PostgreSQLでのSQL構文検査・実行、AI出力、実データ、性能、API・画面は実測していません。
