Devin.KR
로그인

SQL WHERE 조건절 사용법 - 비교 연산자, BETWEEN, IN, LIKE, NULL (SQL 초급 2단원)

개발자 조회 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 ba 이상 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 NULLNULL 여부. 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 / MariaDBPostgreSQLOracleSQL 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 으로 바뀌어도 쿼리가 조용히 틀리지 않는다.

스스로 확인하기

  1. 개발팀(20) 또는 영업팀(30) 소속이면서 연봉이 4000 이상 6000 이하인 사원의 이름·부서번호·연봉을 조회하라.
  2. 부서가 배정되지 않은 사원을 조회하라. WHERE dept_id = NULL 이 왜 안 되는지 설명하라.
  3. 이름이 '김' 으로 시작하지 않는 사원 중 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';

MySQL 공식 문서 - Working with NULL Values