기출노트 로고기출노트
시작하기

SQL NULL을 등호로 찾으면 행이 나오지 않습니다

SELECT name FROM contacts WHERE phone = NULL;

전화번호가 없는 행을 찾으려고 쓴 쿼리인데 결과가 나오지 않습니다. SQL의 일반 비교는 입력에 NULL이 있으면 참·거짓 대신 unknown을 반환할 수 있어요. NULL = NULL도 참이라고 비교하지 않습니다.1

NULL 여부를 찾는 조건은 phone IS NULL입니다. 결과 행이 없었다는 사실을 곧바로 ‘전화번호가 없는 사람이 없다’고 해석하면 안 돼요. 무엇을 검사한 쿼리인지가 먼저 달랐습니다.

아래 코드는 PostgreSQL에서 실행할 작은 임시 테이블입니다.

CREATE TEMP TABLE contacts (
  name text,
  phone text
);

INSERT INTO contacts VALUES
  ('가', '010-example'),
  ('나', NULL),
  ('다', '');

화면의 빈칸을 저장값과 구별합니다

이 자료에서 가는 일반 문자열, 나는 NULL, 다는 빈 문자열을 가집니다. PostgreSQL에서는 뒤의 두 값이 같지 않아요.

SELECT name FROM contacts WHERE phone IS NULL;
-- 나

SELECT name FROM contacts WHERE phone = '';
-- 다

SELECT name FROM contacts WHERE phone IS NOT NULL;
-- 가, 다

다의 빈 문자열도 NULL이 아닌 값입니다. 따라서 IS NOT NULL 조건을 통과해요. 빈칸처럼 보이는 화면이 실제 저장값의 차이까지 표시해주는 것은 아닙니다.

이 차이를 모든 DBMS에 그대로 옮길 수도 없습니다. Oracle Database의 SQL 문서는 길이가 영인 문자열을 현재 NULL로 취급한다고 설명합니다.2 SQLD나 데이터베이스 문제에서 사용하는 DBMS·전제 조건이 제시되어 있다면 그 규칙을 적용해야 해요. 이 글의 실행 코드는 PostgreSQL 기준입니다.

COUNT는 ‘연락 가능’을 검사하지 않아요

SELECT COUNT(*) AS row_count,
       COUNT(phone) AS phone_count
FROM contacts;
-- row_count = 3, phone_count = 2

COUNT(*)는 입력 행을 세고, COUNT(phone)은 phone 값이 NULL이 아닌 행을 셉니다.3 빈 문자열인 다도 뒤의 집계에 포함됩니다.

따라서 phone_count를 ‘연락 가능한 사람 수’라고 이름 붙이면 쿼리가 확인하지 않은 의미를 더하게 됩니다. 전화번호의 형식이 맞는지, 실제 연락이 가능한지 검사한 적은 없으니까요. 값이 존재하는 행 수와 그 값이 유효한 행 수는 다를 수 있습니다.

NULL과 빈 문자열이 함께 있는 PostgreSQL 테이블 및 두 COUNT의 결과

COUNT(*) − COUNT(phone)은 이 입력에서 1입니다. 같은 행 집합을 대상으로 비교했으므로 NULL인 phone의 개수를 알 수 있어요. 각각에 다른 WHERE를 붙이면 처음부터 다른 행을 세는 것이므로 차이를 같은 뜻으로 읽지 않습니다.

NOT을 붙여도 unknown은 참이 되지 않습니다

다음 쿼리는 가의 문자열과 같지 않은 값을 찾습니다.

SELECT name FROM contacts
WHERE NOT (phone = '010-example');
-- 다

나가 돌아오지 않았어요. NULL과의 비교는 unknown이고, NOT을 적용해도 unknown으로 남기 때문입니다. WHERE는 조건이 참인 행을 남깁니다.1

NULL도 포함하려는 목적이면 그 조건을 분명히 적을 수 있습니다.

SELECT name FROM contacts
WHERE phone <> '010-example' OR phone IS NULL;
-- 나, 다

이 결과는 ‘다른 문자열인 값’뿐 아니라 ‘값을 알 수 없는 행’도 요청한 것입니다. 부정만 붙인 쿼리와 같지 않았어요.

NOT IN 목록에 NULL이 섞인 경우도 비슷한 이유로 읽습니다.

SELECT 2 NOT IN (1, NULL);
-- NULL, 즉 unknown

2가 1과 다르다는 비교는 참이지만, NULL과도 다르다는 비교를 참으로 확정할 수 없습니다. 목록에 같다고 확인된 값이 없다는 이유만으로 NOT IN 전체가 참이 되는 것은 아니에요. PostgreSQL 문서도 같은 값이 없고 오른쪽에 NULL이 있으면 NOT IN이 NULL을 반환할 수 있다고 명시합니다.4

마지막 예제는 숫자 2가 실제 테이블에 없다는 보고가 아니라 비교식을 계산한 것입니다. 일반 비교, NULL 검사, 집계가 각각 무엇을 반환하는지 구별하면 행이 빠진 이유도 좁혀집니다.

위 코드의 결과 이름을 적은 순서는 설명을 위한 표기예요. SELECT에 ORDER BY가 없다면 출력 순서는 보장되지 않습니다.5 행 개수를 묻는 보기와 표시 순서를 묻는 보기를 같은 방식으로 추측하지 않아야 합니다.

Footnotes

  1. PostgreSQL 「Comparison Functions and Operators」의 일반 비교에 NULL이 들어올 때 unknown이 되는 설명과 IS NULL 항목, 「Logical Operators」의 NOT/NULL 표를 확인했다. 비교, 논리 연산 ↩ ↩2

  2. Oracle Database 19c SQL Language Reference 「Nulls」에서 길이가 영인 문자열을 NULL로 취급하는 설명을 확인했다. 문서는 향후 처리 변경 가능성도 밝혀 이를 모든 SQL 구현의 영구 규칙으로 쓰지 않았다. Oracle NULL ↩

  3. PostgreSQL 「Aggregate Functions」의 count(*)와 count(expression) 항목에서 입력 행과 NULL이 아닌 입력값을 세는 규칙을 확인했다. COUNT ↩

  4. PostgreSQL 「Subquery Expressions」의 NOT IN 항목에서 왼쪽 NULL 또는 오른쪽 NULL이 포함될 때의 결과를 확인했다. NOT IN ↩

  5. PostgreSQL 「Sorting Rows」의 정렬을 선택하지 않으면 행 반환 순서가 지정되지 않는다는 설명을 확인했다. 행 정렬 ↩