業務システム開発・保守の実務メモを発信中

指定したIDのうちDBに存在しないものをSQLで出す方法

ExcelやメールでID一覧を渡されて、「この顧客ID、登録されているか見ておいて」と言われることがあります。

存在するIDだけでよければ、IN (...) で確認できます。 ただ、確認作業で本当に欲しいのは、登録されていたIDではなく、渡された一覧のうちDBに存在しなかったID だったりします。

IN で検索すると、存在する行だけが返ります。 存在しなかったIDは、結果に出ません。 画面上では静かに消えるので、あとから「あれ、1003は見たんだっけ」となりがちです。まあまあ嫌なやつです。

指定一覧の不足を見たいときは、まず確認したいID一覧をSQL内の小さな表として作ります。その表をDBのテーブルと突き合わせると、存在しなかった値だけを結果として返せます。

この記事では、ダミーの customers テーブルを使います。 確認対象は 1001, 1002, 1003, 1004, 1005 の5件です。 このうち、テーブルに存在しないIDをSQLで出します。

目次

IN句で確認すると存在するIDだけが返る

ダミーの顧客テーブルを用意する

まず、確認先になる顧客テーブルを作ります。 ここではSQLiteで動かせる形にしていますが、考え方はOracleでも同じです。 業務システムなら、顧客、取引先、商品、部署、社員、請求先あたりのマスタ確認でよく出ます。

SQL
CREATE TABLE customers (
    customer_id INTEGER PRIMARY KEY,
    customer_name TEXT NOT NULL,
    status TEXT NOT NULL
);

INSERT INTO customers (customer_id, customer_name, status) VALUES
    (1001, 'Aoba Trading', 'active'),
    (1002, 'Kitamachi Works', 'active'),
    (1004, 'Nanko Logistics', 'inactive'),
    (1007, 'Chuo Foods', 'active');

投入後の中身は次の通りです。

Markdown
customer_id | customer_name     | status
1001        | Aoba Trading      | active
1002        | Kitamachi Works   | active
1004        | Nanko Logistics   | inactive
1007        | Chuo Foods        | active

この表には、1001100210041007 が入っています。 この時点で、人間の目では 10031005 が無いと分かります。 ただし、実務では一覧が50件、100件、500件になることがあります。 目で見て探すと、抜けも転記ミスも起きます。

tomo

ID一覧が短いと目で追えますが、件数が増えると急に危なくなります。確認作業では、存在したものより、存在しなかったものを機械的に返す形にした方が残しやすいです。

IN句で検索した結果を見る

確認したいID一覧を、次の5件だとします。

Markdown
1001
1002
1003
1004
1005

まずは、よく書く IN のSQLです。

SQL
SELECT
    customer_id,
    customer_name,
    status
FROM
    customers
WHERE
    customer_id IN (1001, 1002, 1003, 1004, 1005)
ORDER BY
    customer_id;

実行結果は次の通りです。

Markdown
customer_id | customer_name     | status
1001        | Aoba Trading      | active
1002        | Kitamachi Works   | active
1004        | Nanko Logistics   | inactive

100110021004 は返りました。 でも、10031005 は結果に出ません。 これはエラーではなく、IN の普通の動きです。 IN は、指定した値に一致したテーブル行を返します。 一致しなかった指定値そのものは、結果セットに残りません

ここで詰まりやすいのは、SQLの結果だけを見ると不足分が分からないことです。 確認対象5件に対して結果3件なので、2件足りないことは分かります。 ただし、どの2件が無いのかは、元の一覧と結果を見比べないと分かりません。

NOT INでは不足IDの確認にならない

NOT INが返すのはテーブル側の別データ

「INの逆ならNOT INでは」と考えたくなります。 これは自然です。 ただ、今回の目的では違う結果になります。

SQL
SELECT
    customer_id,
    customer_name
FROM
    customers
WHERE
    customer_id NOT IN (1001, 1002, 1003, 1004, 1005)
