LabHub

SQL 실전 · 조회와 필터 · 이론

조회와 필터 — 세 값 논리와 조건의 순서

LabHub 에서 이어서 보기

한 줄 요약

SQL 의 WHERE 는 참인 행만 통과시키고, NULL 이 섞이면 결과가 참도 거짓도 아닌 미지가 되므로, 조건을 쓸 때는 늘 세 번째 값을 염두에 둬야 한다.

왜 이게 필요했나

대부분의 프로그래밍 언어는 참과 거짓 두 값만 다룬다. SQL 은 세 값을 다룬다. 참, 거짓, 그리고 미지다. NULL 이 관여하는 모든 비교는 미지가 되고, WHERE 는 참인 행만 통과시키므로 미지는 거짓처럼 취급된다.

이 한 가지 차이가 실무에서 조용한 버그를 만든다. 조건을 뒤집었는데 행 개수의 합이 전체와 맞지 않는 현상이 대표적이다.

SELECT count(*) FROM customers WHERE city = '서울';       -- 48SELECT count(*) FROM customers WHERE city <> '서울';      -- 335-- 합이 383 인데 전체는 400 이다. 나머지 17 은 city 가 NULL 인 행이다.

<> 로도 걸러지지 않으므로 NULL 행은 양쪽 어디에도 없다. 이때 필요한 것이 IS DISTINCT FROM 이다. 이 연산자는 NULL 을 하나의 값처럼 비교해 주므로 위 예에서 352를 돌려준다.

어떻게 동작하나

자주 쓰는 조건들의 성격을 정리하면 이렇다.

| 조건 | 의미 | 주의할 점 |
| --- | --- | --- |
| BETWEEN a AND b | a 이상 b 이하 | 양 끝값을 포함한다. 날짜 범위에 쓰면 마지막 날의 자정 이후가 빠질 수 있다 |
| IN (...) | 목록 중 하나 | 목록에 NULL 이 있고 NOT IN 을 쓰면 결과가 통째로 사라진다 |
| LIKE 'kim%' | 접두사 일치 | 인덱스 탐색 가능 |
| LIKE '%kim%' | 부분 일치 | 시작점을 모르므로 일반 인덱스로는 탐색 불가 |
| IS NULL | 값이 없음 | = NULL 은 언제나 0행 |
| COALESCE(a, b) | a 가 NULL 이면 b | 표시용 치환에 쓰고, 조건에 쓰면 인덱스를 못 탄다 |

CASE 는 위에서부터 처음 참인 가지를 고른다. 그래서 경계가 겹치는 조건을 나열할 때 순서가 곧 우선순위가 된다. 금액 구간을 나눌 때 큰 값부터 쓰는 것이 관례인 이유다.

페이지네이션도 짚고 갈 필요가 있다. ORDER BY 없이 LIMIT 만 쓰면 어떤 행이 나올지 보장되지 않는다. 그리고 정렬 기준에 동점이 있으면 페이지 사이에서 행이 중복되거나 누락된다. 정렬은 항상 유일한 값까지 포함해 tie-break 를 만들어야 한다. 실무에서는 ORDER BY created_at DESC, id DESC 처럼 마지막에 기본 키를 붙인다.

현장에서 만나는 모습

OFFSET 방식의 페이지네이션은 뒷 페이지로 갈수록 느려진다. OFFSET 10000 은 만 행을 읽고 버리는 일이기 때문이다. 대안은 마지막으로 본 값을 기준으로 이어 읽는 커서 방식이다. 조건이 WHERE (created_at, id) < (마지막값, 마지막id) 형태가 되어 인덱스 탐색으로 곧바로 시작 지점을 찾는다.

다음 실습에서 할 것

실제 이커머스 스키마에서 BETWEEN, IN, LIKE, COALESCE, CASE, IS DISTINCT FROM 을 차례로 써 본다. 특히 NULL 이 섞인 컬럼에서 조건을 뒤집었을 때 행 수가 맞지 않는 경험을 직접 하게 된다.