MySQL에서 날짜를 검색하다 보면 이런 쿼리를 자주 작성하게 됩니다.
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-08-31';
의미는 명확합니다.
created_at에서 날짜 부분만 가져와서 2026-08-31인지 확인하는 것입니다.
그런데 created_at에 인덱스가 있다면 어떨까요?
CREATE INDEX idx_orders_created_at
ON orders (created_at);
인덱스까지 만들어 놨는데도 쿼리가 기대만큼 빨라지지 않는 경우가 있습니다.
왜 그럴까요?
핵심은 WHERE 조건에서 컬럼에 함수를 적용하고 있다는 것입니다.
이번 글에서는 실제 SQL을 통해 다음 두 가지 조건을 비교해보겠습니다.
WHERE DATE(created_at) = '2026-08-31'
그리고
WHERE created_at >= '2026-08-31 00:00:00'
AND created_at < '2026-09-01 00:00:00'
두 조건이 왜 다르게 동작할 수 있는지 알아보겠습니다.
1. 컬럼에 함수를 적용한다는 것은 무엇일까?
먼저 가장 단순한 예를 보겠습니다.
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-08-31';
여기서 created_at은 원래 다음과 같은 값이라고 해보겠습니다.
2026-08-31 09:30:00
2026-08-31 14:20:00
2026-08-31 23:10:00
DATE()를 적용하면 날짜 부분만 남습니다.
DATE('2026-08-31 09:30:00')
→ 2026-08-31
DATE('2026-08-31 14:20:00')
→ 2026-08-31
DATE('2026-08-31 23:10:00')
→ 2026-08-31
따라서
WHERE DATE(created_at) = '2026-08-31'
은 원하는 데이터를 정확하게 찾을 수 있습니다.
문제는 검색 조건이 어떤 형태로 만들어지는가입니다.
2. created_at에 인덱스가 있다면?
다음과 같이 인덱스를 만들어보겠습니다.
CREATE INDEX idx_orders_created_at
ON orders (created_at);
인덱스는 created_at 값을 기준으로 정렬된 구조를 가지고 있습니다.
개념적으로 단순화하면 다음과 같습니다.
2026-08-29 10:20:00
2026-08-29 15:30:00
2026-08-30 09:10:00
2026-08-30 18:40:00
2026-08-31 08:00:00
2026-08-31 12:30:00
2026-08-31 21:20:00
2026-09-01 09:00:00
이런 구조라면
WHERE created_at >= ...
AND created_at < ...
같은 조건으로 특정 시간 범위를 찾는 것이 자연스럽습니다.
그런데 다음과 같이 작성하면 이야기가 달라집니다.
WHERE DATE(created_at) = '2026-08-31'
MySQL은 단순히 created_at 값을 비교하는 것이 아니라,
created_at
↓
DATE()
↓
날짜 값 계산
↓
2026-08-31과 비교
라는 과정을 거쳐야 합니다.
즉 조건의 왼쪽에 있는 값이 원래 컬럼 자체가 아니라 함수의 결과가 됩니다.
3. EXPLAIN으로 확인해보자
먼저 다음 쿼리를 실행합니다.
EXPLAIN
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-08-31';
그리고 다음 쿼리도 확인합니다.
EXPLAIN
SELECT *
FROM orders
WHERE created_at >= '2026-08-31 00:00:00'
AND created_at < '2026-09-01 00:00:00';
두 실행 계획을 비교해봅니다.
특히 다음 항목을 확인하면 됩니다.
type
possible_keys
key
rows
Extra
이번 테스트의 핵심은 단순히 key 하나만 보는 것이 아닙니다.
MySQL이 인덱스를 이용해서 검색 범위를 얼마나 좁힐 수 있는지를 확인하는 것이 중요합니다.
실제 결과는 테스트 데이터의 양과 분포, MySQL 버전, 통계 등에 따라 달라질 수 있습니다.
따라서 실행 결과의 숫자를 임의로 적어서는 안 됩니다.
4. 더 좋은 형태로 조건을 작성할 수 있다
날짜만 검색하려는 목적이라면 다음과 같이 작성할 수 있습니다.
SELECT *
FROM orders
WHERE created_at >= '2026-08-31 00:00:00'
AND created_at < '2026-09-01 00:00:00';
이 조건은 의미상 다음과 같습니다.
2026-08-31 00:00:00 이상
그리고
2026-09-01 00:00:00 미만
즉 하루 전체를 조회합니다.
2026-08-31 00:00:00
↓
↓ 조회
↓
2026-08-31 23:59:59
↓
2026-09-01 00:00:00
여기서는 created_at 컬럼 자체를 비교하고 있습니다.
created_at >= ...
created_at < ...
DATE(created_at)처럼 컬럼을 함수로 변환하지 않습니다.
이런 형태의 조건은 인덱스를 이용한 범위 검색과 잘 맞습니다.
5. 왜 >=와 <를 사용하는가?
처음 보면 다음과 같이 작성하고 싶을 수도 있습니다.
WHERE created_at >= '2026-08-31 00:00:00'
AND created_at <= '2026-08-31 23:59:59';
하지만 날짜와 시간 값을 다룰 때는 이런 방식보다 다음처럼 작성하는 것을 권장할 수 있습니다.
WHERE created_at >= '2026-08-31 00:00:00'
AND created_at < '2026-09-01 00:00:00';
이렇게 하면 하루의 시작과 다음 날의 시작을 기준으로 범위를 표현할 수 있습니다.
개념적으로는 다음과 같습니다.
[ 2026-08-31 00:00:00 , 2026-09-01 00:00:00 )
왼쪽은 포함하고 오른쪽은 포함하지 않는 범위입니다.
이 방식은 시간의 마지막 순간을 억지로 계산하지 않아도 된다는 장점이 있습니다.
다만 BETWEEN과 범위 조건 자체에 대한 자세한 내용은 다음 글에서 별도로 다루겠습니다.
6. 이것이 SARGable 조건과 연결된다
여기서 조금 어려운 용어가 하나 등장합니다.
바로 SARGable입니다.
처음부터 용어를 어렵게 외울 필요는 없습니다.
쉽게 말하면,
데이터베이스가 인덱스를 이용해서 검색 범위를 효율적으로 줄일 수 있는 형태의 조건
이라고 이해하면 됩니다.
예를 들어 다음 조건을 비교해보겠습니다.
WHERE DATE(created_at) = '2026-08-31'
반면 다음은
WHERE created_at >= '2026-08-31 00:00:00'
AND created_at < '2026-09-01 00:00:00'
created_at 자체를 기준으로 범위를 지정하고 있습니다.
B-Tree 인덱스가 다음처럼 정렬되어 있다고 생각하면 이해하기 쉽습니다.
2026-08-29
2026-08-30
2026-08-31
2026-09-01
2026-09-02
특정 날짜의 범위를 찾는다면
2026-08-31
↓
여기부터
↓
2026-09-01 전까지
라는 범위를 찾아갈 수 있습니다.
즉 인덱스의 정렬 구조를 활용할 수 있는 형태입니다.
7. 함수가 항상 문제인 것은 아니다
여기서 중요한 오해를 하나 짚고 넘어가야 합니다.
WHERE에서 함수를 사용하면 무조건 인덱스를 사용할 수 없다.
이렇게 이해하면 안 됩니다.
문제는 함수를 사용했다는 사실 자체가 아니라, 인덱스가 걸린 컬럼에 함수를 적용해서 검색 조건을 만드는 방식입니다.
예를 들어 다음은 전혀 다른 문제입니다.
SELECT *
FROM users
WHERE name = UPPER('kim');
여기서는 함수가 컬럼에 적용된 것이 아닙니다.
name
=
UPPER('kim')
즉 비교 대상인 값에 함수를 적용했습니다.
반면 다음은
WHERE UPPER(name) = 'KIM'
컬럼인 name에 함수를 적용하고 있습니다.
이 둘은 데이터베이스 입장에서 같은 형태의 조건이 아닙니다.
따라서 SQL을 작성할 때는
함수를 사용했는가?
보다
인덱스가 걸린 컬럼에 함수를 적용했는가?
를 확인하는 것이 중요합니다.
8. DATE() 외에도 같은 문제가 발생할 수 있다
DATE()만의 이야기가 아닙니다.
다음과 같이 컬럼에 함수를 적용하는 조건도 같은 관점에서 살펴봐야 합니다.
WHERE YEAR(created_at) = 2026;
WHERE MONTH(created_at) = 8;
WHERE LOWER(email) = 'test@example.com';
WHERE UPPER(name) = 'KIM';
또는 계산을 적용하는 경우도 있습니다.
WHERE price + 1000 > 10000;
이런 조건을 발견했다면 무조건 문제가 있다고 단정하기보다,
1. 해당 컬럼에 인덱스가 있는가?
2. 실제 데이터가 얼마나 많은가?
3. EXPLAIN에서 어떤 실행 계획이 나오는가?
4. 컬럼 자체를 비교하는 조건으로 바꿀 수 있는가?
를 확인해야 합니다.
9. YEAR(created_at)도 날짜 범위로 바꿀 수 있다
예를 들어 다음 쿼리가 있다고 해보겠습니다.
SELECT *
FROM orders
WHERE YEAR(created_at) = 2026;
2026년의 데이터를 조회하려는 목적입니다.
이 경우에도 컬럼에 YEAR()를 적용하는 대신 해당 연도의 범위를 직접 표현할 수 있습니다.
SELECT *
FROM orders
WHERE created_at >= '2026-01-01 00:00:00'
AND created_at < '2027-01-01 00:00:00';
여기서 중요한 것은 YEAR()라는 함수를 사용하지 않는 것이 목적이 아니라는 점입니다.
검색하려는 범위를 컬럼 자체에 적용할 수 있도록 조건을 변경하는 것이 핵심입니다.
10. 실무에서 자주 발생하는 문제
이런 문제는 ORM을 사용할 때도 쉽게 만들어집니다.
예를 들어 Laravel에서 날짜를 기준으로 조회하다 보면 애플리케이션 코드에서 편리한 메서드를 사용하게 됩니다.
Order::whereDate('created_at', '2026-08-31')->get();
코드 자체는 매우 읽기 쉽습니다.
하지만 중요한 것은 ORM 코드가 아니라 최종적으로 데이터베이스에 어떤 SQL 조건을 전달하는가입니다.
ORM을 사용한다고 해서 데이터베이스의 실행 원리가 달라지는 것은 아닙니다.
따라서 성능 문제가 의심된다면 애플리케이션 코드만 보는 것이 아니라 실제 SQL과 실행 계획을 확인해야 합니다.
Laravel에서는 실제 실행되는 쿼리를 확인한 뒤 MySQL에서 직접 EXPLAIN을 실행해보는 방식으로 접근할 수 있습니다.
11. 그렇다고 함수를 전부 제거해야 하는 것은 아니다
실무에서 가장 중요한 부분입니다.
다음과 같은 쿼리를 발견했다고 해서
WHERE DATE(created_at) = ?
무조건 리팩터링해야 하는 것은 아닙니다.
데이터가 매우 적고 실행 빈도도 낮다면 성능 문제가 전혀 없을 수도 있습니다.
반대로
수백만 건의 데이터
+
자주 호출되는 API
+
인덱스가 있는 컬럼
+
검색 범위를 크게 줄일 수 있는 조건
이라면 작은 SQL 차이가 실제 서비스 성능에 영향을 줄 수 있습니다.
따라서 다음과 같은 순서로 판단하는 것이 좋습니다.
쿼리 발견
↓
실제 실행 빈도 확인
↓
데이터 규모 확인
↓
인덱스 확인
↓
EXPLAIN 확인
↓
문제가 있다면 조건 변경
↓
다시 EXPLAIN
↓
실제 성능 확인
12. 이번 글에서 직접 테스트할 내용
실제로 확인하려면 다음과 같은 테스트를 준비하면 됩니다.
먼저 인덱스를 생성합니다.
CREATE INDEX idx_orders_created_at
ON orders (created_at);
그 다음 두 조건을 각각 실행합니다.
테스트 1
EXPLAIN
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-08-31';
테스트 2
EXPLAIN
SELECT *
FROM orders
WHERE created_at >= '2026-08-31 00:00:00'
AND created_at < '2026-09-01 00:00:00';
그리고 두 실행 계획에서
type
possible_keys
key
rows
Extra
를 비교합니다.
MySQL 8.0 환경이라면 추가로 다음도 확인할 수 있습니다.
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE DATE(created_at) = '2026-08-31';
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE created_at >= '2026-08-31 00:00:00'
AND created_at < '2026-09-01 00:00:00';
여기서 나오는 실제 결과를 가지고
실제로 인덱스를 사용했는가?
얼마나 많은 Row를 확인했는가?
실행 시간이 얼마나 차이 나는가?
를 판단하면 됩니다.
데이터가 적은 테스트에서는 차이가 거의 나타나지 않을 수 있으므로, 성능 비교를 목적으로 한다면 충분한 양의 테스트 데이터를 준비하는 것이 좋습니다.
13. 핵심은 "컬럼을 가공하지 말라"가 아니다
이번 내용을 한 문장으로 외우면 오히려 위험합니다.
컬럼에 함수를 사용하면 안 된다.
가 아닙니다.
더 정확하게 표현하면 다음과 같습니다.
인덱스를 활용해야 하는 검색 조건이라면, 인덱스가 걸린 컬럼을 함수로 가공하기보다 컬럼 자체를 비교할 수 있는 조건을 먼저 고려한다.
예를 들어 날짜 검색이라면
WHERE DATE(created_at) = '2026-08-31'
보다
WHERE created_at >= '2026-08-31 00:00:00'
AND created_at < '2026-09-01 00:00:00'
같은 형태를 검토할 수 있습니다.
하지만 최종 판단은 항상 실행 계획과 실제 데이터에 따라 이루어져야 합니다.
마무리
이번 글에서 살펴본 문제는 아주 단순한 SQL에서 시작합니다.
WHERE DATE(created_at) = '2026-08-31'
개발자 입장에서는 읽기도 쉽고 원하는 데이터도 정확하게 가져옵니다.
하지만 created_at에 인덱스가 있고 데이터가 많다면 한 번쯤 생각해볼 필요가 있습니다.
"이 조건은 인덱스를 효율적으로 사용할 수 있는 형태인가?"
그리고 가능하다면 컬럼 자체를 기준으로 범위를 지정하는 방식으로 변경할 수 있습니다.
WHERE created_at >= '2026-08-31 00:00:00'
AND created_at < '2026-09-01 00:00:00'
중요한 것은 단순히 함수를 없애는 것이 아닙니다.
함수 적용
↓
인덱스 활용 가능성 확인
↓
EXPLAIN
↓
실제 실행 계획 확인
↓
필요하면 조건 변경
↓
다시 확인
이 과정을 통해 SQL 문법이 아니라 데이터베이스가 실제로 어떻게 검색하는지를 확인하는 습관을 만드는 것이 중요합니다.
그리고 이 개념은 날짜 검색에만 적용되는 것이 아닙니다.
YEAR(), MONTH(), LOWER(), UPPER() 등 컬럼에 함수를 적용하는 다양한 조건을 만났을 때도 같은 관점으로 생각할 수 있습니다.
결국 중요한 질문은 하나입니다.
"내가 작성한 WHERE 조건을 MySQL이 인덱스를 이용해 효율적으로 검색할 수 있는 형태로 만들었는가?"
'개발 > 데이터베이스' 카테고리의 다른 글
| MySQL DISTINCT 제대로 이해하기 - 중복 제거가 왜 느려질까? (0) | 2026.09.01 |
|---|---|
| MySQL BETWEEN과 범위 검색 제대로 이해하기 - 인덱스는 어떻게 범위를 찾을까? (0) | 2026.08.31 |
| MySQL LIKE 검색과 인덱스 - 어떤 검색은 왜 인덱스를 못 탈까? (0) | 2026.08.31 |
| MySQL LIMIT 성능 제대로 이해하기 - LIMIT 20인데 왜 느릴까? (0) | 2026.08.28 |
| MySQL ORDER BY와 인덱스 - Using filesort가 발생하는 이유 (0) | 2026.08.27 |