h1. 제 2절 SQL 파싱 부하
1. SQL 처리과정
2. 캐싱된 SQL공유
3. 바인드 변수 사용하기
4. Static SQL과 Dynamic SQL
5. 애플리케이션 커서 캐싱
{panel:title=1.SQL 처리과정
borderColor=#ccctitleBGColor=#B0C4DEbgColor=#ffffff}
* 실행계획이 만들어 지기까지의 과정(SQL파싱,SQL최적화(optimization)

가. SQL 파싱


1. SQL을 실행하면 제일먼저 SQL 파서(parser)가 SQL 문장에 문법적 오류가 없는지 검사(Syntax 검사)
2. 문법적 오류가 없다면 의미상 오류가 없는지 검사(Semantic 검사, 오브젝트 존재유무등)
3. 사용자가 발생한 SQL과 그 실행계획이 라이브러리캐시(프로시저캐시)에 캐싱되어 있는지 확인
4. SQL과 실행계획이 캐싱되어 있다면 소프트파싱, 캐싱되어있지 않다면 하드파싱

  • 라이브러리캐시는 해시구조로관리
    -SQL마다 해시값에따라 여러해시버킷으로나뉘어저장, SQL찾을때는 SQL문장을 해시함수에 적용하여 반환되는 해시값을 이용해 해시버킷 탐색.

나.SQL 최적화(Optimaiztion)

  • 옵티마이저 : SQL최적화 담당하는 엔진,사용자가 요청한 SQL을 가장 빠르고 효율적으로 수행할 최적비용,처리경로를 선택해주는 DBMS핵심엔진
  • 3장에 자세히

2.캐싱된 SQL공유

가.실행계획 공유 조건

  • SQL 수행절차
    1. 문법적 오류와 의미상 오류가 없는지 검사
    2. 해시 함수로부터 반환받은 해시 값으로 라이브러리 캐시 내 해시버킷 탐색
    3. 찾아간 해시버킷에 체인으로 연결된 엔트리를 차례로 스캔하면서 같은 SQL 문장 탐색
    4. SQL문장을 찾으면 함께 저장된 실행계획을 가지고 바로 실행
    5. 찾아간 해시 버킷에서 SQL 문장을 찾지 못하면 최적화를 수행
    6. 최적화를 거친 SQL과 실행계획을 방금 탐색한 해시 버킷 체인에 연결
    7. 방금 최적화한 실행계획을 가지고 실행

나. 실행계획을 공유하지 못한는 경우

  • SQL문이 조금이라도 다르면 공유안됨
  • 바인드변수를 설정하지 않으면(Literal SQL) 라이버러리 캐시효율이 떨어진다.

3.바인드 변수 사용하기

가. 바인드변수의 중요성

  • 바인드변수를 사용했을때의 효과
    -SQL과 실행계획을 반복적으로 재사용함으로써 파싱 소요시간과 메모리 사용량을 줄여줌
    -궁극적으로 시스테전반의 CPU 와 메모리 사용률을 낮춰 데이터베이스 성능과 확장성을 높임
  • 바인드 변수를 사용하지 않아도 되는 예외상황
  1. 배치프로그램이나 DW, OLAP 등 정보계 시스테에서 사용되는 Long Running 쿼리
    >파싱 소요시간이 총 소요시간에서 차지하는 비중이 낮음
    >수행빈도가 낮아 하드파싱에 의한 라이브러리 캐시 부하 유발 가능성이 낮음
    >그러므로 상수조건절을 사용하여 옵티마이저가 컬럼히스토그램 정보를 활용할수 있도록 유도하는것이 유리함
  2. 조건절 컬럼의 값 종류(Distinct value)가 소수 일때
    >분포도가 좋지 않은 값은 옵티마이저가 컬럼히스토그램 정보를 활용할수 있도록 유도(바인드변수는 컬럼히스토그램정보 사용X)
  • 리터럴SQL일경우 상수를 자동으로 변수화 시켜주는 기능
    > ORACLE : cursor_sharing 파라미터 (FORCE or SIMILAR)
    > SQL Server : 단순매개변수화(기본적으로 활성화), 가급적 바인드변수를 사용함으로써 이기능작동하는 경우를 최소화

나. 바인드 변수 사용 시 주의 사항

  • 칼럼의 분포가 균일할때는 바인드 변수 처리가 나쁘지 않음
  • 칼럼의 분포가 균일하지 않을때에는 실행 시점에 바인딩되는 값에 따라 쿼리 성능이 다르게 나타날 수 있으므로 이럴때는 상수값을 사용하는것이 나을수 있음

다. 바인드 변수 부작용을 극복하기 위한 노력

  • 바인드변수 Peeking 기능 도입 : 첫번째 바인드 변수값을 살짝 훔쳐보고 그 값에 대한 분포를 이용하여 실행계획 결정하는 기능
Oracle바인드 변수 Peeking
SQL ServerParameter Sniffing

>바인드변수 Peeking 기능 도입 : 첫번째 바인드 변수값을 살짝 훔쳐보고 그 값에 대한 분포를 이용하여 실행계획 결정하는 기능
>바인드변수 Peeking 기능 도입 : 첫번째 바인드 변수값을 살짝 훔쳐보고 그 값에 대한 분포를 이용하여 실행계획 결정하는 기능
>오라클은 11g부터는 적응적 커서공유(Adaptive Cursor Sharing)를 도입하여 칼럼 분포에 따라 다른 실행계획이 사용되도록 처리하였지만 이또한 완전한 기능이 아니므로 주의해서 사용 필요

4.Static SQL과 Dynamic SQL

가.Static SQL

  • String형 변수에 담지 않고 코드 사이에 직접 기술한 SQL문(Embedded SQL)
  • 개발언어 : PowerBuilder, PL/SQL, Pro*C, SQLJ

Proc*C 구분으로 Static SQL 작성한 예시
int main()
{
  printf("사번을 입력하십시오 : ");
  scanf("%d", &empno);
  EXEC SQL WHENEVER NOT FOUND GOTO notfound;
  EXEC SQL SELECT ENAME INTO :ename
           FROM EMP
           WHERE EMPNO = :empno;
  printf("사원명 : %s.\n", ename);

notfound:
  printf("%d는 존재하지 않는 사번입니다. \n", empno); }

-SQL문을 String 변수에 담지 않고 마치 예약된 키워드처럼 C/C++ 코드 사이에 섞어 기술
-구문분석, 유효 오브젝트 여부, 오브젝트 엑세스 권한등의 체크 가능

나.Dynamic SQL

  • String 형 변수에 담아서 기술하는 SQL문

int main()
{
   char select_stmt[50] = "SELECT ENAME FROM EMP WHERE EMPNO = :empno";
   // scanf("%c", &select_stmt); → SQL문을 동적으로 입력 받을 수도 있음

   EXEC SQL PREPARE sql_stmt FROM :select_stmt;

   EXEC SQL DECLARE emp_cursor CURSOR FOR sql_stmt;
  
   EXEC SQL OPEN emp_cursor USING :empno;

   EXEC SQL FETCH emp_cursor INTO :ename;
  
   EXEC SQL CLOSE emp_cursor;

   printf("사원명 : %s.\n", ename);
}

-조건에 따란 SQL이 동적으로 바뀔수 있으므로 syntax, semantics 체크 불가능

다. 바인드 변수의 중요성 재 강조

  • Static를 사용하든 Dynamic SQL을 사용하든 옵티마이저는 SQL 문장 자체만을 인식할 뿐이므로 성능에 영향을 주지는 않는다.
  • 라이브러리 캐시 효율은 Static이냐 Dynamic의 차이가 아니라 바인드 변수의 사용여부에 초점을 맞춰야 함.

5. 애플리케이션 커서 캐싱

  • 애플리케이션 커서 캐싱 : 같은 SQL을 여러번 박복해서 수행할 경우 SQL문장의 문법적, 의미적 오류를 확인하고 해시함수로부터 반환된 해시값을 이용해서
    캐시에서 실행계획을 찾고, 수행이 필요한 메모리를 할당 받는 등의 과정을 생략하고 빠르게 SQL을 수행하는 방법
  • 개발언어마다 구현방식이 다르므로 이 기능을 활용하려면 API를 살펴봐야함.
  • 애플리케이션 커서 캐싱 예시( Proc*C)

for(;;) {
   EXEC ORACLE OPTION (HOLD_CURSOR=YES);
   EXEC ORACLE OPTION (RELEASE_CURSOR=NO);
   EXEC SQL INSERT ...... ; // SQL 수행
   EXEC ORACLE OPTION (RELEASE_CURSOR=YES);
}

  • 애플리케이션에서 커서를 캐싱한 상태에서 SQL 5,000 반복 수행 했을때 SQL 트레이스

call    count cpu  elapsed  disk query current rows
----- ------ ----- ------- ----- ------- ------ -----
Parse      1  0.00    0.00     0      0       0     0
Execute 5000  0.18    0.14     0      0       0     0
Fetch   5000  0.17    0.23     0  10000       0  5000
----- ------ ----- -------  ----- ------- ------  -----
total 10001 0.35 0.37 0 10000 0 5000

Misses in library cache during parse: 1

  • 일반적으로 SQL을 반복 수행할 때에는 Parse Call 횟수가 Execute Call 횟수와 같지만
  • 위의 결과는 Parse Call 한번만 발생했고, 이후 4,999번 수행할 때에도 Parse Call이 전혀 발생하지 않았음
  • JAVA에서 위의 기능을 구현하기 위한 방법 : 묵시적캐싱 옵션 사용(Implicit Caching)

public static void CursorCaching(Connection conn, int count) throws Exception{
  // 캐시 사이즈를 1로 지정
  ((OracleConnection)conn).setStatementCacheSize(1);

  // 묵시적 캐싱 기능을 활성화 
((OracleConnection)conn).setImplicitCachingEnabled(true);

  for (int i = 1; i <= count; i++) {
    // PreparedStatement를 루프문 안쪽에 선언
    PreparedStatement stmt = conn.prepareStatement(
      "SELECT ?,?,?,a.* FROM emp a WHERE a.ename LIKE 'W%'"); stmt.setInt(1,i);
    stmt.setInt(2,i); stmt.setString(3,"test");
    ResultSet rs=stmt.executeQuery();
   
    rs.close();
   
    // 커서를 닫더라도 묵시적 캐싱 기능을 활성화 했으므로 닫지 않고 캐시에 보관하게 됨
    stmt.close();
  }
  }

  • 또는 아래처럼 Statement를 닫지 않고 재사용해도 같은 효가를 얻을 수 있음

public static void CursorHolding(Connection conn, int count) throws Exception{

  // PreparedStatement를 루프문 바깥에 선언
  PreparedStatement stmt = conn.prepareStatement(
    "SELECT ?,?,?,a.* FROM emp a WHERE a.ename LIKE 'W%'");
  ResultSet rs;

   for (int i = 1; i <= count; i++) {
      stmt.setInt(1,i);
      stmt.setInt(2,i);
      stmt.setString(3,"test");
      rs=stmt.executeQuery();
      rs.close();
   } // 루프를 빠져 나왔을 때 커서를 닫는다.
     stmt.close();
}

  • PL/SQL에서는 위와 같이 옵션을 별도로 적용하지 않더라도 자동적으로 커서를 캐싱함(단, Stastic SQL 사용시에만)
  • Dynamic SQL을 사용하거나 Cursor Variable(=Ref Cursor)를 사용할 때는 커서를 자동으로 캐싱하는 효과가 사라짐

|