본문 바로가기
Q&A해결2026년 7월 12일2분 읽기

created_at으로 날짜 조회하는데 인덱스 놔두고 seq scan을 타네요

i
idx_seunghyun
조회 1094댓글 4

주문 테이블(orders, 800만건 좀 넘어요)에서 특정 날짜 주문만 뽑는 목록 쿼리가 어느 순간부터 확 느려져서 봤더니 인덱스를 안 타고 있더라고요. created_at 컬럼에 btree 인덱스 걸어둔 지는 한참 됐는데도요.

쿼리는 대충 이런 식입니다.

SELECT * FROM orders
WHERE created_at::date = '2026-07-01'
ORDER BY id DESC
LIMIT 50;

EXPLAIN 찍어보면 이렇게 나와요.

Seq Scan on orders  (cost=0.00..241832.10 rows=39000 width=220)
  Filter: ((created_at)::date = '2026-07-01'::date)
  Rows Removed by Filter: 7960000

인덱스는 분명히 idx_orders_created_at 로 있고, 혹시나 해서 ANALYZE도 다시 돌렸는데 그대로예요. created_at으로 범위 조건(>=, <)을 주면 인덱스를 잘 타는데 저렇게 날짜로 비교만 하면 계속 seq scan이라... 제가 뭘 놓친 걸까요?

댓글 4

김김영삼3개월 전
원인은 인덱스가 아니라 조건식이 인덱스를 못 쓰게 생긴 거예요. created_at::date 처럼 컬럼에 캐스팅이나 함수를 씌우면 그건 더 이상 created_at 값이 아니라 '가공된 값'이라, created_at 자체에 걸린 인덱스로는 매칭이 안 됩니다. 그래서 옵티마이저가 전 범위를 훑는 seq scan으로 떨어진 거예요.

해결은 캐스팅을 빼고 범위 조건으로 바꾸는 겁니다.

WHERE created_at >= '2026-07-01' AND created_at < '2026-07-02'

이러면 컬럼을 가공하지 않으니 기존 idx_orders_created_at 를 그대로 탑니다. 하루치를 뽑을 때 ::date가 편한 건 아는데, 인덱스 관점에선 '컬럼 = 값' 또는 '컬럼 >= a AND 컬럼 < b' 형태(sargable 하다고들 하죠)를 유지해야 해요.

정 표현식을 써야 하면 CREATE INDEX ON orders ((created_at::date)) 로 표현식 인덱스를 따로 만들 수도 있는데, 인덱스가 하나 더 늘고 timezone까지 얽혀서 저는 그냥 범위 조건을 권합니다.
iidx_seunghyun(guest)3개월 전
오 바로 인덱스 타네요 감사합니다. 근데 한 가지 걸리는 게, created_at이 timestamptz 타입이고 저희는 KST 기준이거든요. 지금처럼 '2026-07-01' 이렇게 날짜만 넣으면 UTC 자정 기준으로 잘리는 거 아닌가요? 그럼 한국 날짜랑 9시간 어긋날 것 같은데 이건 어떻게 맞추나요?
김김영삼3개월 전
정확히 보셨어요. 문자열 '2026-07-01'을 timestamptz랑 비교하면 세션 TimeZone 설정에 따라 해석돼서, 세션이 UTC면 UTC 자정이 경계가 됩니다. KST 하루를 원하면 경계 값에 타임존을 명시해주면 돼요.

WHERE created_at >= '2026-07-01 00:00:00+09' AND created_at < '2026-07-02 00:00:00+09'

이러면 한국시간 7월 1일 0시부터 2일 0시 직전까지가 딱 잡히고, 여전히 컬럼은 가공 안 했으니 인덱스도 그대로 탑니다. 앱에서 파라미터 넘길 때도 KST 자정을 UTC로 환산한 값을 주면 같은 결과고요.

세션 타임존을 SET TIME ZONE 'Asia/Seoul' 로 바꾸는 방법도 있긴 한데, 커넥션 풀 쓰면 세션이 돌려쓰기 돼서 예측이 어렵습니다. 저는 쿼리에서 경계에 오프셋 박는 쪽이 사고가 없어서 그렇게 써요.
iidx_seunghyun(guest)3개월 전
아 타임존 명시하니까 딱 맞아떨어지네요. 그동안 ::date로 대충 쓰던 게 인덱스도 못 타고 날짜도 애매했던 거였군요. 덕분에 확실히 정리됐습니다 🙏
Ctrl+Enter로 등록