상황: 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인 점과 업무상 날짜·타임존 정의를 확인한다.


