Search

REMINDER 02-06. NULL은 일반 값과 다르게 비교하자

상황: 주문 730,327건 중 픽업일시 pkup_dtm이 NULL인 20건을 찾는다. 이 예제는 NULL이 '픽업 전'을 의미한다는 업무 전제를 사용한다.

BAD SQL

EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT o.ord_no ,o.pkup_dtm FROM startdbpg.tr_ord o WHERE o.pkup_dtm = NULL;
SQL
복사
실측 실행계획 — 결과 0건
Result (actual time=0.001..0.001 rows=0 loops=1) One-Time Filter: false Planning: Buffers: shared hit=105 Planning Time: 0.574 ms Execution Time: 0.135 ms
Plain Text
복사
•
= NULL은 NULL을 찾는 올바른 비교 방법이 아니다.
•
일반 비교식의 결과는 TRUE가 아닌 UNKNOWN이므로, 실측에서는 One-Time Filter: false로 테이블을 읽지 않고 0건을 반환했다.

BAD SQL 추가 — COALESCE로 NULL을 치환해 비교

SELECT o.ord_no ,o.pkup_dtm FROM startdbpg.tr_ord o WHERE COALESCE(o.pkup_dtm, TIMESTAMP '1900-01-01') = TIMESTAMP '1900-01-01';
SQL
복사
•
COALESCE는 pkup_dtm이 NULL이면 지정한 날짜로 바꾼다. 하지만 NULL 여부만 검사하는 데 불필요한 컬럼 가공이 추가된다.
•
실제로 1900-01-01 값이 저장된 행까지 조회할 수 있어 IS NULL과 결과가 달라질 위험이 있다.
•
pkup_dtm에 일반 B-tree 인덱스가 있더라도 이런 표현식은 인덱스 조건으로 직접 활용하기 어려울 수 있다(표현식 인덱스 등은 예외).
•
이 SQL은 추가 설명용 예제이며 실행계획과 성능은 별도로 측정하지 않았다.

GOOD SQL

EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT o.ord_no ,o.pkup_dtm FROM startdbpg.tr_ord o WHERE o.pkup_dtm IS NULL;
SQL
복사
실측 실행계획 — 결과 20건
Gather (actual time=473.025..551.952 rows=20 loops=1) Workers Planned: 1 Workers Launched: 1 Buffers: shared hit=15630 read=1686 -> Parallel Seq Scan on tr_ord o (actual time=461.138..461.141 rows=10 loops=2) Filter: (pkup_dtm IS NULL) Rows Removed by Filter: 365154 Buffers: shared hit=15630 read=1686 Planning: Buffers: shared hit=112 dirtied=1 Planning Time: 9.445 ms Execution Time: 552.673 ms
Plain Text
복사
•
IS NULL은 NULL인 주문 20건을 정상 조회한다.
•
이번에는 Parallel Seq Scan이 실행되어 BAD보다 오래 걸렸지만, 정확한 결과를 반환하는 것이 우선이다.

핵심

•
NULL 여부는 = NULL이 아니라 IS NULL 또는 IS NOT NULL로 비교하자.
•
빠르게 끝난 잘못된 SQL보다 업무상 정확한 데이터를 반환하는 SQL이 중요하다.
💡
참고
•
IS DISTINCT FROM과 IS NOT DISTINCT FROM은 NULL을 포함해 비교할 때 사용할 수 있다.
•
NULL이 업무적으로 무엇을 뜻하는지는 별도 확인이 필요하다. 이 예제에서는 '픽업 전'이라고 가정한다.
•
BAD 0.135ms, GOOD 552.673ms는 원문 실측값이다. BAD가 빠른 이유는 올바른 조회를 수행하지 않았기 때문이지 성능이 우수해서가 아니다.