SQL WHERE 조건절 사용법 - 비교 연산자, BETWEEN, IN, LIKE, NULL (SQL 초급 2단원)
이 단원에서 배우는 것
1단원에서 SELECT ... FROM 으로 테이블 전체를 읽어 왔고, 처리 순서에서 WHERE 가 SELECT 보다 먼저 온다는 것을 봤다. 이번 단원은 그 WHERE 를 채운다. 사원 10명짜리 실습 테이블에서는 전체 조회가 아무 문제 없지만 실무 테이블은 수백만 행이다. 필요한 행만 남기는 일이 조회 성능의 대부분을 결정한다. 테이블은 1단원에서 만든 emp·dept 를 그대로 쓴다.
- 비교·논리 연산자와
BETWEEN·IN·LIKE를 상황에 맞게 골라 쓴다. - NULL 비교가 왜
=로 안 되는지를 3값 논리로 설명한다. - 조건식을 어떻게 쓰면 인덱스가 죽는지 알고 피한다.
개념
WHERE 를 안 쓰고 전체를 읽어다가 애플리케이션에서 if 문으로 거르면 안 되느냐고 묻는 사람이 실제로 있다. 안 된다. 100만 행을 읽어 네트워크로 실어 나른 다음 999,990 행을 버리는 일이기 때문이다. 인덱스를 타면 디스크에서 10행만 꺼내고 끝난다. WHERE 는 결과를 다듬는 기능이 아니라 읽을 양을 줄이는 장치다.
그리고 WHERE 를 이해하는 핵심은 SQL 이 참·거짓 두 값이 아니라 참·거짓·UNKNOWN 세 값으로 판정한다는 점이다. NULL 이 낀 비교는 거짓이 아니라 UNKNOWN 이 된다. WHERE 는 판정 결과가 참인 행만 남긴다. 거짓도 UNKNOWN 도 다 버린다. 이 한 줄이 아래 함정 절 전체의 뿌리다.
표준 SQL 문법과 예제
비교 연산자와 논리 연산자
| 연산자 | 의미 | 예시 |
|---|---|---|
= | 같다 | WHERE dept_id = 20 |
<> 또는 != | 다르다. <> 가 표준이다 | WHERE job <> '팀장' |
> >= < <= | 크다 / 작다 | WHERE salary >= 5000 |
BETWEEN a AND b | a 이상 b 이하. 양 끝을 포함한다 | WHERE salary BETWEEN 4000 AND 5000 |
IN (...) | 목록 중 하나와 같다 | WHERE dept_id IN (10, 30) |
LIKE | 패턴 일치. % 는 0자 이상, _ 는 정확히 1자 | WHERE emp_name LIKE '김%' |
IS NULL / IS NOT NULL | NULL 여부. NULL 판정은 이것뿐이다 | WHERE bonus IS NULL |
AND OR NOT | 결합. AND 가 OR 보다 먼저 묶인다 | WHERE a = 1 AND (b = 2 OR b = 3) |
-- 개발팀에서 연봉 5000 이상인 사원
SELECT emp_id, emp_name, salary
FROM emp
WHERE dept_id = 20
AND salary >= 5000;
-- 2019년에 입사한 사원. 날짜도 대소 비교가 된다
SELECT emp_name, hire_date
FROM emp
WHERE hire_date >= DATE '2019-01-01'
AND hire_date < DATE '2020-01-01';
-- 이름이 '신'으로 끝나는 사원
SELECT emp_name
FROM emp
WHERE emp_name LIKE '%신';
-- 보너스를 받지 못한 사원
SELECT emp_name, bonus
FROM emp
WHERE bonus IS NULL;
AND 와 OR 를 섞을 때는 괄호를 친다
-- 의도: (인사팀 또는 영업팀) 이면서 연봉 4000 이상
-- 아래는 그 의도가 아니다. AND 가 먼저 묶여서
-- '인사팀 전원' + '영업팀이면서 4000 이상' 이 된다
WHERE dept_id = 10 OR dept_id = 30 AND salary >= 4000
-- 올바른 표현
WHERE (dept_id = 10 OR dept_id = 30) AND salary >= 4000
-- 또는
WHERE dept_id IN (10, 30) AND salary >= 4000
같은 컬럼에 대한 OR 나열은 IN 으로 바꾸는 편이 짧고, 괄호 실수를 원천적으로 막는다.
패턴 안에 % 나 _ 가 들어갈 때
-- '100%' 라는 문자열을 찾고 싶다. % 는 와일드카드라 그냥 쓰면 안 된다
WHERE memo LIKE '100!%' ESCAPE '!'
ESCAPE 로 탈출 문자를 지정하면 그 다음 한 글자는 와일드카드가 아니라 일반 문자로 취급된다. 사용자 입력을 그대로 LIKE 에 넣어 검색할 때는 입력에 든 % 와 _ 를 이렇게 처리해야 결과가 엉키지 않는다.
DB별 차이
| 항목 | MySQL / MariaDB | PostgreSQL | Oracle | SQL Server |
|---|---|---|---|---|
| LIKE 대소문자 | 콜레이션을 따름. 기본 _ci 라 구분하지 않음 | 구분함. 무시하려면 ILIKE | 구분함 | 콜레이션을 따름. 기본은 구분하지 않음 |
빈 문자열 '' | 길이 0인 문자열 | 길이 0인 문자열 | NULL 로 취급 | 길이 0인 문자열 |
| NULL 안전 비교 | <=> 뿐. 표준 IS NOT DISTINCT FROM 은 MySQL·MariaDB 모두 없다 | IS NOT DISTINCT FROM | 없음. NVL·DECODE 로 우회 | 2022 부터 IS NOT DISTINCT FROM |
| 정규식 비교 | REGEXP / RLIKE | ~, ~* | REGEXP_LIKE() | 2025 부터 REGEXP_LIKE()(호환성 수준 170 이상). 그 이전 버전은 없음 |
Oracle 의 빈 문자열 규칙은 이식할 때 반드시 확인한다. Oracle 에서 WHERE location = '' 은 아무것도 찾지 못한다. WHERE location IS NULL 이라고 써야 한다.
실무에서 자주 틀리는 것
1. <> 조건이 NULL 행을 통째로 빠뜨린다
-- 보너스가 300이 아닌 사원. 실행하면 4행만 나온다
SELECT emp_name, bonus FROM emp WHERE bonus <> 300;
bonus 가 NULL 인 사원 5명이 결과에서 빠진다. NULL <> 300 은 거짓이 아니라 UNKNOWN 이고, WHERE 는 참인 행만 남기기 때문이다. 사람이 말하는 "300이 아닌 사원"에는 보통 보너스가 아예 없는 사원도 포함된다. 그 의도라면 NULL 을 명시적으로 살려야 한다.
SELECT emp_name, bonus
FROM emp
WHERE bonus <> 300 OR bonus IS NULL;
같은 이유로 WHERE bonus = NULL 은 오류가 아니라 항상 0행이다. 오류로 터져 주면 차라리 나은데 조용히 빈 결과를 내므로 발견이 늦다. 더 사나운 형태는 NOT IN 이다. 서브쿼리 결과에 NULL 이 한 건만 섞여도 전체가 0행이 된다.
-- emp.dept_id 에 NULL 이 있으므로 이 조회는 항상 0행이다
SELECT dept_name FROM dept
WHERE dept_id NOT IN (SELECT dept_id FROM emp);
-- 해결 1: 서브쿼리에서 NULL 을 제거한다
SELECT dept_name FROM dept
WHERE dept_id NOT IN (SELECT dept_id FROM emp WHERE dept_id IS NOT NULL);
-- 해결 2: NOT EXISTS 를 쓴다. NULL 의 영향을 받지 않는다
SELECT d.dept_name FROM dept d
WHERE NOT EXISTS (SELECT 1 FROM emp e WHERE e.dept_id = d.dept_id);
두 해결책 모두 '총무팀' 한 행을 돌려준다. 사원이 없는 부서를 찾는 것이 원래 의도였다.
2. 컬럼을 함수로 감싸서 인덱스를 죽인다
인덱스는 컬럼에 저장된 원래 값 순서로 정렬돼 있다. 컬럼을 함수에 넣거나 연산하면 그 정렬이 쓸모없어져서 전체를 훑는다.
-- 나쁨: hire_date 인덱스를 쓰지 못한다
WHERE YEAR(hire_date) = 2019
WHERE SUBSTRING(emp_name, 1, 1) = '김'
WHERE emp_id + 0 = 1001
-- 좋음: 범위 조건이나 접두 LIKE 로 바꾼다
WHERE hire_date >= DATE '2019-01-01' AND hire_date < DATE '2020-01-01'
WHERE emp_name LIKE '김%'
WHERE emp_id = 1001
LIKE 도 '김%' 처럼 앞이 고정돼야 인덱스를 쓴다. '%신' 이나 '%길동%' 은 시작점을 잡을 수 없어 전체 스캔이다. 뒤에서 찾는 검색이 꼭 필요하면 전문 검색 인덱스를 따로 만든다.
3. 타입이 다른 값을 비교한다
사원번호가 VARCHAR 인데 WHERE emp_no = 1001 처럼 숫자로 비교하면 MySQL 은 컬럼 쪽을 숫자로 변환해 버려 인덱스를 쓰지 못한다. Oracle 은 값 하나라도 숫자로 못 바꾸면 ORA-01722: invalid number 로 죽는다. 죽는 쪽보다 조용히 느려지는 쪽이 더 위험하다. 컬럼 타입과 같은 타입의 리터럴을 쓴다(WHERE emp_no = '1001').
4. 날짜 BETWEEN 으로 마지막 날을 잃어버린다
-- 컬럼이 DATETIME 이면 12월 31일 00:00:00 이후 데이터가 빠진다
WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31'
-- 안전한 형태: 시작 이상, 다음 구간 시작 미만
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'
날짜·시각 컬럼에는 BETWEEN 대신 이 "이상 · 미만" 꼴을 기본으로 쓴다. 컬럼 타입이 DATE 에서 DATETIME 으로 바뀌어도 쿼리가 조용히 틀리지 않는다.
스스로 확인하기
- 개발팀(20) 또는 영업팀(30) 소속이면서 연봉이 4000 이상 6000 이하인 사원의 이름·부서번호·연봉을 조회하라.
- 부서가 배정되지 않은 사원을 조회하라.
WHERE dept_id = NULL이 왜 안 되는지 설명하라. - 이름이 '김' 으로 시작하지 않는 사원 중 2019-01-01 이후(당일 포함) 입사자를 조회하라.
-- 1
SELECT emp_name, dept_id, salary
FROM emp
WHERE dept_id IN (20, 30)
AND salary BETWEEN 4000 AND 6000;
-- 2
SELECT emp_id, emp_name FROM emp WHERE dept_id IS NULL;
-- dept_id = NULL 은 UNKNOWN 으로 판정되고, WHERE 는 참인 행만 남긴다.
-- 오류가 아니라 0행이 나오므로 더 위험하다.
-- 3
SELECT emp_name, hire_date
FROM emp
WHERE emp_name NOT LIKE '김%'
AND hire_date >= DATE '2019-01-01';