ORDER BY
    customer_id;

実行結果は次の通りです。

Markdown
customer_id | customer_name
1007        | Chuo Foods

1007 が返りました。 これは「確認対象の一覧に含まれていないが、customersテーブルには存在するID」です。 今回知りたい 10031005 ではありません。

NOT IN は、テーブルの中から「この一覧に該当しない行」を探します。 確認したい一覧の中から「テーブルに無い値」を探すわけではありません。 左側に置かれている対象が、customersテーブルだからです。

不足IDを出したい場面で NOT IN を使うと、テーブル側にある別のIDが返ります。今回のように「渡された一覧の中で無かったもの」を見たい場合は、確認対象の一覧を左側に置く必要があります。

欲しい結果を言葉で固定する

SQLを書く前に、欲しい結果を一文で固定します。 今回は次です。

Markdown
確認対象ID一覧のうち、customers.customer_id に存在しないIDを返す

この文で主語になっているのは、customers テーブルではありません。 主語は、渡された確認対象ID一覧です。 だから、SQLでも確認対象ID一覧を先に表として作り、その表を基準にして結合します。

ここを曖昧にすると、INNOT INEXISTSLEFT JOIN の名前だけを行き来することになります。 覚えるべきなのは構文名より、どちらの一覧を基準にして結果を返すか です。

tomo

迷ったら、「結果に残したい一覧はどちらか」を先に決めます。今回なら、残したいのは渡されたID一覧です。

ID一覧を小さな表として作る

VALUESで確認対象IDを表にする

SQLiteでは、WITHVALUES を使うと、確認対象ID一覧を一時的な表のように扱えます。 ここでは target_ids という名前にします。

SQL
WITH target_ids(customer_id) AS (
    VALUES (1001), (1002), (1003), (1004), (1005)
)
SELECT
    customer_id
FROM
    target_ids
ORDER BY
    customer_id;

このSQLの結果は次の通りです。

Markdown
customer_id
1001
1002
1003
1004
1005

まだ customers テーブルとは結合していません。 まず、確認したいID一覧をSQLの中に持ち込んだだけです。 ここで、後続のSQLの書きやすさが分かれます。 IN (...) の括弧内に書いた値は、検索条件としては使えても、結果としてそのまま返す基準にはしにくいからです。

target_ids という表にしておくと、各IDを1行として扱えます。 そのため、あとで customers と結合したとき、結合できなかった行も target_ids 側には残せます。 不足IDを出すための前処理は、確認対象を表にすることです

OracleではDUALやコレクションで一覧を作る

Oracleで同じ考え方を使うなら、古くから使える形は DUALUNION ALL です。

SQL
WITH target_ids AS (
    SELECT 1001 AS customer_id FROM dual
    UNION ALL SELECT 1002 FROM dual
    UNION ALL SELECT 1003 FROM dual
    UNION ALL SELECT 1004 FROM dual
    UNION ALL SELECT 1005 FROM dual
)
SELECT
    customer_id
FROM
    target_ids
ORDER BY
    customer_id;

数値IDだけなら、Oracleの組み込みコレクションを使って短く書けることもあります。

SQL
WITH target_ids AS (
    SELECT
        column_value AS customer_id
    FROM
        TABLE(sys.odcinumberlist(1001, 1002, 1003, 1004, 1005))
)
SELECT
    customer_id
FROM
    target_ids
ORDER BY
    customer_id;

どちらを使うかは、現場のOracleバージョン、権限、SQLを貼るツールの癖で変わります。 短い確認なら DUALUNION ALL は読まれやすいです。 ID数が少し多いなら、sys.odcinumberlist の方が書きやすい場面もあります。

OracleでもSQLiteでも、考え方は同じです。確認対象のID一覧を target_ids として作り、その一覧を基準にDBテーブルへ突き合わせます。

LEFT JOINで存在しないIDだけを出す

一覧を左側に置いてテーブルと結合する

