주문 테이블(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
해결은 캐스팅을 빼고 범위 조건으로 바꾸는 겁니다.
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까지 얽혀서 저는 그냥 범위 조건을 권합니다.
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' 로 바꾸는 방법도 있긴 한데, 커넥션 풀 쓰면 세션이 돌려쓰기 돼서 예측이 어렵습니다. 저는 쿼리에서 경계에 오프셋 박는 쪽이 사고가 없어서 그렇게 써요.