ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 제외 목록에 NULL 하나 넣었더니 SQL 결과가 사라지는 이유
    Data Analysis 2026. 10. 5. 00:21
    728x90
    반응형

    1과 2의 비교는 참, 1과 NULL의 비교는 알 수 없음이며, AND 결과도 알 수 없어 WHERE에서 제외되는 흐름.
    NOT IN (2, NULL)을 후보 1에 적용한 비교 흐름입니다. PostgreSQL 문서의 규칙을 설명한 도식이며 실행 결과 화면은 아닙니다.

    NOT IN 조회가 갑자기 0행이 됐다면 제외 목록에 NULL이 들어왔는지 먼저 확인하세요. NOT EXISTS로 바꿀 때는 후보 ID의 NULL도 어떻게 처리할지 정해야 합니다.

    후보가 1, 2, 3이고 제외할 ID가 2라면 1, 3을 기대합니다. 그런데 제외 목록이 2, NULL이 되면 다음 쿼리는 아무 행도 남기지 않습니다.

    WITH candidates(id) AS (
      VALUES (1), (2), (3)
    ), excluded(id) AS (
      VALUES (2), (NULL::integer)
    )
    SELECT id
    FROM candidates
    WHERE id NOT IN (
      SELECT id FROM excluded
    )
    ORDER BY id;
    

    아래 결과는 PostgreSQL 18 문서의 규칙으로 도출한 예상입니다. 데이터베이스 실행 결과나 성능 측정은 아닙니다.

    NULL과 다르다는 것을 확정할 수 없어서입니다

    후보 1에 대한 조건을 펼치면 다음과 같습니다.

    1 NOT IN (2, NULL)
    -- 비교의 의미를 풀면:
    (1 <> 2) AND (1 <> NULL)
    

    1 <> 2는 참입니다. 하지만 1 <> NULL은 서로 다른 값인지 확정할 수 없습니다. PostgreSQL의 일반 비교에서 한쪽이 NULL이면 결과도 NULL, 즉 알 수 없음이 됩니다. 두 NULL끼리 =로 비교해도 참이 되지는 않습니다. 비교 연산과 NULL

    참인 조건과 알 수 없는 조건을 AND로 묶으면 전체 결과도 알 수 없음입니다. NOT IN은 목록에 같은 값이 없더라도 NULL이 섞여 있으면 참으로 판단하지 못합니다. NOT IN의 정의

    후보 ID 조건의 결과 WHERE 통과
    1 알 수 없음 제외
    2 거짓 제외
    3 알 수 없음 제외

    WHERE는 참인 행만 남깁니다. 그래서 세 후보가 모두 빠집니다. 2는 제외 목록에 실제로 있고, 1과 3은 목록의 모든 값과 다르다고 확정하지 못한 것입니다. WHERE의 행 선택 규칙

    진단할 때는 원본 테이블뿐 아니라 제외 목록을 만드는 서브쿼리의 결과를 확인해야 합니다. 원본 열에 NULL이 없어도 바깥 조인 등 중간 처리에서 NULL이 생길 수 있습니다. 최종 조회가 0행인지보다, 안쪽 조회가 어떤 값을 내보내는지가 먼저입니다.

    같은 ID가 있는지만 묻고 싶다면 NOT EXISTS

    원하는 규칙이 ‘제외 목록에 같은 ID가 있는 후보만 뺀다’라면 다음처럼 쓸 수 있습니다.

    WITH candidates(id) AS (
      VALUES (1), (2), (3)
    ), excluded(id) AS (
      VALUES (2), (NULL::integer)
    )
    SELECT c.id
    FROM candidates AS c
    WHERE NOT EXISTS (
      SELECT 1
      FROM excluded AS e
      WHERE e.id = c.id
    )
    ORDER BY c.id;
    

    안쪽 쿼리는 후보마다 같은 ID를 찾습니다. 2에는 일치하는 행이 있고, 1과 3에는 없습니다. NOT EXISTS는 안쪽 쿼리가 행을 하나도 반환하지 않을 때 참이므로, 예상 결과는 1, 3입니다. EXISTS의 정의

    여기서 제외 목록의 NULL은 어느 후보와도 등호 조건을 참으로 만들지 못합니다. 따라서 다른 ID의 통과 여부에 영향을 주지 않습니다. 이것이 앞의 NOT IN과 결과가 달라지는 이유입니다.

    후보 ID가 NULL인 경우도 결정해야 합니다

    위 쿼리의 후보에 (NULL::integer)를 추가하면 그 후보도 남습니다. e.id = c.id가 참인 행을 찾을 수 없기 때문입니다.

    ID가 없는 후보는 처리 대상에서 빼야 한다면, 바깥 조건에 IS NOT NULL을 추가합니다.

    WHERE c.id IS NOT NULL
      AND NOT EXISTS (
        SELECT 1
        FROM excluded AS e
        WHERE e.id = c.id
      )
    

    반대로 ‘제외 목록에 NULL이 있으면 후보의 NULL도 제외한다’는 규칙이라면, 안쪽 비교를 다음처럼 바꿀 수 있습니다.

    WHERE e.id
      IS NOT DISTINCT FROM c.id
    

    IS NOT DISTINCT FROM은 두 값이 모두 NULL일 때도 참입니다. 일반 등호와 달리 NULL끼리의 일치도 표현합니다. 제외 목록에 NULL이 없을 때는 NULL 후보가 남으므로, 앞의 c.id IS NOT NULL과 목적이 다릅니다. NULL을 포함해 비교하는 술어

    NULL을 거르는 방식은 빈 목록까지 확인합니다

    NOT IN을 유지하려면 안쪽에서 NULL을 걸러 내는 방법도 있습니다.

    WHERE c.id NOT IN (
      SELECT e.id
      FROM excluded AS e
      WHERE e.id IS NOT NULL
    )
    

    제외 목록의 NULL을 무시해도 되는 규칙이라면 쓸 수 있습니다. 다만 후보의 NULL까지 항상 제외되는 것은 아닙니다. 안쪽 조회가 0행이면 NOT IN은 참이 되며, 이때는 NULL 후보도 통과합니다. NULL 한 행이 있는 목록과 아예 빈 목록을 구별해야 합니다. 빈 서브쿼리의 NOT IN 결과

    따라서 ‘후보 ID가 없으면 무조건 제외’가 요구사항이라면 이 방식에서도 바깥의 c.id IS NOT NULL이 필요합니다.

    수정한 쿼리는 남는 ID로 비교하세요

    이번 예시에서 요구사항을 다음처럼 정했다고 해 보겠습니다.

    • 후보 ID가 NULL이면 제외한다.
    • 제외 목록의 NULL은 무시한다.
    • 알려진 ID가 일치하면 제외한다.

    이 규칙이면 후보를 1, 2, 3, NULL로 고정하고 다음 결과를 기대할 수 있습니다.

    제외 목록 남아야 할 ID
    2 1, 3
    2, NULL 1, 3
    NULL만 있음 1, 2, 3
    빈 목록 1, 2, 3
    2, 2 1, 3

    행 수만 비교하면 엉뚱한 두 행이 남은 경우를 놓칩니다. 수정 전후의 결과를 정렬해 어떤 ID가 남았는지 비교하세요. 위 표는 지금 정한 규칙의 기대값이므로, 실제 요구사항이 다르면 먼저 표부터 바꿔야 합니다.

    자료 수집 실패로 제외 목록이 비어 버린 경우라면 쿼리 수정과 별개로 입력 누락을 처리해야 합니다. SQL은 빈 목록을 정상 입력으로 평가합니다. 정상적으로 ‘제외할 대상 없음’인 상황과 수집 실패를 쿼리 결과만으로 구별해 주지는 않습니다.

    성능은 실제 실행 계획과 데이터로 따로 확인할 문제입니다. 여기서는 NOT EXISTS의 속도를 보장하지 않습니다. 먼저 양쪽 NULL과 빈 목록에서 남겨야 할 ID를 정하고, 그 집합을 반환하는 쿼리인지 확인하세요.


    자료 확인: 2026년 9월 27일, PostgreSQL 18 공식 문서. 단일 정수 ID 비교를 다루며 복합 행 비교는 범위에서 제외했습니다.

    728x90
    반응형
Designed by Tistory.