不足IDを出す本命のSQLです。 ポイントは、target_ids を左側に置くことです。 左側に置いた行は、右側の customers に一致しなくても残ります。

SQL
WITH target_ids(customer_id) AS (
    VALUES (1001), (1002), (1003), (1004), (1005)
)
SELECT
    t.customer_id AS missing_customer_id
FROM
    target_ids t
LEFT JOIN customers c
    ON c.customer_id = t.customer_id
WHERE
    c.customer_id IS NULL
ORDER BY
    t.customer_id;

実行結果は次の通りです。

Markdown
missing_customer_id
1003
1005

これで、確認対象ID一覧のうち、customers に存在しなかったIDだけが返りました。 100110021004 は結合できたので除外されます。 10031005 は結合先が無いため、c.customer_idNULL になります。 その行だけを WHERE c.customer_id IS NULL で残しています。

全件の判定結果も出して確認する

不足IDだけを返す前に、存在したものと存在しなかったものを一緒に見るSQLも便利です。 問い合わせ前の確認や、作業メモに貼る用途ではこちらの方が説明しやすいことがあります。

SQL
WITH target_ids(customer_id) AS (
    VALUES (1001), (1002), (1003), (1004), (1005)
)
SELECT
    t.customer_id AS checked_customer_id,
    CASE
        WHEN c.customer_id IS NULL THEN 'missing'
        ELSE 'exists'
    END AS check_result,
    c.customer_name,
    c.status
FROM
    target_ids t
LEFT JOIN customers c
    ON c.customer_id = t.customer_id
ORDER BY
    t.customer_id;

実行結果は次の通りです。

Markdown
checked_customer_id | check_result | customer_name     | status
1001                | exists       | Aoba Trading      | active
1002                | exists       | Kitamachi Works   | active
1003                | missing      | NULL              | NULL
1004                | exists       | Nanko Logistics   | inactive
1005                | missing      | NULL              | NULL

この結果を見ると、10031005missing です。 同時に、1004 は存在していますが inactive です。 業務上は「存在しない」だけでなく、「存在するが無効」という別問題が混ざることもあります。 そのため、最初から不足IDだけを出すより、確認作業では全件判定を一度見た方が安全な場面があります。

作業メモに残すなら、不足IDだけの結果と全件判定の結果を分けると説明しやすいです。

tomo

不足IDだけを返すSQLは報告用に強いです。全件判定のSQLは、なぜその結果になったかを確認する途中経過として使いやすいです。

NOT EXISTSで同じ結果を確認する

相関サブクエリで存在しない行を判定する

NOT EXISTS でも同じ不足IDを返せます。 target_ids の各行について、customers に同じIDが存在するかを確認し、存在しなかった行だけを残します。

SQL
WITH target_ids(customer_id) AS (
    VALUES (1001), (1002), (1003), (1004), (1005)
)
SELECT
    t.customer_id AS missing_customer_id
FROM
    target_ids t
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            customers c
        WHERE
            c.customer_id = t.customer_id
    )
ORDER BY
    t.customer_id;

実行結果は次の通りです。

Markdown
missing_customer_id
1003
1005

LEFT JOIN 版と同じ結果です。 NOT EXISTS は、target_ids の1行ごとに「同じIDの顧客が存在するか」を確認します。 存在しなければ、その target_ids の行を返します。 読み方としても追いやすいです。

LEFT JOINとNOT EXISTSの使い分け

確認作業では、まず LEFT JOIN 版を使うことが多いです。 理由は、存在した行の名前や状態も一緒に出しやすいからです。 先ほどの全件判定のように、existsmissing を並べる形へ広げやすいです。

NOT EXISTS は、不足IDだけをきれいに返したいときに読みやすいです。 結合先の列を結果に出さないなら、SQLの意図もはっきりします。 ただし、条件が増えるとサブクエリの中に業務条件が入り、読み慣れていない人には少し追いにくくなります。

