Search

REMINDER 03-01. WHERE에서 인덱스 컬럼을 함수로 가공하지 말자

상황: 2023년 2월 1일에 주문된 데이터 건수를 조회한다. Supabase startdbpg.tr_ord에는 약 73만 건이 있으며, ord_dtm에 B-tree 인덱스 tr_ord_x51이 있다. 실제 조회 결과는 604건이다.

BAD SQL — 인덱스 컬럼에 TO_CHAR 적용

EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT COUNT(*) AS ord_cnt FROM startdbpg.tr_ord o WHERE TO_CHAR(o.ord_dtm, 'YYYYMMDD') = '20230201';
SQL
복사
실측 실행계획 (BAD)
Finalize Aggregate (actual time=325.510..334.002 rows=1 loops=1) Buffers: shared hit=1970 -> Gather (actual time=325.372..333.994 rows=2 loops=1) Workers Planned: 1 Workers Launched: 1 Buffers: shared hit=1970 -> Partial Aggregate (actual time=320.191..320.192 rows=1 loops=2) Buffers: shared hit=1970 -> Parallel Index Only Scan using tr_ord_x51 on tr_ord o (actual time=139.968..320.152 rows=302 loops=2) Filter: (to_char(ord_dtm, 'YYYYMMDD'::text) = '20230201'::text) Rows Removed by Filter: 364862 Heap Fetches: 0 Buffers: shared hit=1970 Planning: Buffers: shared hit=128 Planning Time: 0.620 ms Execution Time: 334.152 ms
Plain Text
복사

GOOD SQL — 원본 TIMESTAMP 컬럼의 범위 조건

EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT COUNT(*) AS ord_cnt FROM startdbpg.tr_ord o WHERE o.ord_dtm >= TIMESTAMP '2023-02-01 00:00:00' AND o.ord_dtm < TIMESTAMP '2023-02-02 00:00:00';
SQL
복사
실측 실행계획 (GOOD)
Aggregate (actual time=0.255..0.256 rows=1 loops=1) Buffers: shared hit=5 -> Index Only Scan using tr_ord_x51 on tr_ord o (actual time=0.081..0.203 rows=604 loops=1) Index Cond: ((ord_dtm >= '2023-02-01 00:00:00'::timestamp without time zone) AND (ord_dtm < '2023-02-02 00:00:00'::timestamp without time zone)) Heap Fetches: 0 Buffers: shared hit=5 Planning: Buffers: shared hit=127 Planning Time: 0.835 ms Execution Time: 0.373 ms
Plain Text
복사
실측 비교: 두 SQL 모두 604건을 집계한다. BAD도 tr_ord_x51의 Parallel Index Only Scan을 사용했지만, TO_CHAR(ord_dtm) 조건이 Index Cond가 아닌 Filter로 남아 인덱스를 광범위하게 읽었다. Rows Removed by Filter: 364862는 병렬 작업 1회당 평균 값(loops=2)으로, 전체 제거 행 수를 364,862건으로 해석하면 안 된다. 실행 버퍼는 BAD shared hit=1970, GOOD shared hit=5였다. 실행시간은 BAD 334.152ms, GOOD 0.373ms로 측정됐으나 단일 실행 결과이며 환경에 따라 달라질 수 있다.

핵심

•
인덱스 컬럼을 함수로 가공하지 말고 원본 컬럼으로 검색 범위를 표현하자. 노드 이름이 Index Only Scan이어도 효율적인 탐색인지 판단하려면 Index Cond와 Filter를 구분해야 한다.
💡
참고·예외
•
BAD가 반드시 Seq Scan이 되는 것은 아니다. TO_CHAR와 같은 표현식을 지원하도록 별도로 설계한 검색 구조가 있다면 계획이 달라질 수 있다. 날짜 검색은 종료일 미만의 반열린 구간을 사용하며, 실제 ord_dtm이 timestamp without time zone인 점과 업무상 날짜·타임존 정의를 확인한다.