본문 바로가기

데이터베이스/SQL 튜닝

[SQL튜닝/아키텍처 기반 튜닝 원리] 데이터베이스 Call과 네트워크 부하 (데이터베이스 Call 종류 및 최소화, Array Processing, Fetch Call 최소화, 페이지 처리, 분산 쿼리 최적화, 사용자 정의 함수/프로시저 제한적 사용/성능 저하 요인 관리)

🖥️ 들어가며

📌 One SQL 구현의 중요성
- 루프를 돌면서 여러 작업을 반복 수행하는 프로그램(Java, C, VB 등)을 One SQL로 구현했을 때 데이터베이스 Call 횟수를 줄여 성능 개선 효과를 얻을 수 있습니다.
- DBMS 내에서 수행되는 사용자 정의 프로시저로 개발하면 네트워크 트래픽이 없는 Recursive Call만 발생하므로 빠르게 수행됩니다.
- 데이터베이스 Call은 개별 프로그램의 수행속도에 큰 영향 + 시스템 전체의 확장성에 영향을 미칩니다.

📌 One SQL 예시
- 메소드를 5번 호출: Parse Call 5번 + Execute Call 5번 발생하는 프로그램이 24시간 돌 때
=> 메소드 1번만 호출하게 수정: 시스템이 5배의 확장성을 갖게 되는 것입니다.

 


 

✏️ 1. 데이터베이스 Call 종류

  • SQL Cursor 구분 1: 작업 요청별
    • (1) Parse Call: SQL 파싱을 요청하는 Call
    • (2) Execute Call: SQL 실행을 요청하는 Call
    • (3) Fetch Call: SELECT문의 결과 데이터 전송을 요청하는 Call
  • SQL Cursor 구분 2: Call 발생위치별
    • (1) User Call
      • DBMS 외부로부터 요청되는 Call
      • 동시 접속자 수가 많은 Peak 시간대에 시스템 확장성을 떨어뜨리는 가장 큰 요인 중 하나입니다.
      • 개발자의 기술력 문제, 애플리케이션 설계 문제, 프레임워크 기술 구조의 문제
        - Array Processing을 지원하지 않는 프레임워크
        - 화면 페이지 처리에 대한 잘못 설계된 표준 가이드
        - 사용자 정의 함수/프로시저에 대한 무조건적 제약
        - 프로시저 단위 모듈을 지나치게 잘게 쪼개서 SQL을 건건이 호출하도록 설계
        => User Call을 최소화하려는 노력 필요
      • User Call을 줄이기 위한 기술 요소
        - Loop 쿼리를 해소, 집합적 사고를 통해 One SQL 구현
        - Array Processing: Array 단위 Fetch, Bulk Insert/Update/Delete
        - 부분범위처리 원리 활용
        - 효과적인 화면 페이지 처리
        - 사용자 정의 함수/프로시저/트리거의 적절한 활용 (⚠️필요한 때에 일부 허용해야 한다는 말)
    • (2) Recursive Call
      • DBMS 내부에서 발생하는 Call
      • SQL 파싱과 최적화 과정에서 발생하는 데이터 딕셔너리 조회, 사용자 정의 함수/프로시저 내에서의 SQL 수행합니다.
      • Recursive Call 최소화 방안
        - 바인드 변수를 통한 하드파싱 발생횟수 감소
        - 사용자 정의 함수/프로시저의 적절한 사용 (⚠️필요한 때에 일부 허용해야 한다는 말)

 

 

 

✏️ 2. Array Processing 활용

  • Array Processing 기능을 활용하면 한번의 SQL(Insert/Update/Delete) 수행으로 다량의 레코드를 동시에 처리할 수 있습니다.
  • 네트워크 Call 감소, SQL 수행시간, CPU 사용량이 감소합니다.
  • Array에 담아 한꺼번에 처리 → Call 횟수를 줄여 성능 개선에 도움됩니다.
  • Array Processing 기법을 지원하는 인터페이스는 개발언어마다 다르므로 API를 통해 확인하고 활용해야 합니다.
