본문 바로가기

SQLP 자격증

SQLP - 데이터베이스 Call과 네트워크 부하

@markdown


# 데이터베이스 Call과 네트워크 부하

- 서버 프로세스에게 작업을 지시하거나 결과집합을 요청할 때 모든 명령은 데이터베이스 Call을 통해 전달된다.


## 데이터베이스 Call 종류

____

### SQL 커서에 대한 작업 요청에 따른 구분

- 1) Parse Call : SQL 파싱을 요청하는 Call

- 2) Execute Call : SQL 실행을 요청하는 Call

- 3) Fetch Call : SELECT문의 결과 데이터 전송을 요청하는 Call


### Call 발생 위치에 따른 구분

#### 1) User Call

- DBMS 외부로부터 요청되는 Call

- User Call이 많은 Peek 시간대에 시스템 확장성을 떨어뜨리는 가장 큰 원인

- User Call을 줄이기 위한 기술요소

- ① Loop 쿼리를 해소하고 집합적 사고를 통해 One SQL로 구현

- ② Array Preocessing : Array 단위 Fetch, Bulk `Insert` / `Update` / `Delete`

- ③ 부분범위처리 원리 활용

- ④ 효과적인 화면 페이지 처리

- ⑤ `사용자 정의 함수` / `프로시저`/ `트리거`의 적절한 활용


#### 2) Recursive Call

- DBMS 내부에서 발생하는 Call

SQL 파싱과 최적화 과정에서 발생하는 데이터 딕셔너리 조회, 사용자 정의함수/프로시저 내에서의 SQL수행

Recursive Call 최소화방안

① 바인드 변수 적극 활용하여 하드파싱 발생횟수를 줄인다.

- ② 사용자 정의 함수와 프로시저를 내부적으로 어떻게 수행되는지 잘 이해하고 사용

</br>


## 데이터베이스 Call과 성능

____

### One SQL 구현의 중요성

- Loop를 돌면서 여러 작업을 반복 수행하는 프로그램을 One SQL로 구현했을 때 데이터베이스 Call 횟수를 줄여 성능 개선 할 수 있다.

DBMS 내에서 수행되는 사용자 정의 프로시저로 개발하면 네트워크 트래픽없는 Recursive Call만 발생하므로 빠르게 수행됨


#### Java Loop Query

