---
title: "一条 NULL 为什么让 NOT IN 查不到任何行"
date: 2026-11-13
slug: sql-not-in-null-anti-join
canonical: https://kernelranch.com/blog/2026/11/sql-not-in-null-anti-join
markdown_url: https://kernelranch.com/blog/2026/11/sql-not-in-null-anti-join.md
tags: ["sql", "sqlite", "null"]
---一份排除名单混入 `NULL` 后，`WHERE id NOT IN (...)` 可能过滤掉原本要保留的所有行。对未匹配的 ID，SQL 无法断言它与未知值不同，于是得到未知。查找没有匹配记录的行时，可以用相关子查询 `NOT EXISTS` 表达意图，并单独决定左侧空值是否保留。

## 1. 本机复现

保存为 `null_lab.py`，运行 `python null_lab.py`。左侧有三个客户和一条没有客户 ID 的记录，右侧有一条明确排除的记录和一个空值。

```python
from contextlib import closing
import sqlite3

with closing(sqlite3.connect(":memory:")) as db:
    db.executescript("""
        CREATE TABLE incoming (customer_id INTEGER);
        CREATE TABLE blocked (customer_id INTEGER);
        INSERT INTO incoming VALUES (1), (2), (3), (NULL);
        INSERT INTO blocked VALUES (2), (NULL);
    """)

    result = db.execute("""
        SELECT customer_id, customer_id NOT IN (SELECT customer_id FROM blocked)
        FROM incoming ORDER BY customer_id
    """).fetchall()
    assert result == [(None, None), (1, None), (2, 0), (3, None)]
    print("NOT IN as a value:", result)

    filters = {
        "NOT IN": "i.customer_id NOT IN (SELECT customer_id FROM blocked)",
        "NOT EXISTS": """NOT EXISTS (
            SELECT 1 FROM blocked b WHERE b.customer_id = i.customer_id
        )""",
        "NOT EXISTS, ID required": """i.customer_id IS NOT NULL AND NOT EXISTS (
            SELECT 1 FROM blocked b WHERE b.customer_id = i.customer_id
        )""",
    }
    expected = [[], [None, 1, 3], [1, 3]]
    for (label, condition), want in zip(filters.items(), expected):
        rows = db.execute(f"""
            SELECT i.customer_id FROM incoming i
            WHERE {condition} ORDER BY i.customer_id
        """).fetchall()
        ids = [row[0] for row in rows]
        assert ids == want
        print(label + ":", ids)
```

本地实测输出如下。

```text
NOT IN as a value: [(None, None), (1, None), (2, 0), (3, None)]
NOT IN: []
NOT EXISTS: [None, 1, 3]
NOT EXISTS, ID required: [1, 3]
```

## 2. 先定义两侧 NULL 的业务含义

SQLite 查询结果中的 `None` 对应 SQL `NULL`。对 ID 1 来说，名单里的 2 能排除，但比较名单里的 `NULL` 得不到真或假。`WHERE` 只保留条件为真的行，所以它与命中名单的 ID 2 一起消失。`NOT EXISTS` 检查子查询有没有返回匹配行。左侧缺少 ID 时，等值条件同样找不到匹配，因此该行会保留，除非显式要求左侧 ID 存在。

如果右侧空值表示无效数据，导入时就应验证或通过 `NOT NULL` 约束拒绝。保留 `NOT IN` 写法时，可以在右侧子查询加 `IS NOT NULL`，同时为左侧空值明确规则。验收至少包括右侧含空值、右侧为空集、左侧为空值以及实际命中四类样本。这里的结论针对筛选语义，性能需用实际索引和执行计划判断。

## 小结

先把过滤表达式作为一列查询，确认哪条记录变成了未知。按排除规则选择相关子查询和空值约束。

## 数据来源与检索时间

检索时间 2026-11-13。

| 来源 | URL | 对应内容 |
|---|---|---|
| SQLite SQL Language Expressions | https://www.sqlite.org/lang_expr.html#the_in_and_not_in_operators | NOT IN 真值表与 EXISTS 语义 |
| PostgreSQL 18 Subquery Expressions | https://www.postgresql.org/docs/18/functions-subquery.html#FUNCTIONS-SUBQUERY-NOTIN | NOT IN 的空值与子查询规则 |

## 核验说明

- Windows、Python 3.11.9、SQLite 3.45.1 验证三种过滤方式及返回行。PostgreSQL 部分依据对应版本文档。
