상황: 회원 M0035의 전체 주문 건수를 조회한다. 조건에 해당하는 주문은 2,150건이고, ord_no는 PK(NOT NULL), mbr_id에는 tr_ord_fk01 인덱스가 있다.
BAD SQL
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT COUNT(o.ord_no) AS order_cnt
FROM startdbpg.tr_ord o
WHERE o.mbr_id = 'M0035';
SQL
복사
실측 실행계획
Aggregate (actual time=9.731..9.732 rows=1 loops=1)
Buffers: shared hit=2044
-> Index Scan using tr_ord_fk01 on tr_ord o (actual time=0.068..9.394 rows=2150 loops=1)
Index Cond: ((mbr_id)::text = 'M0035'::text)
Buffers: shared hit=2044
Planning:
Buffers: shared hit=118 read=1
Planning Time: 2.624 ms
Execution Time: 9.961 ms
Plain Text
복사
•
COUNT(ord_no)는 해당 컬럼에서 NULL이 아닌 값의 개수를 계산한다.
•
이번 실습에서는 ord_no가 검색 인덱스에 포함되지 않아 Index Scan으로 테이블 접근이 발생했다. 실행 버퍼는 shared hit=2044였다.
GOOD SQL
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
SELECT COUNT(*) AS order_cnt
FROM startdbpg.tr_ord o
WHERE o.mbr_id = 'M0035';
SQL
복사
실측 실행계획
Aggregate (actual time=1.582..1.583 rows=1 loops=1)
Buffers: shared hit=72
-> Index Only Scan using tr_ord_fk01 on tr_ord o (actual time=0.681..1.388 rows=2150 loops=1)
Index Cond: (mbr_id = 'M0035'::text)
Heap Fetches: 67
Buffers: shared hit=72
Planning:
Buffers: shared hit=116
Planning Time: 0.552 ms
Execution Time: 1.727 ms
Plain Text
복사
•
COUNT(*)는 조건에 맞는 전체 행 수를 계산하므로 특정 컬럼값을 조회할 필요가 없다.
•
이번 실습에서는 Index Only Scan을 사용해 테이블 접근을 크게 줄였다. 실행 버퍼는 shared hit=72, Heap Fetches: 67이었다.
핵심
•
조건에 맞는 전체 행 수가 필요하다면 COUNT(*)를 사용하자.
•
이번에는 두 SQL 모두 2,150건을 반환했지만, COUNT(컬럼)과 COUNT(*)는 의미가 다르고 실행계획·I/O도 달라질 수 있다.
•
SQL이 요구하는 결과가 '전체 행 수'인지, '특정 컬럼의 NULL이 아닌 값 개수'인지 구분해 작성하는 습관이 중요하다.
참고
•
COUNT(컬럼)은 NULL을 제외한다. 이 예제의 ord_no는 PK이므로 두 결과가 같지만, NULL 허용 컬럼에서는 서로 다를 수 있다.
•
PostgreSQL의 Index Only Scan도 Visibility Map 상태에 따라 Heap Fetches가 발생한다.
•
Oracle에서는 NOT NULL 컬럼의 COUNT(컬럼)을 COUNT(*)와 동등하게 최적화하는 Count Transformation이 적용될 수 있다. 이 PostgreSQL 실측 결과를 다른 DBMS에 그대로 일반화하지 않는다.
•
실측 실행시간은 BAD 9.961ms, GOOD 1.727ms였다. 단일 측정값이므로 일정한 성능 향상 비율을 보장하지 않는다.


