ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 친절한 SQL 튜닝_3장.인덱스 튜닝_3
    친절한 SQL 튜닝 2020. 9. 5. 10:18
    반응형

     

    인덱스 튜닝

    3.3 인덱스 스캔 효율화

     

    3.3.1 인덱스 탐색

     

    루트 블록에는 키 값을 갖지 않는 특별한 레코드가 하나 있다. 가장 왼쪽에 ‘LMC(Leftmost Child)’ 레코드다. LMC는 자식 노드 중 가장 왼쪽 긑에 위치한 블록을 가리킨다. LMC가 가리키는 주소로 찾아간 블록에는 키 값을 가진 첫 번째 레코드보다 작거나 같은 값을 갖는 레코드가 저장돼 있다.

     

    수직적 탐색은 스캔 시작점을 찾는 과정이다.

     

     

     

    3.3.2 인덱스 스캔 효율성

     

    인덱스 선행 컬럼이 조건절에 없가나 ‘=’ 조건이 아니면 인덱스 스캔 과정에 비효율이 발생한다.

     

    인덱스 스캔 효율설 측정

     

    인덱스 스캔 효율이 좋은지 나쁜지는 어떻게 알 수 있을까? 조건절 데이터를 일일이 조회해 보는 방법도있지만, SQL 트레이스를 통해 쉽게 알 수 있다.

     

    1
    2
    3
    Rows Row Source Operation
    10 TABLE ACCESS BY INDEX ROWID BIG_TABLE(cr=7471 pr=1466 pw=0 time=22137 us)
    10   INDEX RANGE SCAN BIG_TABLE_IDX(cr=7463 pr=1466 pw=0 time=22328 us)
    cs

     

    위 트레이스를 분석해 보면, 인덱스를 스캔하고 얻은 레코드가 열 개인데, 그 과정에서 7,463개 블록(cr=7463)을 읽었다는 사실을 알 수 있다. 인덱스 리프 블록에는 테이블 블록보다 훨씬 더 많은 레코드가 담긴다. 한 블록당 평균 500개 레코드가 담긴다고 가정하면, 3,731,500(=7,463 * 500)개 레코드를 읽은 셈이다.

     

     

     

    3.3.3 액세스 조건과 필터 조건

     

    액세스 스캔 효율성을 계속 설명하기에 앞서 반드시 이해해야 할 용어 두 가지가 있다. ‘액세스 조건필터 조건이다.

     

    인덱스를 스캔하는 단계에 처리하는 조건절은 액세스 조건과 필터 조건으로 나뉜다. 인덱스 액세스 조건은 인덱스 스캔 범위를 결정하는 조건절이다. 인덱스 수직적 탐색을 통해 스캔 시작점을 결정하는 데 영향을 미치고, 인덱스 리프 블록을 스캔하거나 어디서 멈출지를 결정하는 데 영향을 미치는 조건절이다. 인덱스 필터 조건은 테이블로 액세스할지를 결정하는 조건절이다.

     

    인덱스를 이용하든, 테이블을 Full Scan 하든, 테이블 액세스 단계에서 처리되는 조건절은 모두 필터 조건이다. 테이블 필터 조건은 쿼리 수행 다음 단계로 전달하거나 최종 결과집합에 포함할지를 결정한다.

     

     

     

    3.3.4 비교 연산자 종류와 컬럼 순서에 따른 군집성

     

    테이블과 달리 인덱스에는 같은 값을 갖는 레코드들이 서로 군집해 있다. ‘같은 값을 찾을 대 ‘=’ 연산자를 사용하므로 인덱스 컬럼을 앞쪽부터 누락없이 ‘=’ 연산자로 조회하면 조건절을 만족하는 레코드는 모두 모여 있다. 어느 하나를 누락하거나 ‘=’ 조건이 아닌 연산자로 조회하면 조건절을 만족하는 레코드가 서로 흩어진 상태가 된다.

     

    선행 컬럼이 모두 ‘=’ 조건인 상태에서 첫 번째 나타나는 범위검색 조건까지만 만족하는 인덱스 레코드는 모두 연속해서 모여 있지만, 그 이하 조건까지 만족하는 레코드는 비교 연산자 종류에 상관없이 흩어진다.

     

     

     

    3.3.5 인덱스 선행 컬럼이 등치(=) 조건이 아닐 때 생기는 비효율

     

    인덱스 스캔 효율설은 인덱스 컬럼을 조건절에 모두 등치(=) 조건으로 사용할 때 가장 좋다. 리프 블록을 스캔하면서 읽은 레코드는 하나도 걸러지지 않고 모두 테이블 액세스로 이어지므로 인덱스 스캔 단계에서의 비효율은 전혀 없다.

     

    인덱스 컬럼 중 일부가 조건절에 없거나 등치 조건이 아니더라도, 그것이 뒤쪽 컬럼일 때는 비효율이 없다.

     

    반면, 인덱스 선행 컬럼이 조건절에 없거나, 부등호, BETWEEN, LIKE 같은 범위검색 조건이면, 인덱스를 스캔하는 단계에서 비효율이 생긴다.

     

    예를 들어, 인덱스를 『아파트시세코드 + 평형 + 평형타입 + 인터넷매물』 순으로 구성한 상황에서 아래 SQL을 수행하는 경우를 살펴보자.

     

    1
    2
    3
    4
    5
    6
    7
    SELECT 해당층, 평당가, 입력일, 해당동, 매물구분, 연사용일수, 중개업소코드
      FROM 매물아파트매매
     WHERE 아파트시세코드 = ‘A01011350900056’
       AND 평형 = ‘59’
       AND 평형타입 = ‘A’
       AND 인터넷매물 BETWEEN ‘1’ AND ‘3’
     ORDER BY 입력일 DESC
    cs

     

    인터넷매물이 BETWEEN 조건이지만 선행 컬럼들(아파트시세코드, 평형, 평형타입)이 모두 ‘=’ 조건이기 때문에 전혀 비효율 없이 조건을 만족하는 세 건을 빠르게 찾았다. 비효율이 전혀 없다는 것은 세 건을 찾기 위해 단 네 건만 스캔했음을 의미한다. 맨 마지막 스캔은 조건을 만족하는 레코드가 더 없음을 확인하기 위한 one-plus 스캔이므로 불가피하다.

     

    인덱스 선행 컬럼이 모두 ‘=’ 조건일 때 필요한 범위만 스캔하고 멈출 수 있는 것은, 조건을 만족하는 레코드가 모두 한데 모여 있기 때문이다.

     

    이제 인덱스 구성을 『인터넷매물 + 아파트시세코드 + 평형 + 평형타입』 순으로 바꾼 후 같은 SQL을 수행하면, 인덱스 스캔 범위가 넓어진다.

     

    인덱스 선두 컬럼 인터넷매물에 BETWEEN 연산자를 사용하면 나머지 조건을 만족하는 레코드들이 인터넷매물 값 별로 뿔뿔이 흩어져 있게 된다. 따라서 조건을 만족하지 않는 레코드까지 스캔하고서 버리는 비효율이 생긴다.

     

     

     

    3.3.6 BETWEENIN-List로 전환

     

    범위검색 컬럼이 맨 뒤로 가도록 인덱스를 『 아파트시세코드 + 평형 + 평형타입 +  인터넷매물』 순으로 변경하면 좋겠지만 운영 시스템에서 인덱스 구성을 바꾸기는 쉽지 않다. 이럴 때 BETWEEN 조건을 아래와 같이 IN-List로 바꿔주면 큰 효과를 얻는 경우가 있다.

     

    1
    2
    3
    4
    5
    6
    7
    SELECT 해당층, 평당가, 입력일, 해당동, 매물구분, 연사용일수, 중개업소코드
      FROM 매물아파트매매
     WHERE 인터넷매물 IN (‘1’, ‘2’, ‘3’)
       AND 아파트시세코드 = ‘A01011350900056’
       AND 평형 = ‘59’
       AND 평형타입 = ‘A’
     ORDER BY 입력일 DESC
    cs

     

    IN-List 개수만큼 UNION ALL 브랜치가 생성되고 각 브랜치마다 모든 컬럼을 ‘=’ 조건으로 검색하므로 앞서 선두 컬럼에 BETWWEN을 사용할 때와 같은 비효율이 사라진다.

     

    BETWEEN 조건을 IN-List로 전환할 때 주의 사항

     

    BETWEEN 조건을 IN-List 조건으로 전환할 대 주의할 점은, IN-List 개수가 많지 않아야 한다는 것이다. IN-List 개수가 많으면 수직적 탐색이 많이 발생한다.

     

    인덱스 스캔 과정에 선택되는 레코드들이 서로 멀리 떨어져 있을 때만 유용하다는 사실도 기억하기 바란다.

     

     

     

    3.3.7 Index Skip Scan 활용

     

    3.3.8 IN 조건은 ‘=’ 인가

     

    SQL 튜닝 입문자에게서 흔히 볼 수 있는 현상은 IN조건을 ‘=‘ 조건과 동등시한다는 점이다. 아래 SQL에 대한 인덱스를 『상품ID + 고객번호』로 설계할 때와 『 고객번호 + 상품ID』로 설계할 때 차이가 있는지 물어보면 쉽게 알 수 있다. 흔히 차이가 없다고 생각한다. 그런데 IN조건은 ‘=’이 아니다.

     

    1
    2
    3
    4
    SELECT *
      FROM 고객별가입상품
     WHERE 고객번호 = :cust_no
       AND 상품ID IN (‘NH00037’, ‘NH00041’, ‘NH00050’)
    cs

     

    인덱스를 『상품ID + 고객번호』순으로 생성하면, 같은 상품은 고객번호 순으로 정렬된 상태로 하나(또는 연속된 두 개)의 리프 블록에 저장된다. 반면, 고객번호 기준으로는 같은 고객번호가 상품ID에 따라 뿔뿔이 흩어진 상태가 된다.

     

    인덱스가 이렇게 구성돼 있다면, 상품ID 조건절이 IN-List Iterator 방식으로 풀리는 것이 효과적이다. 고객번호 조건을 만족하는 레코드가 서로 멀리 떨어져 있기 때문이다. 상품ID 조건절이 IN-List Iterator 방식으로 풀린다는 건 SQL이 아래와 같은 방식으로 실행된다는 의미다. IN조건이 ‘=’ 조건이 됐다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    SELECT *
      FROM 고객별가입상품
     WHERE 고객번호 = :cust_no
       AND 상품ID IN = ‘NH00037’
     
    UNION ALL
     
    SELECT *
      FROM 고객별가입상품
     WHERE 고객번호 = :cust_no
       AND 상품ID IN = ‘NH00041’
     
    UNION ALL
     
    SELECT *
      FROM 고객별가입상품
     WHERE 고객번호 = :cust_no
       AND 상품ID IN = ‘NH00050’
    cs

     

    상품ID 조건절을 이처럼 IN-List Iterator 방식으로 풀면 고객번호와 상품ID 둘 다 인덱스 액세스 조건으로 사용된다.

     

    인덱스를 『고객번호 + 상품ID』순으로 생성해 보자. 그러면 고객은 상품ID 순으로 정렬된 상태로 같은 리프 블록에 저장된다. 여기서도 상품ID 조건절을 IN-List Iterator 방식으로 풀면, 인덱스를 수직적으로 세 번 탐색하는 과정에 아홉 개 블록을 읽는다.

     

    상품ID 조건절을 IN-List Iterator 방식으로 풀지 않으면, 상품ID 조건절을 필터로 처리한다. 그러면 고객번호만 액세스 조건이므로 고객번호 = 1234인 레코드를 모두 스캔한다. 같은 고객은 한 블록(또는 연속된 두 블록)에 모여 있으므로 블록 I/O는 수직적 탐색 과정을 포함해 총 세 개(또는 네 개)만 발생한다.

     

    요컨대, IN 조건은 ‘=’이 아니다. IN조건이 ‘=’이 되려면 IN-List Iterator 방식으로 풀려야만 한다. 그렇지 않으면, IN 조건은 필터 조건이다. 그런데 과연 IN 조건을 ‘=’ 조건으로 만들기 위해, 즉 액세스 조건으로 만들기 위해 IN-List Iterator 방식으로 푸는 것이 항상 효과적인가?

     

    방금 본 사례에서 상품ID가 액세스 조건으로서 의미있는 역할을 하려면, 고객별로 상품 데이터가 아주 많아야 한다. 그렇지 않은 상황에서 상품ID는 필터 방식으로 처리되는 게 오히려 낫다.

     

     

     

    3.3.9 BETWEEN LIKE 스캔 범위 비교

     

    월별로 집계된 테이블에서 20091월부터 12월 데이터를 조회하고자 할 때, 흔히 아래와 같이 LIKE 연산자를 사용한다.

     

    1
    2
    3
    SELECT * 
      FROM 월별고객별판매집계
     WHERE 판매월 LIKE ‘2009%’
    cs

     

    아래 BETWEEN이 더 정확한 표현식인데도 개발자들이 LIKE를 더 선호하는 이유는 단순하다. LIKE로 코딩하는 것이 더 편리하기 때문이다.

     

    1
    2
    3
    SELECT *
      FROM 월별고객별판매집계
     WHERE 판매월 BETWEEN ‘200901’ AND ‘200912’
    cs

     

    LIKE BETWEEN은 둘 다 범위검색 조건으로서, 앞에서 설명한 범위검색 조건을 사용할 때의 비효율 원리가 똑같이 적용된다. 하지만 데이터 분포와 조건절 값에 따라 인덱스 스캔량이 서로 다를 수 있다.

     

    결론부터 말하면, LIKE보다 BETWEEN을 사용하는 게 낫다.

     

     

     

    3.3.10 범위검색 조건을 남용할 때 생기는 비효율

     

     

    3.3.11 다양한 옵션 조건 처리 방식의 장단점 비교

     

    OR 조건 활용

     

    옵션 조건 처리에 아래와 같이 OR 조건을 사용할 수 있다.

     

    1
    2
    3
    4
    SELECT * 
      FROM 고객
     WHERE (:CUST_ID IS NULL OR 고객ID = :CUST_ID)
       AND 거래일자 BETWEEN :DT1 AND :DT2
    cs

     

    개발자들이 흔히 사용하는 이 방식의 가장 큰 문제점은 옵션 조건 컬럼을 선두에 두고 『고객ID + 거래일자』 순으로 인덱스를 구성해도 이를 사용할 수 없다는 데 있다. 따라서 인덱스 선두 컬럼에 대한 옵션 조건에 OR 조건을 사용해선 안 된다.

     

    OR 조건을 활용한 옵션 조건 처리를 정리하면 다음과 같다.

     

    - 인덱스 액세스 조건으로 사용 불가

     

    - 인덱스 필터 조건으로도 사용 불가

     

    - 테이블 필터 조건으로만 사용 가능

     

     

     

    LIKE/BETWEEN 조건 활용

     

    앞에서 설명한 것처럼 LIKE/BETWEEN도 옵션 조건 처리를 위해 많이 사용하는 방식 중 하나다. 아래와 같이 변별력이 좋은 필수 조건이 있는 상황(당일 등록 상품은 소수)에서 이들 패턴을 사용하는 것은 나쁘지 않다. 필수 조건 컬럼을 인덱스 선두에 두고 액세스 조건으로 사용하면, LIKE/BETWEEN이 인덱스 필터 조건이어도 충분히 좋은 성능을 낼 수 있기 때문이다.

     

    1
    2
    3
    4
    5
    -- 인덱스 : 등록일 + 상품분류코드
    SELECT * 
      FROM 상품
     WHERE 등록일시 >= TRUNC(SYSDATE) -- 필수 조건(당일 등록 상품)
       AND 상품분류코드 LIKE :PRD_CLS_CD || ‘%’ -- 옵션 조건
    cs

     

    더구나, 필수 조건이 아래와 같이 ‘=’이면 옵션 조건인 상품분류코드까지도 인덱스 액세스 조건이므로 최적의 성능을 낼 수 있다.

     

    1
    2
    3
    4
    5
    -- 인덱스 : 상품명 + 상품분류코드
    SELECT * 
      FROM 상품
     WHERE 상품명 = :prd_nm -- 필수 조건
       AND 상품분류코드 LIKE :prd_cls_cd || ‘%’ -- 옵션 조건
    cs

     

    문제는 필수 조건의 변별력이 좋지 않을 때다. 예를 들어, 아래 SQL에서 상품대분류코드만으로 조회할 때는 Table Full Scan이 유리하다. 그런데 옵티마이저는 상품코드까지 입력할 때를 기준으로 Index Range Scan을 선택한다. 다행히 사용자가 상품코드까지 입력하면 최적의 성능을 내겠지만, 그렇지 않을 때 성능에 문제가 생긴다.

     

    1
    2
    3
    4
    5
    -- 인덱스 : 상품대분류코드 + 상품코드
    SELECT * 
      FROM 상품
     WHERE 상품대분류코드 = :prd_lcls_cd -- 필수 조건
       AND 상품코드 LIKE :prd_cd || ‘%’ -- 옵션 조건
    cs

     

    이 외에도 LIKE/BETWEEN 패턴을 사용하고자 할 때는 아래 네 가지 경우에 속하는지 반드시 점검해야 한다. (BETWEEN 조건은 1번과 2번 조건에 해당하는지만 점검하면 된다.)

     

    1. 인덱스 선두 컬럼

     

    2. NULL 허용 컬럼

     

    3. 숫자형 컬럼

     

    4. 가변 길이 컬럼

     

    첫째, 인덱스 선두 컬럼에 대한 옵션 조건을 LIKE/BETWEEN 연산자로 처리하는 것은 금물이다. 예를 들어, 인덱스를 『고객ID + 거래일자』로 구성한 상황에서 고객ID에 대한 옵션 조건을 아래와 같이 LIKE로 처리했다고 하자.

     

    1
    2
    3
    4
    SELECT *
      FROM 거래
     WHERE 고객ID LIKE :CUT_ID || ‘%’
       AND 거래일자 BETWEEN :DT1 AND :DT2
    cs

     

    사용자가 고객ID 값을 입력하면, 둘 다 범위검색 조건이어서 인덱스 스캔 과정에 약간 비효율이 있더라도 고객ID가 변별력이 매우 좋기 때문에 비교적 빠르게 조회된다. 그런데 만약 사용자가 고객ID 값을 입력하지 않으면, 인덱스에서 모든거래 데이터를 스캔하면서 거래일자 조건을 필터링하는 불상사가 생긴다.

     

    둘째, NULL 허용 컬럼에 대한 옵션 조건을 LIKE/BETWEEN 연산자로 처리하는 것도 금물이다. 성능을 떠나 결과 집합에 오류가 생기기 때문이다. SQL에서 :cust_id 변수에 NULL을 입력하면 조건절은 아래와 같은 형태가 된다.

     

    1
    2
    3
    4
    SELECT *
      FROM 고객
     WHERE 고객ID LIKE ‘%’
       AND 거래일자 BETWEEN :DT1 AND :DT2
    cs

     

    거래일자 조건에 해당하는 모든 고객의 거래를 선택해야 하는 상황인데, 고객IDNULL 허용컬럼이고 실제 NULL 값이 입력돼 있다면 그 데이터는 결과집합에서 누락된다.

     

    BETWEEN 조건을 사용할 때도 컬럼 값이 NULL인 데이터는 결과집합에서 누락된다.

     

    셋째, 숫자형이면서 인덱스 액세스 조건으로도 사용 가능한 컬럼에 대한 옵션 조건 처리는 LIKE 방식을 사용해선 안 된다. 예를 들어, 인덱스를 『거래일자 + 고객ID』 순으로 구성한 상황에서 SQL을 아래와 같이 작성하면, :cust_id에 값을 입력했을 대 두 컬럼 모두 인덱스 액세스 조건으로 사용된다.

     

    1
    2
    3
    4
    SELECT * 
      FROM 거래
     WHERE 거래일자 = :TRD_DT
       AND 고객ID LIKE :CUST_ID || ‘%’
    cs

     

    그런데 만약 고객ID가 숫자형 컬럼이면, 아래와 같이 자동 형변환이 일어나므로 고객ID가 필터 조건으로 사용된다.

     

    1
    2
    3
    4
    SELECT * 
      FROM 거래
     WHERE 거래일자 = :TRD_DT
       AND TO_CHAR(고객ID) LIKE :CUST_ID || ‘%’
    cs

     

    넷째, LIKE를 옵션 조건에 사용할 때는 컬럼 값 길이가 고정적이어야 한다. 예를 들어, 고객명 컬럼에는 김훈’, ‘김훈남등 길이가 다른 값이 입력될 수 있다. 그런데 고객명에 대한 옵션 조건을 아래와 같이 LIKE 패턴으로 처리하면, ‘김훈고객을 찾기 위해 :cust_nm 변수에 김훈을 입력했을 때 김훈남고객도 같이 조회된다.

     

    1
    WHERE 고객명 LIKE :CUST_NM || ‘%’ -- CUST_NM = ‘김훈
    cs

     

     

     

    UNION ALL 활용

     

    :cust_id 변수에 값을 입력했는지에 따라 위아래 SQL 중 어느 하나만 실행되게 하는 방식이다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    SELECT * 
      FROM 거래
     WHERE :CUST_ID IS NULL
       AND 거래일자 BETWEEN :DT1 AND :DT2
     
    UNION ALL  
     
    SELECT * 
      FROM 거래
     WHERE :CUST_ID IS NOT NULL
       AND 고객ID = :CUST_ID
       AND 거래일자 BETWEEN :DT1 AND :DT2 
    cs

     

    :cust_id 변수에 값을 입력하지 않으면 위쪽 브랜치에서 거래일자가 선두인 인덱스를 사용하고, 변수에 값을 입력하면 아래쪽 브랜치에서 『고객ID + 거래일자』 인덱스를 사용하고 있다.

     

     

     

    NVL/DECODE 함수 활용

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    SELECT *
      FROM 거래
     WHERE 고객ID = NVL(:CUST_ID, 고객ID)
       AND 거래일자 BETWEEN :DT1 AND :DT2
     
    또는 
     
    SELECT *
      FROM 거래
     WHERE 고객ID = DECODE(:CUST_ID, NULL, 고객ID, :CUST_ID)
       AND 거래일자 BETWEEN :DT1 AND :DT2
    cs
     

     

    :cust_id 변수에 값을 입력하지 않으면 위쪽 브랜치에서 거래일자가 선두인 인덱스를 사용하고, 변수에 값을 입력하면 아래쪽 브랜치에서  『고객ID + 거래일자』 인덱스를 사용한다는 것을 표현하고 있다.

     

    고객ID 컬럼을 함수 인자로 사용했는데 인덱스를 사용할 수 있는 것은 OR Expension 쿼리 변환이 일어났기 때문이다. 앞서 살펴본 UNION ALL 방식으로 옵티마이저가 쿼리를 변환한 것이다.

     

    만약 이 기능이 작동하지 않으면 NVL, DECODE 함수를 사용하는 패턴도 인덱스 액세스 조건으로 사용이 불가능하다. :cust_id에 값을 입력하지 않으면(, NULL을 입력하며) 조건절이 고객ID = 고객ID’ 형태가 되므로 인덱스에서 이 조건을 만족하는 어느 한 시작점을 찾을 수 없기 때문이다.

     

    이 방식의 가장 큰 장점은 옵션 조건 컬럼을 인덱스 액세스 조건으로 사용할 수 있다는 데 있다. , UNION ALL 보다 단순하면서 UNION ALL과 같은 성능을 낸다.

     

    단점은, 앞서 설명한 LIKE 패턴처럼 NULL 허용 컬럼에 사용할 수 없다는 데 있다. 조건절 변수에 NULL을 입력하면 값이 NULL인 레코드가 결과집합에서 누락되기 때문이다.

     

     

     

    3.3.12 함수호출부하 해소를 위한 인덱스 구성

     

    PL/SQL 함수의 성능적 특성

     

    PL/SQL 사용자 정의 함수는 개발자들이 일반적으로 생각하는 것보다 매우 느리다. 예를 들어, 아래처럼 한두 번 호출할 때는 함수를 사용하지 않을 때와 비교해 성능 차이가 잘 느끼지 못한다.

     

    1
    2
    3
    SELECT 회원번호, 회원명, 생년, 생월일, ENCRYPTION(전화번호)
      FROM 회원
     WHERE 회원번호 = :MEMBER_NO -- 한 건 조회
    cs

     

    아래처럼 대량 데이터를 조회해 보면 성능 차이를 확연히 느낄 수 있다.

     

    1
    2
    3
    SELECT 회원번호, 회원명, 생년, 생월일, ENCRYPTION(전화번호)
      FROM 회원
     WHERE 생월일 LIKE ‘01%’ -- 수백 ~ 수백 만 건 조회
    cs

     

    PL/SQL 사용자 정의 함수가 느린 데는 아래 3가지 이유가 있다.

     

    1. 가상머신(VM) 상에서 실행되는 인터프리터 언어

     

    2. 호출 시마다 컨텍스트 스위칭 발생

     

    3. 내장 SQL에 대한 Recursive Call 발생

     

    PL/SQL 사용자 정의 함수의 성능을 떨어뜨리는 가장 결정적인 요소는 Recursive Call이다. 아래 SQL에서 조건을 만족하는 회원이 100만 명이면 GET_ADDR 100만 번 실행하는데, 만약 함수에 SQL이 내장돼 있으면 그 SQL 100만 번 실행한다. 대게 PL/SQL 함수에는 SQL이 내장돼 있으므로 일반적으로 인터프리팅, 컨텍스트 스위칭보다 Recursive Call 부하가 크다.

     

    1
    2
    3
    SELECT 회원번호, 회원명, 생년, 생월일, GET_ADDR(우변번호)  AS 기본주소
      FROM 회원
     WHERE 생월일 LIKE ‘01%’
    cs

     

    SQL PL/SQL 함수를 쓰지 않고 아래와 같이 조인문으로 처리하면 성능 차이가 매우 크다. 당연히 아래 SQL이 빠르다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    SELECT A.회원번호, A.회원명, A.생년, A.생월일
         , ( SELECT B.시도 || ‘ ’ || B.구군 || ‘ ’ || B.읍면동
               FROM 기본주소 B
              WHERE B.우편번호 = A.우편번호
                AND B.순번 = 1
           ) 기본주소
      FROM 회원 A
     WHERE A.생년월이 LIKE ‘01%’
     
    또는 
     
    SELECT A.회원번호, A.회원명, A.생년, A.생월일
          , B.시도 || ‘ ’ || B.구군 || ‘ ’ || B.읍면동 AS 기본주소
      FROM 회원 A, 기본주소 B
     WHERE A.생년월이 LIKE ‘01%’
       AND B.우편번호(+= A.우편번호
       AND B.순번(+= 1
    cs

     

    출처 : 친절한 SQL 튜닝 - 조시형 지음

     

    반응형

    댓글

Designed by Tistory.