※ SQL: Date Condition Optimization.
안녕하세요. 듀스트림입니다.
오랜만에 튜닝 사례를 가져왔습니다.
운영 중 긴급 배포된 쿼리로 인해 Disk I/O 사용률이 장시간 100%로 유지되는 문제가 발생했습니다.
원인을 확인해보니, 날짜 조건에 TO_CHAR() 함수가 사용되고 있었습니다.
그 결과 대량의 데이터 스캔이 발생했고, 읽기 병목으로 이어졌습니다.
1. AS-IS (문제 상황)
문제가 된 조건은 다음과 같습니다.
-- 1번 쿼리 조건절
TO_CHAR(created_at, 'YYYY-MM-DD') = '2026-04-13'
-- 2번 쿼리 조인 조건절
TO_CHAR(a.created_at, 'YYYY-MM-DD') = TO_CHAR(b.created_at, 'YYYY-MM-DD')
created_at 컬럼의 데이터 타입은 timestamp입니다.
위 쿼리들의 공통 문제는 다음과 같습니다.
- 컬럼에 함수가 적용되면 일반적인 (created_at) B-Tree 인덱스를 그대로 활용하기 어렵습니다.
- created_at 컬럼에 TO_CHAR() 함수 적용
결과적으로 대량 데이터 스캔이 발생하여, CPU + I/O 비용이 급격히 증가하고 쿼리가 장시간 수행될 수 있습니다.
2. TO-BE (개선 방향)
핵심은 하나입니다.
컬럼에 함수를 적용하지 않는다.
1번 쿼리 개선
날짜를 문자열 비교에서 시간 범위 조건으로 변환하면 됩니다.
-- AS-IS: 문제 쿼리
TO_CHAR(created_at, 'YYYY-MM-DD') = '2026-04-13'
아래와 같이 작성하면, 타입 오류를 만나실 수도 있습니다.
created_at >= '2026-04-13'
AND created_at < '2026-04-13' + INTERVAL '1 day'
PostgreSQL 등 대부분의 DBMS는 문자열 리터럴 타입을 문맥에 따라 추론할 수 있지만, 날짜 비교 의도를 명확히 하기 위해 타입을 명시하는 편이 더 안전하고 읽기 쉽습니다.
-- TO-BE: 1번 쿼리 최적화
created_at >= CAST('2026-04-13' AS DATE)
AND created_at < CAST('2026-04-13' AS DATE) + INTERVAL '1' DAY
2번 쿼리 같은 경우 해석이 조금 더 필요합니다.
-- AS-IS: 문제 쿼리
TO_CHAR(a.created_at, 'YYYY-MM-DD') = TO_CHAR(b.created_at, 'YYYY-MM-DD')
아래처럼 작성하면 AS-IS의 논리와는 완전히 다르게 되어버립니다.
a.created_at >= b.created_at
AND a.created_at < b.created_at + INTERVAL '1 day'
이유는 AS-IS 쿼리는 특정 시간과 관계없이 해당 날짜 전체를 의미합니다.
하지만 이 구문은 created_at의 실제 시간 값에 24시간을 더하는 방식으로 동작하여, 날짜 기준이 아닌 특정 시점부터 24시간 범위를 의미하게 됩니다.
예를 들어 b.created_at 값이 '2026-04-13 15:30:00'인 경우, 위 쿼리는 다음과 같이 해석됩니다.
a.created_at >= '2026-04-13 15:30:00'
AND a.created_at < '2026-04-13 15:30:00' + INTERVAL '1 day'
/* = '2026-04-13 15:30:00' ~ '2026-04-14 15:29:59' */
즉, 특정 시점부터 24시간 범위를 비교하게 되어 날짜 기준 비교와는 다른 의미를 갖게 됩니다.
따라서 날짜 기준으로 비교하기 위해서는 아래와 같이 타입 변환이 필요합니다.
-- 2번 쿼리 최적화
a.created_at >= CAST(b.created_at AS DATE)
AND a.created_at < CAST(b.created_at AS DATE) + INTERVAL '1 day'
이 경우, b.created_at = '2026-04-13 15:30:00'이라면 아래와 같이 해석됩니다.
a.created_at >= '2026-04-13'
AND a.created_at < '2026-04-13' + INTERVAL '1 day'
/* = '2026-04-13' */
이처럼 날짜 기준(하루 단위)으로 조건을 맞출 수 있으며, AS-IS와 동일한 의미로 데이터를 필터링할 수 있습니다.
+ 아래 처럼 표현도 가능합니다.
-- DATE()
a.created_at >= DATE(b.created_at)
AND a.created_at < DATE(b.created_at) + INTERVAL '1' DAY
++ postgreSQL에서는 전용 문법과 함수를 사용해 아래처럼 표현할 수도 있습니다.
-- ::date
a.created_at >= b.created_at::date
AND a.created_at < b.created_at::date + INTERVAL '1 day'
-- date_trunc()
ah.created_at >= date_trunc('day', aps.created_at)
AND ah.created_at < date_trunc('day', aps.created_at) + interval '1 day'
❗여기서 중요한 점은 인덱스를 활용하려는 컬럼인 a.created_at 자체에는 함수가 적용되지 않았다는 것입니다.
date_trunc()?
PostgreSQL 전용 함수로, 날짜/시간을 특정 단위 기준으로 잘라내는 함수
SELECT date_trunc('day', '2026-04-13 15:23:45'::timestamp);
= 2026-04-13 00:00:00
오늘은 여기까지~
'SQL' 카테고리의 다른 글
| SQL: 윈도우 함수 문법 구조 (0) | 2025.11.24 |
|---|---|
| SQL: 행 스트림 (0) | 2025.11.17 |
| SQL: 상관관계(상관 서브쿼리) (0) | 2025.10.27 |
| SQL: 실행 순서 (0) | 2025.10.24 |
| SQL: EXISTS / NOT EXISTS (0) | 2025.10.15 |