프로그래머스 SQL 문제를 풀다 보면 특정 월에 작성된 데이터를 조회해야 하는 경우가 있다.
예를 들어 다음과 같은 조건이 있다고 하자.
2022년 10월에 작성된 게시글 조회
이때 MySQL에서는 DATE_FORMAT()을 사용해서 날짜를 문자열로 변환한 뒤 비교할 수 있다.
WHERE DATE_FORMAT(b.CREATED_DATE, '%Y-%m') = '2022-10'
Oracle에서는 TO_CHAR()를 사용해서 비슷하게 작성할 수 있다.
WHERE TO_CHAR(b.CREATED_DATE, 'YYYY-MM') = '2022-10'
두 방식 모두 날짜 컬럼에서 연도와 월만 추출해 비교하기 때문에 직관적이다.
하지만 인덱스 활용 측면에서 보면, 날짜 컬럼에 함수를 적용하는 방식보다 날짜 컬럼 자체를 범위 조건으로 비교하는 방식이 더 적합하다고 정리할 수 있다.
WHERE b.CREATED_DATE >= '2022-10-01'
AND b.CREATED_DATE < '2022-11-01'
Oracle에서는 다음처럼 작성할 수 있다.
WHERE b.CREATED_DATE >= TO_DATE('2022-10-01', 'YYYY-MM-DD')
AND b.CREATED_DATE < TO_DATE('2022-11-01', 'YYYY-MM-DD')
MySQL 공식문서의 B-tree Index Characteristics에는 다음과 같은 설명이 있다.
A B-tree index can be used for column comparisons in expressions that use the =, >, >=, <, <=, or BETWEEN operators.
해석하면 다음과 같다.
B-tree 인덱스는 =, >, >=, <, <=, BETWEEN 연산자를 사용하는 컬럼 비교 표현식에서 사용될 수 있다.
여기서 중요한 표현은 column comparisons이다.
즉, 인덱스가 걸린 컬럼을 직접 비교하는 형태에서 B-tree 인덱스를 사용할 수 있다는 의미로 이해할 수 있다.
예를 들어 다음 조건은 CREATED_DATE 컬럼 자체를 >=, < 연산자로 비교한다.
WHERE b.CREATED_DATE >= '2022-10-01'
AND b.CREATED_DATE < '2022-11-01'
이 조건은 MySQL 공식문서에서 설명한 B-tree 인덱스의 사용 조건과 잘 맞는다.
반면 다음 조건은 컬럼 자체를 비교하는 것이 아니다.
WHERE DATE_FORMAT(b.CREATED_DATE, '%Y-%m') = '2022-10'
이 조건에서 비교 대상은 b.CREATED_DATE 원본 값이 아니라, DATE_FORMAT() 함수를 적용한 결과이다.
즉, 실제 비교 대상은 다음 표현식이다.
DATE_FORMAT(b.CREATED_DATE, '%Y-%m')
따라서 일반적인 CREATED_DATE 컬럼 인덱스 기준으로는 날짜 컬럼 자체를 범위 비교하는 방식보다 인덱스 활용이 불리할 수 있다.
공식문서가 “DATE_FORMAT()을 WHERE 절에 쓰면 안 된다”고 직접 말하는 것은 아니다.
다만 B-tree 인덱스가 컬럼 비교 표현식에서 사용될 수 있다고 설명하므로, 날짜 컬럼을 함수로 변환해 비교하는 방식보다 컬럼 자체를 범위 비교하는 방식이 인덱스 구조와 더 잘 맞는다고 정리할 수 있다.
Oracle 공식문서의 Function-Based Index 부분에는 다음과 같은 설명이 있다.
A function-based index computes the value of an expression that involves one or more columns and stores it in the index.
해석하면 다음과 같다.
함수 기반 인덱스는 하나 이상의 컬럼이 포함된 표현식의 값을 계산해서 그 결과를 인덱스에 저장한다.
이 문장은 TO_CHAR(CREATED_DATE, 'YYYY-MM') 조건과 직접 연결된다.
일반적인 날짜 컬럼 인덱스가 다음처럼 있다고 하자.
CREATE INDEX idx_board_created_date
ON USED_GOODS_BOARD(CREATED_DATE);
이 인덱스는 CREATED_DATE 원본 값을 기준으로 만들어진다.
그런데 다음 조건은 CREATED_DATE 원본 값을 그대로 비교하지 않는다.
WHERE TO_CHAR(b.CREATED_DATE, 'YYYY-MM') = '2022-10'
이 조건의 비교 대상은 다음 함수 표현식이다.
TO_CHAR(b.CREATED_DATE, 'YYYY-MM')
즉, 컬럼 자체가 아니라 함수가 적용된 표현식의 결과를 비교한다.
Oracle 공식문서에서는 이런 표현식 값을 인덱스에 저장하는 구조를 Function-Based Index라고 설명한다. 따라서 TO_CHAR(CREATED_DATE, 'YYYY-MM') 같은 조건을 인덱스로 효율적으로 처리하려면 일반적인 CREATED_DATE 컬럼 인덱스가 아니라, 다음과 같은 함수 기반 인덱스를 고려할 수 있다.
CREATE INDEX idx_board_created_ym
ON USED_GOODS_BOARD(TO_CHAR(CREATED_DATE, 'YYYY-MM'));
또한 Oracle 공식문서에는 다음 설명도 있다.
A function-based index improves the performance of queries that use the index expression.
해석하면 다음과 같다.
함수 기반 인덱스는 해당 인덱스 표현식을 사용하는 쿼리의 성능을 향상시킨다.
즉, TO_CHAR(CREATED_DATE, 'YYYY-MM') 표현식을 자주 조건으로 사용한다면, 그 표현식 자체에 대한 함수 기반 인덱스를 만들었을 때 성능 개선을 기대할 수 있다는 의미이다.
하지만 함수 기반 인덱스에도 유지 비용이 있다. Oracle 공식문서에는 다음과 같은 설명도 있다.
Function-based indexes on columns that are frequently modified are expensive for the database to maintain.
해석하면 다음과 같다.
자주 수정되는 컬럼에 대한 함수 기반 인덱스는 데이터베이스가 유지 관리하기에 비용이 크다.
따라서 함수 기반 인덱스는 무조건 사용하는 것이 아니라, 조회 패턴과 데이터 변경 빈도를 함께 고려해야 한다.
결국 단순히 특정 월의 데이터를 조회하는 경우라면, TO_CHAR(CREATED_DATE, 'YYYY-MM')처럼 함수 표현식을 조건에 사용하는 방식보다, CREATED_DATE 컬럼 자체를 범위 조건으로 비교하는 방식이 더 단순하고 일반 컬럼 인덱스 구조와도 잘 맞는다고 볼 수 있다.
MySQL의 DATE_FORMAT()과 Oracle의 TO_CHAR()는 함수 이름과 포맷 문법이 다르지만 목적은 비슷하다.
둘 다 날짜 데이터를 원하는 문자열 형태로 변환할 때 사용한다.
| 구분 | MySQL | Oracle |
|---|---|---|
| 날짜를 문자열로 변환 | DATE_FORMAT() | TO_CHAR() |
| 연-월-일 출력 | DATE_FORMAT(date, '%Y-%m-%d') | TO_CHAR(date, 'YYYY-MM-DD') |
| 연-월 조건 비교 | DATE_FORMAT(date, '%Y-%m') = '2022-10' | TO_CHAR(date, 'YYYY-MM') = '2022-10' |
예를 들어 댓글 작성일을 2022-10-02 형식으로 출력해야 한다면 다음처럼 작성한다.
DATE_FORMAT(r.CREATED_DATE, '%Y-%m-%d') AS CREATED_DATE
TO_CHAR(r.CREATED_DATE, 'YYYY-MM-DD') AS CREATED_DATE
출력 형식을 맞추는 목적이라면 이런 함수 사용이 자연스럽다.
하지만 WHERE 절에서 특정 기간의 데이터를 필터링할 때는 함수 사용 여부를 조금 더 신중하게 생각해야 한다.
다음 두 조건을 비교해보자.
WHERE DATE_FORMAT(b.CREATED_DATE, '%Y-%m') = '2022-10'
WHERE TO_CHAR(b.CREATED_DATE, 'YYYY-MM') = '2022-10'
이 조건들은 DB 입장에서 다음과 같이 처리될 수 있다.
1. CREATED_DATE 값을 가져온다.
2. 날짜 값을 문자열 형식으로 변환한다.
3. 변환된 결과가 '2022-10'인지 비교한다.
4. 조건에 맞는 행만 반환한다.
문제는 비교 대상이 CREATED_DATE 컬럼 자체가 아니라 함수로 변환된 결과라는 점이다.
반면 다음 조건은 컬럼 자체를 비교한다.
WHERE b.CREATED_DATE >= '2022-10-01'
AND b.CREATED_DATE < '2022-11-01'
WHERE b.CREATED_DATE >= TO_DATE('2022-10-01', 'YYYY-MM-DD')
AND b.CREATED_DATE < TO_DATE('2022-11-01', 'YYYY-MM-DD')
이 조건은 CREATED_DATE 컬럼 값을 그대로 두고 범위만 비교한다.
따라서 일반적인 날짜 컬럼 인덱스가 있다면, 함수로 변환한 결과를 비교하는 방식보다 인덱스 활용 측면에서 더 적합하다고 볼 수 있다.
< 다음 달 1일을 사용하는 이유날짜 범위 조회에서 다음처럼 BETWEEN을 사용할 수도 있다.
WHERE b.CREATED_DATE BETWEEN '2022-10-01' AND '2022-10-31'
하지만 날짜 컬럼에 시간이 포함되어 있다면 문제가 생길 수 있다.
예를 들어 데이터가 다음처럼 저장되어 있다고 하자.
2022-10-31 15:30:00
2022-10-31 23:59:59
BETWEEN '2022-10-01' AND '2022-10-31'은 상황에 따라 다음 범위처럼 해석될 수 있다.
2022-10-01 00:00:00 ~ 2022-10-31 00:00:00
이 경우 2022-10-31 15:30:00이나 2022-10-31 23:59:59 같은 데이터가 제외될 수 있다.
그래서 날짜와 시간이 함께 저장되는 컬럼이라면 다음처럼 작성하는 편이 더 안전하다.
WHERE b.CREATED_DATE >= '2022-10-01'
AND b.CREATED_DATE < '2022-11-01'
WHERE b.CREATED_DATE >= TO_DATE('2022-10-01', 'YYYY-MM-DD')
AND b.CREATED_DATE < TO_DATE('2022-11-01', 'YYYY-MM-DD')
이렇게 작성하면 10월 31일의 모든 시간대가 포함된다.
프로그래머스의 “조건에 부합하는 중고거래 댓글 조회하기” 문제에서는 다음 조건을 잘 구분해야 한다.
조건: 2022년 10월에 작성된 게시글
출력: 댓글 작성일
정렬: 댓글 작성일 오름차순, 게시글 제목 오름차순
즉, 조건은 댓글 작성일이 아니라 게시글 작성일에 걸어야 한다.
게시글 작성일은 USED_GOODS_BOARD 테이블의 CREATED_DATE 컬럼이다.
따라서 조건은 b.CREATED_DATE에 작성해야 한다.
출력할 날짜는 댓글 작성일이므로 r.CREATED_DATE를 포맷팅한다.
정렬은 댓글 작성일과 게시글 제목 기준으로 한다.
ORDER BY r.CREATED_DATE ASC, b.TITLE ASC
SELECT
b.TITLE,
b.BOARD_ID,
r.REPLY_ID,
r.WRITER_ID,
r.CONTENTS,
DATE_FORMAT(r.CREATED_DATE, '%Y-%m-%d') AS CREATED_DATE
FROM USED_GOODS_BOARD b
JOIN USED_GOODS_REPLY r
ON b.BOARD_ID = r.BOARD_ID
WHERE b.CREATED_DATE >= '2022-10-01'
AND b.CREATED_DATE < '2022-11-01'
ORDER BY r.CREATED_DATE ASC, b.TITLE ASC;
MySQL에서는 날짜 출력 형식을 맞추기 위해 DATE_FORMAT()을 사용한다.
DATE_FORMAT(r.CREATED_DATE, '%Y-%m-%d') AS CREATED_DATE
하지만 조건에서는 DATE_FORMAT(b.CREATED_DATE, '%Y-%m')를 사용하지 않고, 날짜 컬럼 자체를 범위 비교했다.
WHERE b.CREATED_DATE >= '2022-10-01'
AND b.CREATED_DATE < '2022-11-01'
SELECT
b.TITLE,
b.BOARD_ID,
r.REPLY_ID,
r.WRITER_ID,
r.CONTENTS,
TO_CHAR(r.CREATED_DATE, 'YYYY-MM-DD') AS CREATED_DATE
FROM USED_GOODS_BOARD b
JOIN USED_GOODS_REPLY r
ON b.BOARD_ID = r.BOARD_ID
WHERE b.CREATED_DATE >= TO_DATE('2022-10-01', 'YYYY-MM-DD')
AND b.CREATED_DATE < TO_DATE('2022-11-01', 'YYYY-MM-DD')
ORDER BY r.CREATED_DATE ASC, b.TITLE ASC;
Oracle에서는 날짜 출력 형식을 맞추기 위해 TO_CHAR()를 사용한다.
TO_CHAR(r.CREATED_DATE, 'YYYY-MM-DD') AS CREATED_DATE
하지만 조건에서는 TO_CHAR(b.CREATED_DATE, 'YYYY-MM')를 사용하지 않고, 날짜 컬럼 자체를 범위 비교했다.
WHERE b.CREATED_DATE >= TO_DATE('2022-10-01', 'YYYY-MM-DD')
AND b.CREATED_DATE < TO_DATE('2022-11-01', 'YYYY-MM-DD')
MySQL에서는 날짜를 원하는 문자열 형식으로 출력할 때 DATE_FORMAT()을 사용한다.
Oracle에서는 날짜를 원하는 문자열 형식으로 출력할 때 TO_CHAR()를 사용한다.
출력 형식을 맞추는 목적이라면 두 함수 모두 자연스럽게 사용할 수 있다.
하지만 WHERE 절에서 특정 월 데이터를 필터링할 때는 날짜 컬럼에 함수를 적용하는 방식보다 날짜 컬럼 자체를 범위 조건으로 비교하는 방식이 인덱스 활용 측면에서 더 적합하다.
MySQL 공식문서에서는 B-tree 인덱스가 =, >, >=, <, <=, BETWEEN 연산자를 사용하는 컬럼 비교 표현식에서 사용될 수 있다고 설명한다.
따라서 다음 조건은 공식문서의 설명과 잘 맞는다.
WHERE CREATED_DATE >= '2022-10-01'
AND CREATED_DATE < '2022-11-01'
반면 다음 조건은 컬럼 자체가 아니라 함수로 변환한 결과를 비교한다.
WHERE DATE_FORMAT(CREATED_DATE, '%Y-%m') = '2022-10'
Oracle 공식문서에서는 Function-Based Index를 함수나 표현식의 계산 결과를 인덱스에 저장하는 구조라고 설명한다.
따라서 다음 조건은 일반 컬럼 인덱스보다는 함수 기반 인덱스와 연결되는 조건이다.
WHERE TO_CHAR(CREATED_DATE, 'YYYY-MM') = '2022-10'
이 조건을 인덱스로 효율적으로 처리하려면 다음과 같은 함수 기반 인덱스를 고려할 수 있다.
CREATE INDEX idx_board_created_ym
ON USED_GOODS_BOARD(TO_CHAR(CREATED_DATE, 'YYYY-MM'));
하지만 함수 기반 인덱스는 별도의 인덱스 설계이며, 자주 변경되는 컬럼에서는 유지 비용도 고려해야 한다.
따라서 특정 월 데이터를 조회할 때는 다음처럼 날짜 컬럼 자체를 범위 비교하는 방식이 더 단순하고, 일반적인 날짜 컬럼 인덱스 구조와도 잘 맞는다고 정리할 수 있다.
WHERE 날짜컬럼 >= '시작일'
AND 날짜컬럼 < '다음 기간 시작일'
WHERE 날짜컬럼 >= TO_DATE('시작일', 'YYYY-MM-DD')
AND 날짜컬럼 < TO_DATE('다음 기간 시작일', 'YYYY-MM-DD')
핵심은 다음과 같다.
DATE_FORMAT(), TO_CHAR()
- 날짜를 문자열로 변환할 때 사용한다.
- SELECT 절에서 출력 형식을 맞출 때 유용하다.
- WHERE 절에서 컬럼에 직접 적용하면 일반 컬럼 인덱스 활용에 불리할 수 있다.
날짜 범위 조건
- 컬럼 값을 그대로 비교한다.
- B-tree 인덱스의 범위 비교 구조와 잘 맞는다.
- 시간 값이 포함되어도 안전하다.
- 인덱스 활용 측면에서 더 적합하다.
결론적으로 공식문서가 “DATE_FORMAT()이나 TO_CHAR()를 WHERE 절에 쓰지 말라”고 직접 말하는 것은 아니다.
다만 MySQL 공식문서의 B-tree 인덱스 설명과 Oracle 공식문서의 Function-Based Index 설명을 함께 보면, 날짜 필터링 조건은 컬럼에 함수를 적용하는 방식보다 컬럼 자체를 범위 비교하는 방식이 인덱스 활용 측면에서 더 적합하다고 생각해 볼 수 있는 것 같다.