// Java: Array Processing
1 public class JavaArrayProcessing{ 
2 public static void insertData( Connection con 
3 , PreparedStatement st 
4 , String param1
5 , String param2 
6 , String param3 
7 , long param4) throws Exception{ 
8 st.setString(1, param1); 
9 st.setString(2, param2); 
10 st.setString(3, param3); 
11 st.setLong(4, param4); 
12 st.addBatch();                                          // Insert할 데이터를 담기만 하다가
13 } 
14 
15 public static void execute(Connection con, String input_month) 
16 throws Exception { 
17 long rows = 0; 
18 String SQLStmt1 = "SELECT 고객번호, 납입월" 
19 + ", 지로, 자동이체, 신용카드, 핸드폰, 인터넷 " 
20 + "FROM 월요금납부실적 " 
21 + "WHERE 납입월 = ?"; 
22 
23 String SQLStmt2 = "INSERT INTO 납입방법별_월요금집계 " 
24 + "(고객번호, 납입월, 납입방법코드, 납입금액) " 
25 + "VALUES(?, ?, ?, ?)"; 
26 
27 con.setAutoCommit(false); 
28 
29 PreparedStatement stmt1 = con.prepareStatement(SQLStmt1); 
30 PreparedStatement stmt2 = con.prepareStatement(SQLStmt2); 
31 stmt1.setFetchSize(1000);             // SELECT 결과집합 Fetch할 때도 1000건씩 하도록 조정
32 stmt1.setString(1, input_month); 
33 ResultSet rs = stmt1.executeQuery(); 
34 while(rs.next()){ 
35 String 고객번호 = rs.getString(1); 
36 String 납입월 = rs.getString(2); 
37 long 지로 = rs.getLong(3); 
38 long 자동이체 = rs.getLong(4); 
39 long 신용카드 = rs.getLong(5); 
40 long 핸드폰 = rs.getLong(6); 
41 long 인터넷 = rs.getLong(7); 
42 
43 if(지로 > 0) 
44 insertData (con, stmt2, 고객번호, 납입월, "A", 지로); 
45 
46 if(자동이체 > 0) 
47 insertData (con, stmt2, 고객번호, 납입월, "B", 자동이체); 
48 
49 if(신용카드 > 0) 
50 insertData (con, stmt2, 고객번호, 납입월, "C", 신용카드); 
51 
52 if(핸드폰 > 0) 
53 insertData (con, stmt2, 고객번호, 납입월, "D", 핸드폰); 
54 
55 if(인터넷 > 0) 
56 insertData (con, stmt2, 고객번호, 납입월, "E", 인터넷); 
57 
58 if(++rows%1000 == 0) stmt2.executeBatch();  // 1000건 쌓일 때마다 한번씩 executeBatch 수행
59 
60 } 
61 
62 rs.close(); 
63 stmt1.close(); 
64 
65 stmt2.executeBatch(); 
66 stmt2.close(); 
67 
68 con.commit(); 
69 con.setAutoCommit(true); 
70 } 
71 
72 static Connection getConnection() throws Exception { } 
73 static void releaseConnection(Connection con) throws Exception { ...... } 
74 
75 public static void main(String[] args) throws Exception{ 
76 Connection con = getConnection(); 
77 execute(con, "200903"); 
78 releaseConnection(con); 
79 }
80 }
-- PL/SQL: Array Processing (1000건씩 Fetch해서 Bulk Insert)
DECLARE 
  l_fetch_size NUMBER DEFAULT 1000; -- 1,000건씩 Array 처리 
  CURSOR c IS 
    SELECT empno, ename, job, sal, deptno, hiredate 
    FROM emp; 
... 
   BEGIN 
  
  OPEN C; 
 
   LOOP 

    FETCH c BULK COLLECT 
    INTO p_empno, p_ename, p_job, p_sal, p_deptno, p_hiredate 
    LIMIT l_fetch_size; 

    FORALL i IN p_empno.first..p_empno.last 
      INSERT INTO emp2 
      VALUES ( p_empno (i) 
             , p_ename (i) 
             , p_job (i) 
             , p_sal (i) 
             , p_deptno (i) 
             , p_hiredate (i) ); 
     EXIT WHEN c%NOTFOUND; 
   END LOOP; 

  CLOSE C;

 

 

 

