테마
이 문제는 교재 18화 졸업 과제와 같은 이커머스 데이터베이스를 씁니다. 『튜너의 1년』을 순서대로 읽고 있다면 졸업 과제를 먼저 풀고 오세요. 아래 「테이블과 인덱스」 칸에 과제의 답 일부(제약 조건, 인덱스, 통계의 실제 상태)가 나옵니다.
상품 목록의 카테고리 페이지에는 그 분류의 상품을 배송 완료 기준 판매량 순으로 보여 주는 「이 분류 인기 상품」 칸이 있다. 카테고리를 열 때마다 돌아서 하루 2만 번 호출된다.
개발자는 주문 → 주문상품 → 상품 순서로 해시 조인을 고정해 두었다. 읽는 블록이 491개뿐이라 아무도 의심하지 않았는데, AWR을 보니 이 SQL 하나가 DB CPU의 6%를 쓴다. 아래는 상품 6개가 있는 소분류(category_id = 23)를 연 실행계획이다.
orders: 34,908행 · 193블록 · PK만 있음 · status 6종, 배송 완료('DELIVERED') 24,436건(70%)order_items: 73,498행 · 290블록 · PK만 있음(order_id, product_id에 인덱스 없음) · 소분류 23의 상품 6개가 들어간 줄 1,577줄, 그중 배송 완료 주문의 줄 1,104줄products: 280행 · 8블록 · PK만 있음 · 소분류 하나에 상품 5 ~ 7개SELECT /*+ LEADING(o i p) USE_HASH(i p) */
p.product_id, p.name,
COUNT(DISTINCT o.order_id) AS ord_cnt, SUM(i.quantity) AS qty
FROM orders o, order_items i, products p
WHERE o.status = 'DELIVERED'
AND i.order_id = o.order_id
AND p.product_id = i.product_id
AND p.category_id = :cat
GROUP BY p.product_id, p.name
ORDER BY qty DESC
-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Used-Mem |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 6 |00:00:00.09 | 491 | |
| 1 | SORT ORDER BY | | 1 | 6 | 6 |00:00:00.09 | 491 | 2048 (0) |
| 2 | HASH GROUP BY | | 1 | 6 | 6 |00:00:00.09 | 491 | 1260K (0) |
|* 3 | HASH JOIN | | 1 | 1103 | 1104 |00:00:00.09 | 491 | 1452K (0) |
|* 4 | TABLE ACCESS FULL | PRODUCTS | 1 | 6 | 6 |00:00:00.01 | 8 | |
|* 5 | HASH JOIN | | 1 | 51449 | 51463 |00:00:00.07 | 483 | 2734K (0) |
|* 6 | TABLE ACCESS FULL | ORDERS | 1 | 24436 | 24436 |00:00:00.01 | 193 | |
| 7 | TABLE ACCESS FULL | ORDER_ITEMS | 1 | 73498 | 73498 |00:00:00.02 | 290 | |
-------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("P"."PRODUCT_ID"="I"."PRODUCT_ID")
4 - filter("P"."CATEGORY_ID"=:CAT)
5 - access("I"."ORDER_ID"="O"."ORDER_ID")
6 - filter("O"."STATUS"='DELIVERED')