조건식과 필터링
연속된 범위에는 BETWEEN, 값의 목록에는 IN, 문자열의 패턴에는 LIKE를 쓰는 방법을 고정된 실습 데이터로 확인합니다. AND와 OR의 우선순위, 양 끝 값의 포함 여부, 와일드카드의 위치, 하루를 반열린 구간으로 찾는 방법을 다룹니다.
조회수가 30회에서 60회 사이인 글, 여러 상태 가운데 하나인 글, 제목이 특정한 말로 시작하는 글은 모두 필터지만 값의 모양이 다릅니다. 모든 조건을 긴 OR와 비교식으로만 쓰면 뜻이 흐려지고, 범위의 끝을 빠뜨리기 쉽습니다.
이 글에서는 연속된 범위에 BETWEEN, 값의 목록에 IN, 문자열의 패턴에 LIKE를 씁니다. 문법을 줄이는 것이 목적이 아니라, 요구 사항의 구조가 SQL의 연산자에 그대로 보이게 하는 것이 목적입니다. 앞 글에서 만든 query_post_demo의 14행을 그대로 씁니다.
괄호 없는 OR와 넓은 패턴
공개 글이나 숨김 글 가운데 조회수가 30회에서 60회 사이이고, 제목이 SQL로 시작하는 글을 찾는다고 해 봅시다. 다음 쿼리는 이 요구를 두 군데에서 잘못 옮겼습니다.
SELECT id, title, status, view_count
FROM query_post_demo
WHERE status = 'PUBLISHED'
OR status = 'HIDDEN'
AND view_count BETWEEN 30 AND 60
AND title LIKE '%SQL%'
ORDER BY id;id | title | status | view_count
1 | SELECT 기초 | PUBLISHED | 25
2 | SQL WHERE | PUBLISHED | 50
3 | NULL 복습 | PUBLISHED | 40
4 | 설치 메모 | PUBLISHED | 10
5 | 정렬 연습 | PUBLISHED | 35
6 | MySQL 인덱스 | PUBLISHED | 25
7 | 설치 메모 | PUBLISHED | 10
8 | 고급 SQL 복습 | HIDDEN | 45
9 | SQL 함수 | HIDDEN | 30
10 | LIKE 복습 | PUBLISHED | 20
11 | 정렬 동률 | PUBLISHED | 40
12 | LIMIT 실습 | PUBLISHED | 45초안 두 개를 뺀 12행이 나왔습니다. AND가 먼저 평가되므로 이 조건은 status = 'PUBLISHED' OR (status = 'HIDDEN' AND ...)로 읽히고, 공개 글 10개는 조회수와 제목의 조건을 모두 건너뛰었습니다. 조회수가 10회인 '설치 메모'가 들어온 것이 그 결과입니다. SQL은 줄바꿈이나 들여쓰기로 논리를 바꾸지 않습니다.
숨김 글 쪽에도 문제가 있습니다. %SQL%은 앞뒤에 어떤 문자열이 와도 되므로, 'SQL'로 시작하지 않는 '고급 SQL 복습'도 통과했습니다. 요구 사항의 "SQL로 시작"을 옮긴 패턴은 SQL%입니다.
값의 모양에 맞는 연산자
BETWEEN a AND b는 두 끝을 모두 포함하는 연속된 범위입니다. IN은 한 열이 여러 개의 값 가운데 하나인지를 나타내며, 같은 뜻의 긴 OR보다 목록이라는 의도를 잘 드러냅니다. LIKE에서 %는 0개 이상의 문자를, _는 정확히 한 문자를 뜻합니다.
패턴은 데이터의 값이 아니라 검색의 규칙이므로, 사용자가 입력한 %와 _를 문자 그대로 찾으려면 이스케이프(escape)가 필요합니다. 따로 정하지 않으면 이스케이프 문자는 역슬래시여서 LIKE '100\%'는 문자 그대로의 100%를 찾고, 다른 문자를 쓰려면 ESCAPE 절로 정합니다. BETWEEN과 IN, LIKE는 표준 SQL에 속하지만, MySQL에서 패턴이 대소문자를 구별하는지는 열의 정렬 규칙에 따라 정해집니다.
LIKE 'SQL%'처럼 고정된 접두사가 있는 패턴은 인덱스의 범위를 쓸 여지가 있습니다. 반면 LIKE '%SQL'처럼 와일드카드로 시작하면 일반적인 B+Tree 인덱스에서 탐색을 시작할 위치를 정할 수 없습니다. 실제로 어떤 경로를 쓰는지는 7장 2편과 7장 3편에서 실행 계획으로 확인합니다.
더 깊이: 값이 많은 IN 목록을 MySQL이 다루는 방법
IN 목록의 값이 모두 같은 타입의 상수이고 형 변환이 필요 없으면, MySQL은 목록을 미리 정렬해 두고 이진 탐색으로 일치하는 값을 찾습니다. 그래서 목록이 길어져도 행마다의 판정은 빠르며, 목록에 넣을 수 있는 값의 수는 max_allowed_packet으로만 제한됩니다. 반대로 IN (1, 2, 'a')처럼 따옴표가 있는 값과 없는 값을 섞으면 비교 규칙이 서로 달라 결과가 어긋날 수 있습니다. 공식 문서의 예에서는 'a' IN (0)이 양쪽 모두 부동소수점 0으로 바뀌어 참이 되므로, 목록의 값은 열의 타입 하나로 맞춥니다.
인덱스를 쓸 때는 실행 계획을 세우는 과정도 달라집니다. 유일하지 않은 인덱스로 col IN (...)을 찾을 때, 옵티마이저는 값마다 인덱스에 직접 들어가 범위의 양 끝을 확인하는 인덱스 다이브(index dive)로 행 수를 추정합니다. 이 추정은 정확하지만 값이 많을수록 오래 걸리므로, 값이 eq_range_index_dive_limit(기본 200)개 이상이면 덜 정확한 인덱스 통계로 바꿉니다. 그래서 목록이 199개에서 200개로 늘어나는 순간 추정 행 수가 달라지고, 그에 따라 실행 계획이 바뀔 수도 있습니다.
범위 최적화가 쓰는 메모리에도 한도가 있습니다. range_optimizer_max_mem_size의 기본값은 8388608바이트이고, MySQL 8.4 매뉴얼은 단순한 쿼리에서 OR로 이은 조건 하나가 약 230바이트를 쓴다고 설명합니다. IN 목록의 값 하나는 OR로 이은 조건 하나로 세고, 두 IN 목록을 AND로 이으면 두 목록의 값 수를 곱한 만큼이 됩니다. 한도를 넘기면 옵티마이저는 범위 접근을 포기하고 전체 테이블 스캔을 포함한 다른 방법을 고르며 경고 3170을 남기고, sql_safe_updates가 켜진 UPDATE와 DELETE에서는 이것이 오류가 됩니다.
목록과 범위, 패턴을 함께 쓴다
상태는 값의 목록으로, 조회수는 양 끝을 포함하는 범위로, 제목은 접두사의 패턴으로 나타냅니다. 세 조건이 모두 AND로 이어지므로 괄호의 우선순위도 단순해집니다.
SELECT id, title, status, view_count
FROM query_post_demo
WHERE status IN ('PUBLISHED', 'HIDDEN')
AND view_count BETWEEN 30 AND 60
AND title LIKE 'SQL%'
ORDER BY view_count DESC, id;id | title | status | view_count
2 | SQL WHERE | PUBLISHED | 50
9 | SQL 함수 | HIDDEN | 30이제 세 조건을 모두 만족한 두 행만 남았습니다. 9번은 조회수가 하한인 30과 같아서 BETWEEN이 양 끝을 포함하기 때문에 들어왔고, 8번은 제목이 'SQL'로 시작하지 않아 빠졌습니다. 다음 도표는 대표적인 행마다 세 조건을 어떻게 판정했는지 보여 줍니다.
WHERE · SAME ROW / TWO CONDITIONS
공개 상태만으로 통과하지 않게 세 조건을 묶기
같은 fixture 행에 두 필터를 적용했습니다. 잘못된 OR 분기는 공개 글에 조회수·제목 검사를 적용하지 않고, 포함 패턴은 제목 중간의 SQL도 통과시킵니다.
잘못된 조건 · 공개 분기는 단독 통과
PUBLISHED OR (HIDDEN
AND 30..60회
AND '%SQL%')수정 조건 · 세 조건 모두 충족
(PUBLISHED OR HIDDEN)
AND 30..60회
AND 'SQL%'위 표현은 SQL 조건의 논리를 줄여 쓴 것입니다. 실제 연산자는 본문의 IN·BETWEEN·LIKE를 사용합니다.
| fixture 행 | 잘못된 조건 | 수정 조건 |
|---|---|---|
| 7 · 설치 메모 | 통과 · 공개 분기 | 제외 · 10회, 접두사 불일치 |
| 8 · 고급 SQL 복습 | 통과 · 숨김, 45회, 포함 패턴 | 제외 · SQL로 시작하지 않음 |
| 9 · SQL 함수 | 통과 · 숨김, 30회 | 통과 · 세 조건 충족 |
| 2 · SQL WHERE | 통과 · 공개 분기 | 통과 · 세 조건 충족 |
- 7 · 설치 메모
- 잘못된 조건: 통과 · 공개 분기수정 조건: 제외 · 10회, 접두사 불일치
- 8 · 고급 SQL 복습
- 잘못된 조건: 통과 · 숨김, 45회, 포함 패턴수정 조건: 제외 · SQL로 시작하지 않음
- 9 · SQL 함수
- 잘못된 조건: 통과 · 숨김, 30회수정 조건: 통과 · 세 조건 충족
- 2 · SQL WHERE
- 잘못된 조건: 통과 · 공개 분기수정 조건: 통과 · 세 조건 충족
IN의 목록에 NULL을 넣어 "NULL인 행도 포함"하려고 하면 뜻대로 되지 않습니다. NULL과의 비교는 참이 되지 않으므로, column IN (...) OR column IS NULL처럼 조건을 따로 적어야 합니다. 패턴도 사용자가 입력한 문자열을 그대로 붙이지 말고 바인딩과 이스케이프 규칙을 함께 적용해야, SQL 주입과 와일드카드의 오해를 함께 피할 수 있습니다.
하루를 반열린 구간으로 찾는다
2026년 7월 13일 하루의 글을 찾을 때 BETWEEN '2026-07-13 00:00:00' AND '2026-07-13 23:59:59'로 쓰면, 23:59:59.5처럼 마지막 1초 안의 소수 시각을 빠뜨립니다. 시작 시각 이상, 다음 날의 시작 시각 미만으로 쓰는 반열린 구간(half-open interval)은 열의 정밀도가 바뀌어도 그대로 맞습니다.
SELECT id, title, created_at
FROM query_post_demo
WHERE created_at >= '2026-07-13 00:00:00'
AND created_at < '2026-07-14 00:00:00'
ORDER BY created_at, id;id | title | created_at
8 | 고급 SQL 복습 | 2026-07-13 12:00:00.000000
9 | SQL 함수 | 2026-07-13 13:00:00.000000
10 | LIKE 복습 | 2026-07-13 14:00:00.000000
11 | 정렬 동률 | 2026-07-13 15:00:00.000000
12 | LIMIT 실습 | 2026-07-13 15:00:00.000000이 조건은 DATETIME(6)의 23:59:59.999999에 만들어진 행도 포함하고, 다음 날 0시 정각의 행은 뺍니다. 실습 데이터에는 그런 행이 없지만, 조건이 그 경우를 바르게 나누는지는 값을 대입해 확인할 수 있습니다. 다음 날의 시작 시각은 애플리케이션이나 분명한 날짜 함수로 계산해 넘기고, DATE(created_at) = '2026-07-13'처럼 열에 함수를 씌우는 방식이 인덱스에 어떤 영향을 주는지는 함께 점검합니다.
연산자를 고를 때는 결과의 정확성과 함께 탐색할 수 있는지도 봅니다. 예를 들어 LOWER(title) LIKE 'sql%'은 대소문자의 규칙을 쿼리 안에서 강제하지만, 열에 씌운 표현식 때문에 기존의 인덱스를 그대로 쓰기 어렵습니다. 먼저 알맞은 정렬 규칙을 설계하고, 표현식으로 검색해야 한다면 함수 기반 인덱스나 생성 열을 7장 이후에 검토합니다.
양 끝 값과 빈 목록을 시험한다
필터는 표본 한두 행으로 확인하고 끝내지 않습니다. 30회에서 60회 사이라는 조건이라면 29, 30, 60, 61회를 모두 넣어 포함되는지 기록하고, 날짜의 범위에는 시작 직전과 시작, 끝 직전과 끝을 넣습니다. LIKE에는 빈 문자열, 접두사만 같은 값, 검색어가 가운데 있는 값, %와 _가 실제 문자로 든 값을 준비합니다.
IN의 목록을 애플리케이션이 동적으로 만든다면 목록이 비었을 때를 미리 정해야 합니다. MySQL에서 IN ()은 문법 오류이고, 목록이 비었다는 것을 "조건 없음"으로 볼지 "결과 없음"으로 볼지는 제품이 정할 일이지 SQL을 만드는 코드가 대신 정할 일이 아닙니다. 같은 필터라도 검색 화면과 권한 필터는 오류가 났을 때의 기본 동작이 달라야 합니다.
검색 조건이 많아지면 입력하지 않은 조건을 NULL 파라미터로 넘겨 WHERE (? IS NULL OR status = ?)처럼 한 문장에 섞는 일이 많습니다. 편리하지만 여러 경우의 선택도와 실행 계획이 한 문장에 섞입니다. 조합이 많지 않다면 필요한 조건식만 조립하고 바인딩 값을 따로 넘기는 방식이 읽기에도 튜닝에도 유리합니다.
LIKE 검색어의 길이에도 제한이 필요합니다. 한 글자로 포함 검색을 하면 거의 모든 행을 읽을 수 있고, 사용자에게 와일드카드를 마음대로 쓰게 하면 비용을 예측하기 어렵습니다. 최소 글자 수와 접두사 검색의 우선, 전문 검색을 들일 기준을 제품에서 정한 뒤, SQL은 그 규칙을 정확히 구현합니다.
NOT IN과 NOT BETWEEN, NOT LIKE는 화면의 체크박스를 뒤집는 기능이 아닙니다. NULL이 들어 있는 값은 여전히 알 수 없음으로 남아 반대쪽 집합에도 들어오지 않을 수 있으며, 이 문제는 3장 4편에서 다룹니다. 제외 검색을 만들 때는 원래의 집합과 포함되는 집합, NULL인 집합을 각각 세어 전체가 어떻게 나뉘는지 확인합니다.
문자열 끝의 공백이나 유니코드 정규화 방식이 달라도, 사용자의 눈에는 같은 문자열로 보이지만 비교 결과는 다를 수 있습니다. 검색의 품질이 중요하다면 정렬 규칙 하나로 해결하려 하지 말고, 입력의 정규화와 검색 전용 인덱스를 따로 설계합니다. 사용자가 입력한 지역 날짜를 서버의 UTC 범위로 바꿀 때는 일광 절약 시간과 지역 설정도 시험합니다.
| 따져 볼 것 | 질문 |
|---|---|
| 연속 범위 | 하한과 상한이 포함되는지 빠지는지 분명한가 |
| 값의 목록 | 같은 열의 여러 값을 IN으로 나타낼 수 있는가 |
| 문자 패턴 | 접두사, 접미사, 포함 가운데 실제 요구는 무엇인가 |
| NULL | 목록의 비교와 별도로 IS NULL이 필요한가 |
| 입력 | 와일드카드와 파라미터를 안전하게 처리했는가 |
짧은 문법보다 양 끝 값의 시험이 중요합니다. 최솟값과 최댓값, 바로 바깥의 값, 빈 목록, NULL, %가 든 사용자 입력을 시험 데이터로 만들어 결과를 확인합니다.
정리
BETWEEN은 양 끝을 포함하므로, 소수 초가 있는 시각에는 반열린 구간을 씁니다.IN은 같은 열의 값 목록을 나타내고, NULL은IS NULL로 따로 처리합니다.LIKE의 와일드카드 위치는 찾는 범위와 인덱스를 쓸 수 있는지를 함께 바꿉니다.- 복합 조건은 괄호로 업무의 문장에 맞게 묶고, 양 끝 값과 빈 목록을 시험합니다.
연습 문제
1. 사흘 동안의 MySQL 글
7월 10일부터 7월 12일까지 작성된 글 가운데 상태가 DRAFT나 PUBLISHED이고 제목이 MySQL로 시작하는 행을 찾아 보세요. created_at은 DATETIME(6)이며 7월 12일 밤의 모든 시각을 포함해야 합니다.
해설 보기
날짜는 7월 10일 이상, 7월 13일 미만으로 적으면 12일의 모든 소수 시각을 포함합니다. 상태는 IN으로, 제목은 고정된 접두사의 패턴으로 적습니다.
SELECT id, title, status, created_at
FROM query_post_demo
WHERE created_at >= '2026-07-10 00:00:00'
AND created_at < '2026-07-13 00:00:00'
AND status IN ('DRAFT', 'PUBLISHED')
AND title LIKE 'MySQL%'
ORDER BY created_at, id;id | title | status | created_at
6 | MySQL 인덱스 | PUBLISHED | 2026-07-12 15:00:00.000000실습 데이터에서는 6번 한 행이 나옵니다. 다만 7월 13일 0시 직전이나 정각의 행, 제목 가운데에 MySQL이 든 행은 이 데이터에 없으므로, 그 경우는 조건에 값을 대입해 따로 판정해야 합니다. 결과가 한 행이라는 것만으로 양 끝의 처리까지 확인한 것은 아닙니다.
2. 포함 검색과 접두사 검색
title LIKE '%SQL%'과 title LIKE 'SQL%'은 각각 어떤 행을 찾나요? 두 결과의 차이가 무엇인지 설명해 보세요.
해설 보기
SELECT id, title FROM query_post_demo WHERE title LIKE '%SQL%' ORDER BY id;
SELECT id, title FROM query_post_demo WHERE title LIKE 'SQL%' ORDER BY id;id | title
2 | SQL WHERE
6 | MySQL 인덱스
8 | 고급 SQL 복습
9 | SQL 함수
id | title
2 | SQL WHERE
9 | SQL 함수포함 검색은 'SQL'이 어디에 있어도 되므로 'MySQL 인덱스'처럼 단어의 일부로 든 제목과 가운데에 든 '고급 SQL 복습'까지 찾습니다. 접두사 검색은 제목의 맨 앞만 보므로 두 행만 남습니다. 두 패턴의 차이는 결과의 범위뿐 아니라, 인덱스에서 탐색을 시작할 위치를 정할 수 있는지에도 있습니다.
3. IN 목록의 NULL
게시 시각이 없는 글을 찾으려고 WHERE published_at IN (NULL)이라고 썼습니다. 결과는 몇 행이고, 어떻게 고쳐야 하나요?
해설 보기
published_at IN (NULL)은 published_at = NULL과 같아서 어느 행에서도 참이 되지 않으므로 0행입니다. 게시 시각이 없는 행은 IS NULL로 찾습니다.
SELECT id, title
FROM query_post_demo
WHERE published_at IS NULL
ORDER BY id;id | title
13 | JOIN 계획
14 | GROUP BY 검토목록에 NULL을 함께 넣은 status IN ('DRAFT', NULL)도 DRAFT인 행만 찾을 뿐, NULL인 행을 더해 주지 않습니다. NULL이 비교에서 왜 이렇게 동작하는지는 3장 4편에서 세 값 논리로 정리합니다.
다음 글 정렬·제한·중복 제거에서는 필터를 통과한 결과의 순서와 개수를, 같은 값이 있어도 흔들리지 않는 규칙으로 고정합니다.