Search

REMINDER 08-03. WAS에서 자르지 말고 DB에서 페이징하자

상황: 사용자는 2023년 3월 주문 목록의 첫 30건만 확인한다. 실제 일반 테이블 startdbpg.tr_ord에서 주문일시 → 회원ID → 주문번호 순으로 조회한다. ord_no는 PK이므로 마지막 정렬 키로 두면 같은 주문일시·회원ID의 행까지 일관된 순서로 정렬할 수 있다.

BAD SQL — 전체 조회 후 애플리케이션(WAS)에서 앞 30건만 사용

SELECT o.ord_no, o.ord_dtm, o.shop_id, o.mbr_id, o.ord_amt FROM startdbpg.tr_ord o WHERE o.ord_dtm >= TIMESTAMP '2023-03-01' AND o.ord_dtm < TIMESTAMP '2023-04-01' ORDER BY o.ord_dtm, o.mbr_id, o.ord_no;
SQL
복사
WAS가 결과를 전부 받아서 앞 30건만 보여준다면, 사용하지 않을 17,552건까지 DB에서 생성하고 전송·수신·처리해야 한다. 이 SQL은 전체 17,582건을 조회한다.

GOOD SQL — DB에서 필요한 페이지의 30건만 반환

SELECT o.ord_no, o.ord_dtm, o.shop_id, o.mbr_id, o.ord_amt FROM startdbpg.tr_ord o WHERE o.ord_dtm >= TIMESTAMP '2023-03-01' AND o.ord_dtm < TIMESTAMP '2023-04-01' ORDER BY o.ord_dtm, o.mbr_id, o.ord_no LIMIT 30;
SQL
복사
정렬 기준은 BAD와 완전히 동일하고, DB가 첫 30건만 반환한다. 현재 tr_ord_x51(ord_dtm) 인덱스를 사용한 실제 실행계획에서는 전체 17,582건이 아니라 40행만 읽고 Incremental Sort를 수행한 뒤 30건을 반환했다.
실제 데이터·결과 검증
•
해당 기간 전체 주문: 17,582건
•
BAD SQL 최종 반환 행: 17,582건
•
GOOD SQL 최종 반환 행: 30건
•
BAD 전체 결과를 동일한 ORDER BY로 순위 매긴 첫 30건과 GOOD 30건을 행별 순위까지 포함해 EXCEPT ALL 양방향 비교: 차이 0건
실제 실행계획 비교 (EXPLAIN (ANALYZE, BUFFERS, COSTS OFF))
항목
BAD: 전체 조회
GOOD: LIMIT 30
DB 반환 행 수
17,582
30
주문 Index Scan 읽은 행 수
17,582
40
Buffers: shared hit
274
17
실행시간(각 1회)
31.988ms
0.299ms
주요 노드
Index Scan → Incremental Sort
Index Scan → Incremental Sort → Limit

BAD SQL — 실제 실행계획

Incremental Sort (actual time=0.250..30.650 rows=17582 loops=1) Sort Key: ord_dtm, mbr_id, ord_no Presorted Key: ord_dtm Full-sort Groups: 334 Sort Method: quicksort Average Memory: 26kB Peak Memory: 26kB Pre-sorted Groups: 465 Sort Method: quicksort Average Memory: 25kB Peak Memory: 25kB Buffers: shared hit=274 -> Index Scan using tr_ord_x51 on tr_ord o (actual time=0.053..7.405 rows=17582 loops=1) Index Cond: ((ord_dtm >= '2023-03-01 00:00:00'::timestamp without time zone) AND (ord_dtm < '2023-04-01 00:00:00'::timestamp without time zone)) Buffers: shared hit=262 Planning: Buffers: shared hit=170 Planning Time: 0.909 ms Execution Time: 31.988 ms
Plain Text
복사

GOOD SQL — 실제 실행계획

Limit (actual time=0.186..0.193 rows=30 loops=1) Buffers: shared hit=17 -> Incremental Sort (actual time=0.185..0.188 rows=30 loops=1) Sort Key: ord_dtm, mbr_id, ord_no Presorted Key: ord_dtm Full-sort Groups: 1 Sort Method: quicksort Average Memory: 27kB Peak Memory: 27kB Buffers: shared hit=17 -> Index Scan using tr_ord_x51 on tr_ord o (actual time=0.051..0.069 rows=40 loops=1) Index Cond: ((ord_dtm >= '2023-03-01 00:00:00'::timestamp without time zone) AND (ord_dtm < '2023-04-01 00:00:00'::timestamp without time zone)) Buffers: shared hit=5 Planning: Buffers: shared hit=170 Planning Time: 0.861 ms Execution Time: 0.299 ms
Plain Text
복사
실행계획 읽기: LIMIT이 있다는 사실 자체가 무조건 적은 데이터 검색을 뜻하지는 않는다. 이번 사례에서는 실제로 Index Scan 40행 → Incremental Sort → Limit 30행이므로 필요한 페이지를 얻기 전에 많은 행을 읽지 않았음을 확인할 수 있다. 08-02처럼 인덱스와 ORDER BY의 관계가 중요하다.

핵심

•
화면에 30건만 필요하다면 데이터베이스에서 ORDER BY와 LIMIT으로 30건만 반환하자. 전체 결과를 WAS로 전송한 뒤 잘라내는 방식은 불필요한 DB 작업·네트워크 전송·애플리케이션 메모리 사용을 유발한다.
주의·범위: 여기의 실행시간은 EXPLAIN ANALYZE가 기록한 DB 서버 내부의 각 1회 측정값이며, 실제 네트워크 전송시간·WAS 객체 생성시간은 측정하지 않았다. LIMIT 30은 첫 페이지만 가져오는 예제다. 깊은 페이지의 OFFSET 비용은 08-04에서 별도로 다룬다. 사용자 화면에 일관된 순서가 필요하므로 ORDER BY와 유일한 동률 해소 키를 빠뜨리지 않는다.