Skip to content
튜닝 연습파싱과 바인드 변수
PARSE-035

상품 리뷰 탭을 누르면 0.6초, DB가 그 SQL에 쓴 시간은 0.01초다

중급SELECT이커머스580 / 700

이 문제는 교재 18화 졸업 과제와 같은 이커머스 데이터베이스를 씁니다. 『튜너의 1년』을 순서대로 읽고 있다면 졸업 과제를 먼저 풀고 오세요. 아래 「테이블과 인덱스」 칸에 과제의 답 일부(제약 조건, 인덱스, 통계의 실제 상태)가 나옵니다.

상황 ​

이커머스 상품 상세 화면의 「리뷰」 탭은 그 상품의 최근 리뷰 50개를 보여 준다. 리뷰마다 본문 앞 100자를 보여 주고, '더 보기'를 누르면 그 리뷰의 본문 전체를 펼친다. 지난달 API 서버를 클라우드로 옮긴 뒤로 리뷰 탭 응답이 0.6초 안팎이다. DB는 사내 IDC에 남았다. APM에는 DB 구간이 0.6초로 찍히는데, DBA가 본 그 SQL의 실행 시간은 0.01초다.

테이블과 인덱스 ​

  • reviews: 15,000행 · 775블록 · content CLOB(평균 380자, 대부분 행 안에 저장된다) · 상품당 34 ~ 76건
  • 인덱스 IX_REV_PROD_DT (product_id, created_at, review_id)(SORT-002에서 만든 인덱스) · 통계 최신
  • API 서버와 DB 사이의 왕복은 약 5ms다. JDBC fetch size는 기본값(10)이고, 드라이버 설정에서 LOB 미리 가져오기(LOB prefetch)는 꺼 두었다(2021년 메모리 문제 때 끔).

원본 SQL ​

sql
// ReviewApi.java : 최근 리뷰 50개 · 화면에는 본문 앞 100자만 쓴다
String sql = "SELECT review_id, customer_id, rating, created_at, content "
           + "FROM (SELECT review_id, customer_id, rating, created_at, content "
           + "        FROM reviews WHERE product_id = ? "
           + "       ORDER BY created_at DESC, review_id DESC) "
           + "WHERE ROWNUM <= 50";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setLong(1, productId);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
    Clob c = rs.getClob("content");
    String body = c.getSubString(1, (int) c.length());
    item.setPreview(body.length() > 100 ? body.substring(0, 100) + "…" : body);
}

실행계획 학습용 예시 ​

xplan
[10046 트레이스 · 리뷰 탭 한 번 (상품 1042 · 리뷰 76개 중 50개)]
  PARSE 1 · EXEC 1 · FETCH 5 (r=10 씩)
  LOBGETLEN  50번   (c.length())
  LOBREAD    50번   (c.getSubString())
  WAIT 'SQL*Net message from client'   105번 · 합계 0.57 초 (평균 5.4 ms)
  DB 안에서 쓴 시간 (EXEC + FETCH + LOB 호출 합계)   0.012 초

[세션 통계 · 같은 한 번]
  SQL*Net roundtrips to/from client      106
  lob reads                               50
  session logical reads                  153

[DISPLAY_CURSOR · 같은 실행]
------------------------------------------------------------------------------------------------------------------
| Id  | Operation                       | Name           | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |                |      1 |        |     50 |00:00:00.01 |      53 |
|*  1 |  COUNT STOPKEY                  |                |      1 |        |     50 |00:00:00.01 |      53 |
|   2 |   VIEW                          |                |      1 |     54 |     50 |00:00:00.01 |      53 |
|   3 |    TABLE ACCESS BY INDEX ROWID  | REVIEWS        |      1 |     54 |     50 |00:00:00.01 |      53 |
|*  4 |     INDEX RANGE SCAN DESCENDING | IX_REV_PROD_DT |      1 |     54 |     50 |00:00:00.01 |       3 |
------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter(ROWNUM<=50)
   4 - access("PRODUCT_ID"=:1)

문제 ​

  1. SQL은 0.01초인데 리뷰 탭이 0.6초 걸리는 이유를 트레이스와 세션 통계로 설명하시오. 왕복 106번이 어디서 나오는지 나눠 쓰시오.
  2. 화면이 바뀌지 않게(본문 앞 100자, '더 보기'는 전체) SQL과 코드를 고치고, 고친 뒤의 왕복 수를 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자