✏️ 3. Fetch Call 최소화

  • Fetch Call 최소화 방법 1: 부분범위처리
    • 부분범위처리의 원리: 쿼리결과집합을 전송할 때, 전체 데이터를 연속적으로 전송하지 않고 사용자로부터 Fetch Call이 있을 때마다 일정량씩 나누어 전송하는 것입니다.
    • 부분범위처리의 예시: ArraySize 100건만 차면 사용자에게 데이터 전달
    • SQL Server: 쿼리 분석기 옵션에 설정되어 있는 "네트워크 패키지 크기"로 운반(default 4,096 Byte)
      Oracle: 내부적으로는 SDU(Session Data Unit, Session 레이어), TDU(Transport Data Unit, Transport 레이어) 단위로 나누어 전송
      *ArraySize를 작게 설정하면 하나의 네트워크 패킷에 담아 전송 / 크게 설정하면 여러 개의 패킷으로 나누어 전송합니다.

 

  • Fetch Call 최소화 방법 2: ArraySize 조정에 의한 Fetch Call 감소 및 블록 I/O 감소 효과
    • 대량의 데이터파일 전송 → ArraySize를 크게 하여 Fetch Call 횟수를 줄여주는 것이 유리하고
      적은 의 데이터만 Fetch 하다가 멈추는 프로그램 → ArraySize를 작게 설정하는 것이 유리합니다.
    • ArraySize를 증가시키면 → 네트워크 부하 감소, 서브 프로세스가 읽어야 할 블록 개수 감소 효과가 있습니다.
    • ArraySize를 키운다고 해서 Fetch Count와 블록 I/O가 같은 비율로 줄지 않습니다.
      => ArraySize를 무조건 크게 설정한다고 좋은 것이 아니고, 일정 크기 이상이면 리소스만 낭비하는 결과를 초래할 수 있습니다.
-- SQL Plus 프로그램: Array 단위 Fetch 기능 활용 방법 (default arraysize = 100)
set arraysize 100

-- Oracle PL/SQL: 커서를 열고 레코드를 Fetch
-- 9i: 한 번에 한 로우씩만 처리 (Single-Row Fetch)
-- 10g: 자동으로 100개씩 Array Fetch가 일어남 (Cursor For Loop 구문을 이용할 때만)
for item in cursor
loop
...
end lool;

 

 

 

✏️ 4. 페이지 처리 활용

  • 페이지 미처리시 발생하는 부하요인
    • (1) 다량 발생하는 Fetch Call의 부하
    • (2) 대량의 결과집합을 클라이언트에 전송하면서 발생하는 네트워크 부하
    • (3) 대량의 데이터 블록을 읽으면서 발생하는 I/O 부하
    • (4) AP(Application) 서버 및 웹 서버 리소스 사용량 증가
  • 페이지 처리를 통한 부하해소
    • (1) 페이지 단위로 화면에서 필요한 만큼만, Fetch Call
    • (2) 페이지 단위로 화면에서 필요한 만큼만, 네트워크를 통해 결과 전송
    • (3) 인덱스 + 부분범위처리 원리를 이용 = AP 서버웹 서버 리소스 사용량 최소화
      => 조회할 데이터가 일정량 이상이고 수행빈도가 높다면 "필수적으로 페이지 처리"를 구현해야 합니다.

 

 

 

✏️ 5. 분산 쿼리

  • 부하 분산, 재해복구, 보안 등 여러가지 목적으로 분산 환경의 데이터베이스를 구축합니다.
  • 원격 조인이 자주 문제시 되는데, 네트워크를 통한 데이터 전송을 줄이는 것이 분산 쿼리 튜닝의 핵심 원리입니다.
    (⚠️ @DB LINK는 보안에 좋지 않습니다.)
-- 분산 DB간 테이블 조인 예시
select channel_id, sum(quantity_sold) auantity_cold 
from order a, sales@lk_sales b        -- ☆원격의 sal 테이블을 전송받아 order 테이블과 NL 조인
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)


-- 분산 DB간 성능저하 해결방안
-- order_date 조건에 해당하는 데이터만 원격 전송 조인 + group by 거친 결과 집합만 전송 받음
-- ☆핵심: 네트워크를 통한 데이터 전송량 줄이기☆
select /*+ driving_site(b) */            -- 원격서버가 처리 가능하도록 driving_site 힌트 사용
       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

 

 

 

