Search

REMINDER 09-03. 마지막 한 행은 동률까지 고려해 선택하자

상황: 회원의 가장 최근 주문 한 건에 대한 주문번호·주문일시·주문금액을 조회한다. 실제 일반 테이블 startdbpg.tr_ord의 회원 M0045를 선정했다. 이 회원은 주문 140건이 있고, 가장 최근 주문일시 2025-01-17 07:02:00이 같은 주문이 2건 존재한다.

BAD SQL — MAX 주문일시와 같은 행을 모두 조회

SELECT o.ord_no, o.ord_dtm, o.ord_amt FROM startdbpg.tr_ord o WHERE o.mbr_id = 'M0045' AND o.ord_dtm = ( SELECT MAX(x.ord_dtm) FROM startdbpg.tr_ord x WHERE x.mbr_id = o.mbr_id ) ORDER BY o.ord_no DESC;
SQL
복사
MAX(ord_dtm)는 가장 최근 시각만 결정한다. 그 시각에 주문이 2건이면 2행 모두 반환하므로, 한 건만 필요한 업무 요건을 충족하지 못한다.

GOOD SQL — 최신 일시로 정렬한 후 주문번호로 동률을 해소

SELECT o.ord_no, o.ord_dtm, o.ord_amt FROM startdbpg.tr_ord o WHERE o.mbr_id = 'M0045' ORDER BY o.ord_dtm DESC, o.ord_no DESC LIMIT 1;
SQL
복사
ord_dtm DESC로 최신 시각부터 조회하고, 같은 시각의 주문 중 ord_no가 큰 주문 1건을 선택한다. ord_no는 PK라서 정렬 순서가 유일하게 결정된다. 요청에 따라 NULLS LAST는 사용하지 않았다. 현재 실습 데이터에는 ord_dtm IS NULL인 행이 0건이다.
실제 결과 비교
구분
주문번호(ord_no)
주문일시(ord_dtm)
주문금액(ord_amt)
BAD 첫 행
725809
2025-01-17 07:02:00
11,300
BAD 두 번째 행
725781
2025-01-17 07:02:00
4,500
GOOD 한 행
725809
2025-01-17 07:02:00
11,300
실제 실행계획 검증 — EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
항목
BAD
GOOD
결과 행 수
2
1
주요 방식
상관 MAX SubPlan 반복 + Index Scan
Index Scan Backward → Incremental Sort → Limit
SubPlan 실행 횟수
140회
없음
Buffers: shared hit
518
14
실행시간 (각 1회)
2.517ms
0.223ms

BAD SQL — 실제 실행계획 (핵심 발췌)

Sort (actual time=2.385..2.386 rows=2 loops=1) Sort Key: o.ord_no DESC Buffers: shared hit=518 -> Index Scan using tr_ord_fk01 on tr_ord o (actual time=2.340..2.353 rows=2 loops=1) Index Cond: ((mbr_id)::text = 'M0045'::text) Filter: (ord_dtm = (SubPlan 2)) Rows Removed by Filter: 138 SubPlan 2 -> Result (actual time=0.011..0.011 rows=1 loops=140) InitPlan 1 -> Limit (actual time=0.010..0.011 rows=1 loops=140) -> Index Only Scan Backward using tr_ord_x54 on tr_ord x (actual time=0.010..0.010 rows=1 loops=140) Heap Fetches: 0 Planning Time: 1.039 ms Execution Time: 2.517 ms
Plain Text
복사

GOOD SQL — 실제 실행계획 (핵심 발췌)

Limit (actual time=0.135..0.136 rows=1 loops=1) Buffers: shared hit=14 -> Incremental Sort (actual time=0.134..0.135 rows=1 loops=1) Sort Key: ord_dtm DESC, ord_no DESC Presorted Key: ord_dtm Full-sort Groups: 1 Sort Method: quicksort Peak Memory: 25kB -> Index Scan Backward using tr_ord_x54 on tr_ord o (actual time=0.063..0.076 rows=3 loops=1) Index Cond: ((mbr_id)::text = 'M0045'::text) Planning Time: 0.850 ms Execution Time: 0.223 ms
Plain Text
복사
실행계획 해석: 09-02에서 실제 생성한 복합 인덱스 tr_ord_x54(mbr_id, ord_dtm)를 GOOD에서 사용했다. 인덱스를 역방향으로 조회하되, ord_no가 인덱스 키에 없으므로 같은 주문일시 안에서는 Incremental Sort로 동률을 정렬한다. 실제 스캔은 3행, 결과는 1행이었다. BAD에서는 상관 MAX가 140회 반복됐다. 실행시간은 각각 1회 실측값이며 데이터·캐시·계획에 따라 달라진다.

핵심

•
가장 최근 시각의 MAX만으로 가장 최근 주문 한 건이 결정되는 것은 아니다. 한 건이 필요하면 업무상 동률 해소 기준(예: 주문번호)을 포함한 ORDER BY ... LIMIT 1을 사용하자.
💡
참고·예외
•
최신 주문일시의 동률 주문을 모두 반환해야 하는 업무라면 BAD의 여러 행은 오류가 아니며, LIMIT 1로 줄이면 결과가 달라진다.
•
주문번호 최댓값과 주문일시 최댓값은 같은 의미가 아니다. 먼저 업무상 최신의 기준을 정의한다.
•
이 예제의 ord_dtm은 실제 데이터에서 NULL이 없어서 NULLS LAST를 생략했다. PostgreSQL에서는 DESC 정렬의 기본 NULL 위치가 FIRST이므로, 향후 NULL이 허용·유입되면 별도 처리 여부를 결정해야 한다.
•
적절한 인덱스가 없을 때는 Sort 비용이 증가할 수 있다. 위 성능은 기존 tr_ord_x54 인덱스가 있는 현재 환경에서의 실측이다.