どちらが絶対に正しい、という話ではありません。 確認作業では、途中経過を説明したいならLEFT JOIN、不足分だけ返したいならNOT EXISTS くらいで選ぶと扱いやすいです。 実行性能を厳密に見る場面では、対象DBの実行計画と件数で確認します。

実務で使う前に見る確認ポイント

重複IDがあると件数の見え方が変わる

渡された一覧に同じIDが重複していることがあります。 たとえば、1003 が2回入っている場合、target_ids をそのまま作ると不足結果にも2行出ます。 これはSQLとしては自然です。 確認対象一覧に2行あるから、結果にも2行残ります。

報告で「不足IDは何種類か」を見たいなら、SELECT DISTINCT を使います。 一方で、元一覧の行数と突き合わせたいなら、重複を残したまま確認した方がよいです。 ここを決めずにSQLだけ直すと、件数が合ったり合わなかったりします。

SQL
WITH target_ids(customer_id) AS (
    VALUES (1001), (1002), (1003), (1003), (1005)
)
SELECT DISTINCT
    t.customer_id AS missing_customer_id
FROM
    target_ids t
LEFT JOIN customers c
    ON c.customer_id = t.customer_id
WHERE
    c.customer_id IS NULL
ORDER BY
    t.customer_id;

重複を消すか残すかは、SQLの好みではなく確認目的で決めます 取込前チェックなら重複も問題になることがあります。 問い合わせ回答なら、ユニークな不足IDだけで足りることもあります。

文字列IDは前後スペースとゼロ埋めを見る

顧客IDや商品コードが文字列の場合は、前後スペースとゼロ埋めを見ます。 00123123 は、文字列としては別の値です。 Excelから貼った一覧では、見た目では分からない空白が混ざることもあります。

確認では、いきなり TRIM やゼロ埋め変換を入れるより、まず生の値で不一致を見ます。 その後で、空白を除いたら一致するのか、ゼロ埋めしたら一致するのかを別SQLで確認します。 この順番にすると、「元データが悪い」のか「確認SQLが形式差を吸収しているだけ」なのかを分けられます。

SQL
WITH target_codes(code) AS (
    VALUES ('00123'), (' 00124'), ('125')
)
SELECT
    code,
    LENGTH(code) AS code_length
FROM
    target_codes;

文字列コードの不足確認では、LENGTH を一緒に出すだけでも、前後スペースに気づきやすくなります。 地味ですが、こういう確認の方が事故を減らします。

結果を問い合わせメモに残す

不足IDを返せたら、最後に確認条件を残します。 SQLだけを残すより、次の情報を一緒に書く方があとで読み返せます。

Markdown
確認日時: 2026-07-30 18:20
確認対象: 顧客ID 1001, 1002, 1003, 1004, 1005
確認先: customers.customer_id
確認結果: 1003, 1005 が存在しない
補足: 1004 は存在するが status = inactive

このメモがあると、問い合わせへの回答が短くなります。 「1003と1005は未登録です」だけでは、どの一覧を見たのか、どのテーブルで確認したのかが残りません。 保守作業では、後から同じ確認をやり直すことがよくあります。

不足IDのSQLは、書けること自体より、確認条件と結果を残せることが大事です。対象一覧、確認先テーブル、実行結果、補足条件をセットにすると、あとから同じ判断を再現できます。

今回のSQLで一番大事なのは、IN の逆を探すことではありません。 確認したいID一覧を target_ids として作り、その一覧を左側に置くことです。 そこまで決まれば、LEFT JOIN ... IS NULL でも NOT EXISTS でも不足IDは出せます。

業務の確認では、まず全件判定で existsmissing を並べます。 問題なければ、不足IDだけのSQLを報告用に使います。 この順番なら、SQLの結果を見た人にも、何が存在して何が存在しなかったのかを説明しやすくなります。

よかったらシェアしてね!
  • URLをコピーしました!
目次