<pre><code class="java" style="font-size:14px">public class JavaLoopQuery{ 

  public static void insertData( Connection con 

                               , String param1 

                               , String param2 

                               , String param3 

                               , long param4) throws Exception{ 

  String SQLStmt = "INSERT INTO 납입방법별_월요금집계 " 

          + "(고객번호, 납입월, 납입방법코드, 납입금액) " 

          + "VALUES(?, ?, ?, ?)"; 

  PreparedStatement st = con.prepareStatement(SQLStmt); 

  st.setString(1, param1); 

  st.setString(2, param2); 

  st.setString(3, param3); 

  st.setLong(4, param4); 

  st.execute(); 

  st.close(); 


public static void execute(Connection con, String input_month) throws Exception { 

  String SQLStmt = "SELECT 고객번호, 납입월, 지로, 자동이체, 신용카드, 핸드폰, 인터넷 " 

                 + "FROM 월요금납부실적 " 

                 + "WHERE 납입월 = ?"; 

  PreparedStatement stmt = con.prepareStatement(SQLStmt); 

  stmt.setString(1, input_month); 

  ResultSet rs = stmt.executeQuery(); 

  while(rs.next()){ 

    String 고객번호 = rs.getString(1); 

    String 납입월 = rs.getString(2); 

    long 지로 = rs.getLong(3); 

    long 자동이체 = rs.getLong(4); 

    long 신용카드 = rs.getLong(5); 

    long 핸드폰 = rs.getLong(6); 

    long 인터넷 = rs.getLong(7); 

    if(지로 > 0) insertData (con, 고객번호, 납입월, "A", 지로); 

    if(자동이체 > 0) insertData (con, 고객번호, 납입월, "B", 자동이체); 

    if(신용카드 > 0) insertData (con, 고객번호, 납입월, "C", 신용카드); 

    if(핸드폰 > 0) insertData (con, 고객번호, 납입월, "D", 핸드폰); 

    if(인터넷 > 0) insertData (con, 고객번호, 납입월, "E", 인터넷); 

 } 

  rs.close(); 

  stmt.close(); 

</code></pre>

- 처리해야할 납부실적이 10만건이라고 하면 Insert를 위한 Parse Call 50만번, Execute Call 50만번으로 최대 110만번의 데이터베이스 Call이 발생함

</br>


#### Java One SQL

<pre><code class="java" style="font-size:14px">public class JavaOneSQL{ 

  public static void execute(Connection con, String input_month) throws Exception { 

    String SQLStmt = "INSERT INTO 납입방법별_월요금집계" 

         + "(납입월,고객번호,납입방법코드,납입금액) " 

         + "SELECT x.납입월, x.고객번호, CHR(64 + Y.NO) 납입방법코드 " 

         + " , DECODE(Y.NO, 1, 지로, 2, 자동이체, 3, 신용카드, 4, 핸드폰, 5, 인터넷) " 

         + "FROM 월요금납부실적 x, (SELECT LEVEL NO FROM DUAL CONNECT BY LEVEL <= 5) y " 

         + "WHERE x.납입월 = ? " 

         + "AND y.NO IN ( DECODE(지로, 0, NULL, 1), DECODE(자동이체, 0, NULL, 2) " 

         + " , DECODE(신용카드, 0, NULL, 3) , DECODE(핸드폰, 0, NULL, 4) " 

         + " , DECODE(인터넷, 0, NULL, 5) )" ; 


    PreparedStatement stmt = con.prepareStatement(SQLStmt); 

    stmt.setString(1, input_month); 

    stmt.executeQuery(); stmt.close(); 

  } 

  static Connection getConnection() throws Exception { ...... } 

  static void releaseConnection(Connection con) throws Exception { ...... } 


  public static void main(String[] args) throws Exception{ 

    Connection con = getConnection(); 

    execute(con, "200903"); 

    releaseConnection(con); 

  } 

</code></pre>


- Loop Query를 One SQL로 SQL Statement에 기술하여 실행한다면 위의 최대 110만번 데이터베이스 Call을 단 2회(Parse Call 1회, Execute Call 1회)로 줄일 수 있다.

- 그 이유는 대부분 네트워크 구간에서 오랜 시간이 소비되거나 OS로부터 CPU와 메모리 리소스를 할당 받으려고 기다리면서 소비가 이루어지는데에 비해, 이 부담을 DBMS의 사용자 정의 프로시저로 구현하여 수행함으로써 성능개선 효과를 볼 수 있다.

</br>


### Array Processing 활용

- 데이터베이스 Call은 개별 프로그램의 수행속도와 시스템 전체의 확장성에 영향을 미치기 때문에 중요하다.

- Array Processing 기능을 활용하면 한 번의 SQL(INSERT?UPDATE/DELETE) 수행으로 다량의 레코드를 동시에 처리할 수 있다. 

- 네트워크 Call 감소, SQL 수행시간 감소, CPU 사용량 감소 효과를 볼 수 있다.


<pre><code class="java" style="font-size:14px">public static void execute(Connection con, String input_month) throws Exception { 

    long rows = 0; 

    String SQLStmt1 = "SELECT 고객번호, 납입월" 

        + ", 지로, 자동이체, 신용카드, 핸드폰, 인터넷 " 

        + "FROM 월요금납부실적 " 

        + "WHERE 납입월 = ?"; 

    String SQLStmt2 = "INSERT INTO 납입방법별_월요금집계 " 

        + "(고객번호, 납입월, 납입방법코드, 납입금액) " 

        + "VALUES(?, ?, ?, ?)"; 

    con.setAutoCommit(false); 

    PreparedStatement stmt1 = con.prepareStatement(SQLStmt1); 

    PreparedStatement stmt2 = con.prepareStatement(SQLStmt2); 

    stmt1.setFetchSize(1000); 

    stmt1.setString(1, input_month); 

    ResultSet rs = stmt1.executeQuery(); 


    while(rs.next()){ 

        String 고객번호 = rs.getString(1); 

        String 납입월 = rs.getString(2); 

        long 지로 = rs.getLong(3); 

        long 자동이체 = rs.getLong(4); 

        long 신용카드 = rs.getLong(5); 

        long 핸드폰 = rs.getLong(6); 

        long 인터넷 = rs.getLong(7); 


        if(지로 > 0) 

        insertData (con, stmt2, 고객번호, 납입월, "A", 지로); 

        if(자동이체 > 0);

        insertData (con, stmt2, 고객번호, 납입월, "B", 자동이체);

        if(신용카드 > 0) 

        insertData (con, stmt2, 고객번호, 납입월, "C", 신용카드); 

        if(핸드폰 > 0)

        insertData (con, stmt2, 고객번호, 납입월, "D", 핸드폰); 

        if(인터넷 > 0) 

        insertData (con, stmt2, 고객번호, 납입월, "E", 인터넷); 

        if(++rows%1000 == 0) stmt2.executeBatch(); 

    }

</code></pre>

- 위의 Java 코드는 INSERT할 데이터를 계속 Array에 담기만 하다가 1,000건 쌓일 때마다 한 번씩 executeBatch를 수행한다.

- SELECT 결과집합을 Fetch할 때도 1,000개 단위로 Fetch하도록 조정

- 대용량 데이터 처리를 위한 Array Processing 사용 효과를 극대화하려면 연속된 일련의 처리과정이 모두 Array 단위로 진행돼야 한다.

</br>



## Fetch Call 최소화

____

### 부분범위처리 원리

- 쿼리 결과 집합을 전송할 때, 전체 데이터를 연속적으로 전송하지 않고 사용자로부터 Fetch Call이 있을때마다 일정량씩 나누어서 전송하는 것

![](http://wiki.gurubee.net/download/attachments/26744690/call.jpg)

- SQL Server : 쿼리분석기 옵션에 설정되어있는 "네트워크 패키지 크기"로 운반 (default 4,096 바이트)

- ORACLE : 내부적으로는 SDU(Session Data Unit,Session 레이어), TDU(Transport Data Unit,Transport 레이어) 단위로 나누어서 전송

- array 사이즈를 작게 설정하면 하나의 네트워크 패킷에 담아 전송하겠지만, 크게 설정하면 여러개의 패킷으로 나누어 전송


<pre><code class="java" style="font-size:14px">set arraysize 100

call  count cpu   elapsed disk query current rows 

----- ----- ----- ------- ----- ----- ----- ------ 

Parse     1 0.00    0.00     0     0     0     0 

Execute   1 0.00    0.02     2     2     0     0 

Fetch   301 0.14    0.18     9   315     0 30000 

----- ----- ----- ------- ----- ----- ----- ------ 

total   303 0.14    0.20    11   317     0 30000 

</code></pre>

</br>


### ArraySize 조정에 의한 Fetch Call 감소 및 블록 I/O 감소 효과

- 대량 데이터 파일을 전송할 때  ArraySize를 크게하여 Fetch Call 횟수를 줄여주는 것이 유리

반대로 적은량의 데이터만 fetch 하다가 멈추는 프로그램이라면 arraysize를 작게 설정하는것이 유리

- ArraySize를 증가시키면 네트워크 부하감소 및 서버 프로세스가 읽어야할 블록 갯수 감소 효과

![](http://wiki.gurubee.net/download/attachments/26744690/fetch.jpg)

- 위의 그림 처럼 10개 행으로 수성된 3개의 블록가 총 30 레코드이므로 ArraySize를 3으로 설정하면 Fetch 횟수는 10, 블록 I/O는 12번이 발생하게 된다.

- 이때 ArraySize를 10으로 설정한다면 3번의 Fetch와 3번의 블록 I/O로 줄일 수 있고, 이때 ArraySize와 Fetch Count는 반비례 관계를 보인다.

![](http://wiki.gurubee.net/download/attachments/26744690/array.jpg)


#### Java Fetch 사이즈 조정 후 Fetch 매커니즘

<pre><code class="java" style="font-size:14px">String sql = "select custid, name from customer"; 

PreparedStatement stmt = conn.prepareStatement(sql); 

stmt.setFetchSize(100); -- Statement에서 조정 

ResultSet rs = stmt.executeQuery(); 

// rs.setFetchSize(100); -- ResultSet에서 조정할 수도 있다. 

while( rs.next() ) { 

  int empno = rs.getInt(1); 

  String ename = rs.getString(2); 

  System.out.println(empno + ":" + ename); 

rs.close(); 

stmt.close(); 

</code></pre>

- 1.최초 rs.next() 호출 시 한꺼번에 100건을 가져와서 클라이언트 Array 버퍼에 캐싱한다.

- 2.이후 rs.next() 호출할 때는 데이터베이스 Call을 발생시키지 않고 Array 버퍼에서 읽는다.

- 3.버퍼에 캐싱 돼 있던 데이터를 모두 소진한 후 101번째 rs.next() 호출 시 다시 100건을 가져온다.

- 4.모든 결과집합을 다 읽을 때까지 2~3번 과정을 반복한다.


</br>


## 페이지 처리 활용

____

### 페이지 처리를 하지 않았을 때

- 1.다량 발생하는 Fetch Call의 부하

- 2.대량의 결과 집합을 클라이언트에 전송하면서 발생하는 네트워크 부하

- 3.대량의 데이터 블록을 읽으면서 발생하는 I/O 부하

- 4 .AP 서버 및 웹 서버 리소스 사용량 증가


### 페이지 처리를 통한 부하해소

- 1. 페이지 단위로, 화면에서 필요한 만큼만 Fetch Call

- 2. 페이지 단위로, 화면에서 필요한 만큼만 네트워크를 통해 결과 전송

- 3. 인덱스와 부분범위처리 원리를 이용해 각 페이지에 필요한 최소량만 I/O

- 4. 데이터를 소량씩 나누어 전송하므로 AP웹 서버 리소스 사용량 최소화

- <b>결론 : 조회할 데이터가 일정량 이상이고 수행빈도가 높다면 필수적으로 페이지 처리를 해야한다.</b>

</br>


## 분산 쿼리

____


### 분산 DB 간 테이블을 조인할 경우

<pre><code class="SQL" style="font-size:14px">select channel_id, sum(quantity_sold) auantity_cold 

from order a, sales@lk_sales b 

where a.order_date between :1 and :2 

and b.order_no = a.order no 

group by channel_id 


Rows    Row Source Operation 

-----   --------------------------------------------- 

5 SORT  GROUP BY 

10981    NESTED LOOPS 

500000    REMOTE 

10981     TABLE ACCESS BY INDEX ROWID ORDER 

500000     INDEX UNIQUE SCAN (ORDER_PK) 

</code></pre>

- 원격에 있는 salses 테이블을 전송받아 order 테이블과 NL 방식으로 조인하고 있는 상황

- 50만 건이나 되는 sales 테이블의 데이터를 네트워크를 통해 전송받기 때문에 성능은 당연히 나쁨

</br>


### 분산 DB 간 성능저하 해결방안

<pre><code class="java" style="font-size:14px">select /*+ driving_site(b) */ channel_id, sum(quantity_sold) auantity_cold 

from order a, sales@lk_sales b 

where a.order_date between :1 and :2

 and b.order_no = a.order_no 

group by channel_id 

Rows   Row Source Operation

----   --------------------------------------------- 

5 SORT GROUP BY 

10981   NESTED LOOPS 

939      TABLE ACCESS (BY INDEX ROWID) OF 'ORDER' 

939       INDEX (RANGE SCAN) OF 'ORDER_IDX2' (NON-UNIQUE) 

10981    REMOTE 

</code></pre>


- 이를 해결하기 위해 sales 테이블보다 데이터가 적은 order 테이블의 order_date 필터조건에 해당하는 데이터만 원격으로 sales로 보내 조인과 group by를 거친 결과집합을 받으면 큰 성능 개선 효과를 볼 수 있다.

- 원격 서버가 쿼리를 처리하도록 driving_site 힌트를 지정할 수 있는 방법이 있다.

- 이를 통해 939건의 order 테이블의 데이터를 원격으로 보냈고, 거기서 처리가 완료된 5건만 전송 받은 것을 확인 할 수 있다.

- <b>분산 쿼리의 성능을 높이는 핵심 원리는 네트워크를 통한 데이터 전송량을 줄이는데 있다.</b>


</br>

## 사용자 정의 함수 / 프로시저의 특징과 성능

____

### 사용자 정의 함수 / 프로시저의 특징

- 가상머신과 같은 별도의 실행엔진을 통해 실행됨

- 실행시마다 Context Switching이 일어나므로, 내장함수를 호출할 때와 비교해 성능이 상당히 떨어진다. 


#### 문자타입의 일자 데이터를 날짜 타입으로 변환해주는 사용자 정의 함수

<pre><code class="SQL" style="font-size:14px">create or replace function date_to_char(p_dt date) return varchar2 as 

  begin 

    return to_char(p_dt, 'yyyy/mm/dd hh24:mi:ss'); 

  end; 

 / 

</code></pre>


- `to_char` 함수를 바로 호출하는것 보다 훨씬 느림

- 메인쿼리에 참조하는 사용자 정의 함수에 또 다른 쿼리문이 내장되어 있다면 수행 성능이 훨씬 나빠짐

- Recursive Call이 반복적으로 일어난다.

</br>


### 사용자 정의 함수 / 프로시저에 의한 성능저하 해소 방안

- 대용량 조회 쿼리에서 함수 호출하면 Recursive Call이 반복되기 떄문에 소량 데이터를 조회할 때 사용한다.

- 부분범위 처리가 가능한 상황에서 제한적으로 사용한다.

- 가급적 함수를 풀어 조인 또는 스칼라 서브쿼리 형태로 변환


> SQL 전문가 가이드 : 과목3. SQL 고급 활용 및 튜닝, 1장 SQL 아키텍처 기반 튜닝 원리 - 제3절 데이터베이스 Call과 네트워크 부하(p.499 ~ p.515)


> 사진 출처 : 구루비넷



'SQLP 자격증' 카테고리의 다른 글

SQLP - Lock  (0) 2018.04.29
SQLP - 데이터베이스 I/O 원리  (0) 2018.04.28
SQLP - SQL 파싱 부하  (0) 2018.03.05
SQLP - 데이터베이스 아키텍처(2)  (0) 2018.03.03
SQLP - 데이터베이스 아키텍처(1)  (0) 2018.02.07