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

본사 주문 조회는 한 쪽에 20건인데, 쪽을 넘길 때마다 2.7초가 걸리고 WAS가 GC로 멈칫한다

중급SELECT버거587 / 700

상황 ​

버거 본사 운영팀의 「주문 조회」 화면은 기간(최대 31일)을 넣으면 최근 주문부터 한 쪽에 20건씩 보여 주고, 목록 위에 「총 n건」을 띄운다. 2016년에 만든 화면으로, 쪽 번호를 누를 때마다 DAO가 다시 불린다. 지난달 운영팀이 40명으로 늘자 쪽을 넘길 때마다 2~3초가 걸리고 화면 서버(WAS)가 Full GC로 멈칫한다는 신고가 잦아졌다. DBA는 "그 SQL은 인덱스를 잘 타고 실행당 DB 시간이 0.2초예요. WAS 쪽 문제 아닌가요?"라고 한다.

테이블과 인덱스 ​

  • ORD 주문: 1,000,000행 · 22,422블록 · ORD_NO · ORD_DT VARCHAR2(8) · ORD_HMS VARCHAR2(6) · SHOP_NO · 날짜순 적재 · 2013년 11월 41,666건(테이블 약 934블록)
  • 인덱스 ORD_PK (ORD_NO) · ORD_X01 (ORD_DT, ORD_HMS): BLEVEL 2 · 리프 약 3,570블록(11월은 약 149블록) · CF 22,223 · 통계 최신
  • WAS와 DB는 같은 IDC에 있다(왕복 약 0.45ms). JDBC fetch size는 드라이버 기본값 10이다.

원본 SQL ​

sql
// OrderSearchDao.java (2016) : 쪽 번호를 받아 그 쪽의 20건과 총건수를 함께 돌려준다
public Page<OrderRow> search(String from, String to, int page) throws SQLException {
    String sql = "SELECT ORD_NO, ORD_DT, ORD_HMS, SHOP_NO FROM ORD "
               + "WHERE ORD_DT BETWEEN ? AND ? "
               + "ORDER BY ORD_DT DESC, ORD_HMS DESC";
    try (PreparedStatement ps = conn.prepareStatement(sql,
             ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)) {
        ps.setString(1, from);
        ps.setString(2, to);
        ResultSet rs = ps.executeQuery();
        rs.last();                                  // 맨 끝 행으로 가서
        int total = rs.getRow();                    // 그 행 번호가 총건수
        List<OrderRow> rows = new ArrayList<>();
        if (rs.absolute((page - 1) * 20 + 1)) {     // 그 쪽의 첫 행으로 돌아와 20건
            do { rows.add(map(rs)); } while (rows.size() < 20 && rs.next());
        }
        return new Page<>(rows, total);
    }
}

실행계획 학습용 예시 ​

xplan
[1쪽 한 번 · 2013-11-01 ~ 2013-11-30 · 세션 통계 증가분 (V$SESSTAT)]
  화면 응답 (APM)                           2.71 초
  execute count                                1
  SQL*Net roundtrips to/from client        4,169
  bytes sent via SQL*Net to client          2.9 MB
  session logical reads                    9,425
  DB time                                   0.21 초
  SQL*Net message from client              4,168번 · 1.88 초 (평균 0.45 ms)

[V$SQL · 같은 실행]  EXECUTIONS 1 · FETCHES 4,167 · ROWS_PROCESSED 41,666 · BUFFER_GETS 9,419
[이어서 37쪽을 누른 실행]  위와 같다 (ROWS_PROCESSED 41,666 · roundtrips 4,169 · 2.69 초)

[WAS · 지난달 대비 · 운영팀 40명]
  힙 사용량 1.2 GB → 3.4 GB · Full GC 시간당 2번 → 31번 (한 번에 1~2초 멈춤)
  힙 덤프 상위 : JDBC 드라이버가 ResultSet마다 쌓아 둔 행 캐시 (요청 하나에 41,666행, 약 9 MB)

[DISPLAY_CURSOR · 같은 실행 (모든 페치를 합친 값)]
-----------------------------------------------------------------------------------------------
| Id  | Operation                      | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |         |      1 |        |  41666 |00:00:00.19 |    9419 |
|*  1 |  FILTER                        |         |      1 |        |  41666 |00:00:00.19 |    9419 |
|   2 |   TABLE ACCESS BY INDEX ROWID  | ORD     |      1 |  41100 |  41666 |00:00:00.18 |    9419 |
|*  3 |    INDEX RANGE SCAN DESCENDING | ORD_X01 |      1 |  41100 |  41666 |00:00:00.03 |    4318 |
-----------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter(:1<=:2)
   3 - access("ORD_DT">=:1 AND "ORD_DT"<=:2)

문제 ​

  1. 화면은 20건만 그리는데 한 번에 41,666행과 왕복 4,169번이 오가는 이유를 위 코드와 숫자로 설명하시오. WAS 힙이 차오르는 이유도 쓰시오.
  2. 화면(그 쪽의 20건과 총건수)이 바뀌지 않게 DAO와 SQL을 고치고, 고친 뒤의 왕복 수와 블록 수를 쓰시오. 총건수를 어떻게 셀지와 그 대가도 쓰시오.
원본 SQL을 고쳐 쓰세요. 인덱스 DDL과 힌트도 쓸 수 있습니다.
무엇이 문제였고 왜 이렇게 고쳤는지 쓰세요.0자