✏️ 6. 사용자 정의 함수 / 프로시저의 특징과 성능

  • 사용자 정의 함수/프로시저의 특징
    • 사용자 정의함수/프로시저는 내장함수처럼 Native 코드로 완전 컴파일된 형태가 아닌 인터프리터 방식입니다.
    • 따라서 가상머신(Virtual Machine)같은 별도의 실행엔진을 통해 실행합니다.
    • 실행 시마다 컨텍스트 스위칭*이 일어나므로, 내장함수를 호출할 때와 비교해서 성능이 상당히 떨어집니다.
      - 아래 함수는 to_char 함수를 바로 호출할 때보다 훨씬 느림
      - 메인쿼리가 참조하는 사용자 정의함수에 또다른 쿼리문이 내장되어 있다면 수행성능이 훨씬 나빠짐
      - 함수에 내장된 쿼리를 수행할 때마다 Execute Call, Fetch Call이 재귀적으로 발생함(Parse Call은 1번만 수행)
      - Recursive Call이 반복적으로 일어남(User Call에 비해 성능부하가 미미하지만, 횟수가 무수히 반복되면 성능 저하)
    • *컨텍스트 스위칭
      - PL/SQL 함수 호출 시 매번 SQL 실행엔진이 사용하던 레지스터 정보를 백업합니다.
      - PL/SQL 엔진이 실행을 마치면 다시 복원하는 작업을 반복합니다.
-- 사용자 정의 함수: 문자타입의 일자 데이터 -> 날짜타입으로 변환
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;

 

  • 사용자 정의함수/프로시저에 의한 성능저하 해소방안
    • 소량의 데이터를 조회할 때만 사용합니다. (컨텍스트 스위칭 발생 X)
    • 부분범위처리가 가능한 상황에서 제한적으로 사용합니다.
    • 가급적 함수를 풀어 조인 또는 스칼라 서브쿼리 형태로 변환합니다.
    • 복잡도가 높아 One SQL이 불가능한 경우, 함수 호출을 최소화하여 튜닝합니다.

📒 정리하면

  1. One SQL 구현
    : 반복적인 프로그램 로직을 단일 SQL로 통합하여 데이터베이스 Call 횟수를 줄이면, 성능과 시스템 확장성이 향상됩니다.
  2. 데이터베이스 Call 종류 및 최소화
    - 작업 요청별: Parse Call, Execute Call, Fetch Call
    - User Call(외부 요청): 네트워크 트래픽을 유발하므로 Loop 쿼리 제거, Array Processing 활용, 부분범위처리, 페이지 처리 등으로 최소화해야 합니다.
    - Array Processing 미지원 프레임워크, 잘못된 페이지 처리 표준은 User Call 증가를 유발하므로 주의가 필요합니다.
    - Recursive Call(내부 요청): 바인드 변수 사용과 프로시저 적절한 활용으로 줄일 수 있습니다.
  3. Array Processing
    - 대량 데이터를 일괄 처리 => 네트워크 Call 횟수, SQL 실행 시간, CPU 사용량을 감소시킵니다.
    - Java의 executeBatch()나 PL/SQL의 BULK COLLECT로 구현 가능합니다.
  4. Fetch Call 최소화
    - 부분범위처리: 필요한 데이터만 나누어 전송하도록 설계합니다.
    - ArraySize 조정: 대량 데이터는 크게 설정해 Fetch 횟수 감소, 소량은 작게 설정해 리소스 낭비를 방지합니다.
  5. 페이지 처리
    - 화면에 필요한 데이터만 페이징 처리해 Fetch Call, 네트워크 부하, I/O 블록 접근을 줄입니다.
    - 인덱스와 부분범위처리 원리 적용이 핵심입니다.
  6. 분산 쿼리 최적화
    - 원격 조인 시 driving_site 힌트로 데이터 전송량을 최소화합니다.
    - 네트워크 트래픽 감소가 성능 개선의 핵심입니다.
  7. 사용자 정의 함수/프로시저 제한적 사용
    - 인터프리터 방식 실행으로 인한 컨텍스트 스위칭 오버헤드를 줄이기 위해
    - 소량의 데이터를 조회할 때만 사용하거나, 복잡한 로직은 조인 또는 스칼라 서브쿼리로 변환합니다.
  8. 사용자 정의 함수/프로시저에 의한 성능 저하 요인 관리
    - 대량 결과집합 전송 시 네트워크/AP 서버 리소스 사용량 증가를 방지합니다.
    - 페이지 미처리 시 발생하는 I/O 부하를 인덱스 활용으로 해결합니다.

반응형