SQL

SQL: 날짜 조건 최적화

dewstream 2026. 4. 20. 08:00
728x90

※ 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

오늘은 여기까지~
 
 

728x90

'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