프리 정보 컨텐츠

친절한 SQL 튜닝 - 2장 인덱스 기본 본문

SQLP/친절한 SQL 튜닝

친절한 SQL 튜닝 - 2장 인덱스 기본

쏜스 2025. 2. 9. 09:00
반응형

2장 인덱스 기본

  • 인덱스에 대한 개념만 알고있을 뿐 명확한 그림이 없는 상태에서 설계하고 개발하니 성능이 좋을 리 없다.
  • 인덱스 탐색 과정이 수직적 탐색과 수평적 탐색 두 단계로 이루어진다는 사실에 대해 유념하며 알아보자.

2.1 인덱스 구조 및 탐색

데이터를 찾는 두 가지 방법

  • 어떤 초등학교를 방문해 '홍길동' 학생을 찾는 방법은 두 가지다.
  • 첫째는, 1학년 1반부터 6학년 맨 마지막 반까지 모든 교실을 돌며 홍길동 학생을 찾는 것이다.
  • 둘째는, 교무실에서 학생 명부를 조회해 홍길동 학생이 있는 교실만 찾아가는 것이다.
  • 결과는 홍길동 학생이 많으면 첫째, 몇 안 되면 후자가 빠르다.
  • 데이터베이스 테이블에서 데이터를 찾는 방법도 크게 아래 두 가지에 해당한다.
  1. 테이블 전체를 스캔한다.
  2. 인덱스를 이용한다.
  • 모든 교실을 돌며 학생을 찾는 경우가 전자이고, 학생부를 이용한 경우가 후자인 것이다.
  • 테이블 전체 스캔과 관련해서는 튜인 요소가 많지 않지만, 인덱스와 관련해서는 튜닝 요소가 매우 많고 기법도 다양하다.

인덱스 튜닝의 두 가지 핵심요소

  1. 인덱스 스캔 과정에서 발생하는 비효율을 줄이는 것. 즉, 인덱스 스캔 효율화 튜닝
  • 학생명부에서 시력이 1.0 ~ 1.5 인 홍길동 학생을 찾는 경우를 예로 들어보자.
  • 학생명부를 이름과 시력순으로 정렬해 두었다면 소량만 스캔하면 되지만 학생명부를 시력과 이름순으로 정렬해 두었다면 똑같이 두명을 찾는데도 많은 양을 스캔해야 한다.
  1. 테이블 액세스 횟수를 줄이는 것. 즉, 랜덤 액세스 최소화 튜닝
  • 시력이 1.0 ~ 1.5인 학생은 50명이고 이름이 홍길동인 학생은 5명이고, 시력이 1.0 ~ 1.5인 홍길동 학생은 2명이다.
    • 이름과 시력만으로 각각 정렬한 학생명부가 있다면 어느 쪽이 더 효과적일까?
    • 당연히, 이름순으로 정렬한 학생명부를 선택하면 교실을 다섯 번만 찾아가면 되기 때문에 유리하다.
    • 어떤 학생명부를 사용하느냐에 따라 교실 방문 횟수는 다르다.

  • 인덱스 스캔 효율화 튜닝과 랜덤 액세스 최소화 튜닝 둘 다 중요하지만, 더 중요한 하나를 고른다면 랜덤 액세스 최소화 튜닝이다.
  • 홍길동 학생을 찾는 예만 하더라도 학생명부를 스캔하는 과정에도 비효율이 있겠지만, 학생명부에 없는 나머지 정보를 얻기 위해 직접 교실을 찾아가는 부담이 훨씬 더 크다.
  • 이 예제가 말해주는건 인덱스 튜닝의 핵심요소이며 가장 중요한 결론은 SQL 튜닝은 랜덤 I/O와의 전쟁이다.

SQL 튜닝은 랜덤 I/O 와의 전쟁

  • 데이터베이스 성능이 느린 이유는 디스크 I/O 때문이다.
  • 읽어야 할 데이터량이 많고, 그 과정에 I/O가 많이 발생할 때 느리다.

2.1.1 인덱스 구조

  • 인덱스 : 대용량 테이블에서 필요한 데이터만 빠르게 효율적으로 액세스하기 위해 사용하는 오브젝트이다.
  • 즉, 책 뒤쪽에 있는 색인과 같은 역할을 한다.
  • 데이터베이스에서도 인덱스 없이 데이터를 검색하려면, 테이블을 처음부터 끝까지 모두 읽어야 한다.
  • 반면, 인덱스를 이용하면 일부만 읽고 멈출 수 있으며 범위 스캔이 가능하다. 범위 스캔이 가능한 이유는 인덱스가 정렬돼 있기 때문이다.

  • DBMS는 일반적으로 B+Tree 인덱스를 사용하는데 구조는 위의 그림과 같다.
  • 나무를 거꾸로 뒤집은 모양으로 뿌리(Root)가 위에 있고 가지(Branch)를 거쳐 맨 아래에 잎사귀(Leaf)가 존재한다.
  • 리프 블록에 저장된 각 레코드는 키값 순으로 정렬돼 있을 뿐만 아니라 테이블 레코드를 가리키는 주소값 ROWID를 갖으며 인덱스 키값이 같으면 ROWID 순으로 정렬된다.
  • 인덱스를 스캔하는 이유는 검색 조건을 만족하는 소량의 데이터를 빨리 찾고 거기서 ROWID를 얻기위함이다.
  • ROWID = 데이터 블록 주소 + 로우 번호
  • 데이터 블록 주소 = 데이터 파일 번호 + 블록 번호
  • 블록 번호 : 데이터파일 내에서 부여한 상대적 순번
  • 로우 번호 : 블록 내 순번
  • 수직적 탐색 : 인덱스 스캔 시작지점을 찾는 과정
  • 수평적 탐색 : 데이터를 찾는 과정

2.2.2 인덱스 수직적 탐색

  • 인덱스 수직적 탐색은 정렬된 인덱스 레코드 중 조건을 만족하는 첫 번째 레코드를 찾는 과정이며 즉, 인덱스 스캔 시작지점을 찾는 과정이다.
  • 루트를 포함해 브랜치 블록에 저장된 각 인덱스 레코드는 하위 블록에 대한 주소값을 갖는다. 이것이 루트에서 시작해 리프블록까지 수직적 탐색이 가능한 이유이다.
  • 수직적 탐색 과정에 찾고자 하는 값보다 크거나 같은 값을 만나면, 바로 직전 레코드가 가리키는 하위 블록으로 이동한다.
  • 인덱스를 수직적으로 탐색할 때는, 등산 푯말과 같은 역할을 한다.
  • 이는, 조건을 만족하는 첫 번째 레코드가 목표 지점임을 기억하자.

2.2.3 인덱스 수평적 탐색

  • 수직적 탐색을 통해 스캔 시작점을 찾았으면, 찾고자 하는 데이터가 더 안나타날 때까지 인덱스 리프 블록을 수평적으로 스캔한다. 이는 인덱스에서 본격적으로 데이터를 찾는 과정이다.
  • 인덱스 리프 블록끼리는 서로 앞뒤 블록에 대한 주소값을 갖는 양방향 연결 리스트(double linked list) 구조이다.
  • 인덱스를 수평적으로 탐색하는 이유는 조건절을 만족하는 데이터를 모두 찾고 ROWID를 얻기 위해서이다.
  • 필요한 컬럼을 인덱스가 모두 갖고 있어 인덱스만 스캔하고 끝나는 경우도 있지만, 일반적으로 액세스를 스캔하고서 테이블도 액세스한다. (이 때 ROWID가 필요하다.)

2.2.4 결합 인덱스 구조와 탐색

SELECT 
    이름, 설명
FROM 
    사원
WHERE 
    성별 = '여자'
    AND 이름 = '유관순'
  1. 인덱스를 성별 + 이름 순으로 구성한 경우
    • 총 사원 50명 중 성별 = 여자인 레코드 25건을 찾고, 이름을 검사해 최종적으로 2명 출력 -> 25번의 검사
  2. 인덱스를 이름+ 성별 순으로 구성한 경우
    • 총 사원 50명 중 이름 = '유관순'인 레코드 2건을 찾고, 거기서 성별을 검사해 최종적으로 2명 출력 -> 2번의 검사
  • 여기서 알 수 있듯, 선택도가 낮은 '이름' 컬럼을 앞쪽에 두고 결합인덱스를 생성해야 검사횟수를 줄일 수 있어 성능에 유리하다고들 알고 있다.
  • 하지만 엑셀의 데이터 필터 기능과 같은 위와 달리 B+TREE 인덱스는 엑셀처럼 평면 구조가 아니기에 어느 컬럼을 앞에 두든 일량에는 차이가 없다.

2.2 인덱스 기본 사용법

  • 인덱스 기본 사용법은 인덱스를 Range Scan 하는 방법을 의미한다.
  • 인덱스를 Range Scan 할 수 없게 되는 이유를 알고 나면, Range Scan 하는 방법도 자연스럽게 터득한다.
  • 인덱스 컬럼을 가공하지 않아야 인덱스를 정상적으로 사용할 수 있다.
  • 인덱스를 정상적으로 사용한다라는 표현은 리프 블록에서 스캔 시작점을 찾아 거기서부터 스캔하다가 중간에 멈추는 것을 의미한다. (리프블록 일부만 스캔하는 Index Range Scan)
  • 인덱스 컬럼을 가공해도 인덱스를 사용할 수는 있지만, 스캔 시작점을 찾을 수 없고 멈출 수 없어 리프 블록 전체를 스캔해야만 한다. (일부가 아닌 전체 스캔하는 Index Full Scan)

2.2.1 인덱스를 Range Scan 할 수 없는 이유

모든 SQL 튜닝 책은 이와 같이 말한다. 인덱스 컬럼을 가공하면 인덱스를 정상적으로 사용할 수 없다.

  • 기본 중에서 기본에 해당하므로 모르는 사람은 없으나, 이유에 대해서는 잘설명하지 못한다 이유는 무엇일까??
  • 이를 설명하기 위해서는 앞서 말한 수직적, 수평적 탐색인 인덱스 탐색 과정을 알아야 한다.
  • 인덱스 컬럼을 가공했을 때 인덱스를 정상적으로 사용할 수 없는 이유는 인덱스 스캔 시작점을 찾을 수 없기 때문이다.
  • 쉽게 말하면 Index Range Scan은 범위를 인덱스에서 범위를 스캔한다는 뜻인데, 일정 범위를 스캔하려면 시작 지점과 끝지점이 존재해야한다.

아래 예제를 통해서 알아보자

  • 2007년 1월 1일 이후에 태어난 첫 번째 학생을 찾는 과정이 인덱스로 말하면 수직적 탐색에 해당한다.
  • 데이터베이스에서 아래 조건절을 처리할 때도 똑같은 과정을 거친다.
where 생년월일 between '20070101' and '20070131'
  • 이번에는 년도와 상관없이 5월에 태어난 학생을 찾아보자. 스캔 시작점은 어디고 어디서 멈춰야 할까??
where substr(생년월일, 5, 2) = '05'
  • 위와 같이 조건절을 처리할 경우 인덱스에는 가공되지 않은 값이 저장되어있어 스캔 시작점과 끝지점을 찾을 수 없다.
  • 아래 예제도 전부 마찬가지로 해당한다.
where nvl(주문수량, 0) < 100
where 업체명 like '%대한%'
where (전화번호 = :tel_no OR 고객명 = :cust_nm)
where (전화번호 in :tel_no1, :tel_no2)
  • SQL을 아래와 같이 UNION ALL 방식으로 작성하면 각 브랜치 별로 인덱스 스캔 시작점을 찾을 수 있으므로 Range Scan이 가능하다.
  • 위와 같은 쿼리를 UNION ALL 방식으로 아래와 같이 작성하면 Index Range Scan 을 사용할 수 있다.
select *
from 고객
where 고객명 = :cust_nm
union all 
select *
from 고객
where 전화번호 = :tel_no
and (고객명 <> :cust_nm or 고객명 is null)

select * 
from 고객
where 전화번호 = :tel_no1
select *
from 고객
where 전화번호 = :tel_no2
  • 번외로 IN 조건절에 대해서는 SQL 옵티마이저가 IN_List Iterator 방식을 사용하여, IN-List 개수만큼 Index Range Scan을 반복함으로써 UNION ALL 으로 변환한 것과 같은 효과를 얻을 수가 있다.
  • 정리하자면 위에서 정의한 nvl이나 like 처럼 가공한 인덱스 컬럼을 정상적으로 Index를 사용할 수 없으나, OR 또는 IN 조건절은 옵티마이저의 쿼리 변환을 통해 Index Range Scan 으로 처리되기도 한다.

2.2.2 더 중요한 인덱스 사용 조건

  • 인덱스를 정상적으로 사용하는데 있어 더 중요한 선행조건이 있다.
  • 인덱스를 아래 그림과 같이 소속팀 + 사원명 + 연령 순으로 구성했다고 가정하자.

SELECT 사원번호, 소속팀, 연령, 입사일자, 전화번호
FROM    사원
WHERE 사원명 = '홍길동'
  • 데이터를 소속팀 순으로 정렬하고, 소속팀이 같으면 사원명 순으로 정렬하고, 사원명까지 같으면 연령순으로 정렬한다는 의미이다.
  • 이름이 같은 사원이더라도 소속팀이 다르면 서로 멀리 떨어져 인덱스 스캔 시작점을 찾을 수 없고, 어디서 멈춰야 할지도 알 수 없다. (인덱스 리프 블록을 처음부터 끝까지 모두 스캔해야 한다.)
  • 인덱스를 Range Scan 하기 위한 가장 첫 번째 조건은 인덱스 선두 컬럼이 조건절에 있어야 한다는 사실이다.

아래 예제는 인덱스 컬럼을 가공했는데 Range Scan하는 경우의 예이다.

-- TXA1234_TX02 인덱스 : 기준연도 + 과세구분코드 + 보고회차 + 실명확인번호

SELECT * FROM 
WHERE 기준연도 = :stdr_year
AND SUBSTR(과세구분코드, 1, 4) =: txtn_dcd
AND 보고회차 = :rpt_tmrd
AND 실명확인번호 = :rnm_cnfm_no
  • 인덱스를 Range Scan 하려면 인덱스 선두 컬럼이 가공되지 않은 상태로 조건절에 있어야 한다.
  • 반대로 말해 인덱스 선두 컬럼이 가공되지 않은 상태로 조건절에 있으면 인덱스 Range Scan은 무조건 가능하다.
  • 위의 예제에서는 선두 컬럼인 '기준연도'를 가공하지 않았으므로 인덱스 Range Scan이 가능하다.

인덱스 잘타니까 튜닝 끝??

  • SQL을 개발하면서 실행계획을 확인하지 않는 개발자가 대다수이다.
  • 확인하더라도 인덱스를 타는지, 안타는지 확인하는 수준에 그친다. (인덱스를 잘 타면 성능도 문제없다고 생각한다.)
  • 인덱스를 정말 잘 타는지는 인덱스 리프 블록에서 스캔하는 양을 따져봐야 알 수 있다.
SELECT *
FROM 주문상품
WHERE 주문일자 =:ord_dt
AND 상품번호 LIKE '%PING%'

SELECT *
FROM 주문상품
WHERE 주문일자 =:ord_dt
AND SUBSTR(상품번호, 1, 4) = 'PING';
  • 위 SQL에서는 상품번호는 스캔 범위를 줄이는데 전혀 역할을 하지 못한다.
  • 첫 번째 SQL은 중간 값 검색이기 때문이고, 두 번재 SQL은 컬럼을 가공했기 때문이다.

2.2.3 인덱스를 이용한 소트 연산 생략

  • 인덱스를 Range Scan 할 수 있는 이유는 데이터가 정렬돼 있기 때문이다.
  • 찾고자 하는 데이터가 정렬된 상태로 서로 모여있기 때문에 전체가 아닌 일정 부분만 읽다가 멈출 수 있다.
  • 인덱스 컬럼을 가공해도 인덱스를 사용할 수 있지만, 찾고자 하는 데이터가 전체 구간에 흩어져 있기 때문에 Range Scan이 불가능하거나 비효율이 발생한다.
  • 하지만 테이블과 달리 인덱스는 정렬돼어있기 때문에 Range Scan이 가능하고, 소트 연산 생략효과도 부수적으로 얻게 된다.

  • 위 그림에서는 장비번호 + 변경일자 + 변경순번으로 PK가 구성되어있다.
SELECT *
FROM 상태변경이력
WHERE 장비번호 = 'C'
AND 변경일자 = '20180316'
  • 위와 같이 장비번호와 변경일자를 모두 '=' 조건으로 검색할 때 PK 인덱스를 사용하면 결과집합은 변경순번 순으로 출력된다.
  • 옵티마이저는 이런 속성을 활용해 SQL에 ORDER BY 가 있어도 정렬 연산을 따로 수행하지 않는다.
  • PK 인덱스를 스캔하면서 출력한 결과집합은 어차피 변경순번 순으로 정렬되기 때문이다.
  • 실행계획에 SORT ORDER BY 연산이 없음을 확인해보자.

2.2.4 ORDER BY 절에서 컬럼 가공

  • 인덱스 컬럼을 가공하면 인덱스를 정상적으로 사용할 수 없다 라고 말하는 인덱스 컬럼은 대게 조건절에 사용한 컬럼을 의미한다.
  • 그런데 조건절이 아닌 ORDER BY 또는 SELECT-LIST 에서 컬럼을 가공함으로 인해 인덱스를 정상적으로 사용할 수 없는 경우도 종종 있다.

아래와 같은 예제를 주의하자

SELECT * 
FROM
(
    SELECT TO_CHAR(A.주문번호, 'FM000000') AS 주문번호, A.업체번호, A.주문금액
        FROM 주문 A
    WHERE A.주문일자 = :dt
      AND A.주문번호 > NVL(:next_ord_no, 0)
    ORDER BY 주문번호
)
WHERE ROWNUM <= 30
  • 위의 예제는 주문일자와 주문번호가 PK로 되어있음에도 정렬 연산(SORT ORDER BY) 연산이 실행계획에 나타난다. 이유는 무엇일까??
  • 원인은 ORDER BY 에 기술한 주문번호는 순수 주문번호가 아니라 TO_CHAR 함수로 한 번 가공한 주문번호를 가리키기 때문이다. 해결방법은 간단하게 아래와 같이 작성하면 된다.
SELECT * 
FROM
(
    SELECT TO_CHAR(A.주문번호, 'FM000000') AS 주문번호, A.업체번호, A.주문금액
        FROM 주문 A
    WHERE A.주문일자 = :dt
      AND A.주문번호 > NVL(:next_ord_no, 0)
    ORDER BY A.주문번호
)
WHERE ROWNUM <= 30
  • 간단히 ORDER BY 절에 주문 테이블의 주문번호를 가리키는 Alias 를 붙여주기만 하면 된다.

2.2.5 SELECT-LIST 에서 컬럼 가공

  • 인덱스를 장비번호 + 변경일자 + 변경순번 순으로 구성하면, 아래와 같이 변경순번 최소값을 구할 때도 옵티마이저는 정렬 연산을 따로 수행하지 않는다.
  • 수직적 탐색을 통해 조건을 만족하는 가장 왼쪽 지점으로 내려가서 첫 번째 읽는 레코드가 바로 최소값이기 때문이다.
SELECT MIN(변경순번)
FROM 상태변경이력
WHERE 장비번호 = 'C'
  AND 변경일자 = '20180316'
  • 인덱스의 리프블록은 양방향 구조(DOUBLE LINKED LIST) 구조를 가지고 있기 때문에 최대값도 이하동문이다.
  • 하지만 만약에 SQL을 아래와 같이 작성하면 정렬 연산을 생략할 수 없다. 이유는 뭘까??
SELECT NVL(MAX(TO_NUMBER(변경순번)), 0)
FROM 상태변경이력
WHERE 장비번호 = 'C'
  AND 변경일자 = '20180316'
  • 인덱스에는 문자열 기준으로 정렬돼 있는데, 이를 숫자값으로 변경하여 최종 변경순번을 요구하였기 때문에 SORT 연산이 추가된다.
  • 이를 아래와 같이 바꾸면 정렬 연산 없이 최종 변경순번을 쉽게 찾을 수 있다.
SELECT NVL(TO_NUMBER(MAX(변경순번)), 0)
FROM 상태변경이력
WHERE 장비번호 = 'C'
  AND 변경일자 = '20180316'
  • 하지만 이는 애초에 변경순번 데이터타입을 숫자형으로 설계했다면 이렇게 튜닝할 일도 생기지 않는다.

또 다른 예제를 통해 알아보자

  • 아래 SQL은 장비구분코드 = 'A001'에 해당하는 장비들의 최종 변경일자를 스칼라 서브쿼리를 이용해 상태변경이력 테이블에서 조회하고 있다.
SELECT 장비번호, 장비명, 상태코드
      , (SELECT MAX(변경일자)
         FROM 상태변경이력
         WHERE 장비번호 = P.장비번호) 최종변경일자
FROM 장비 P
WHERE 장비구분코드 = 'A001'
  • 정렬 연산 없이 MIN/MAX, FIRST ROW 방식으로 실행하고 있음을 실행계획에서 확인할 수 있다.
  • 하지만 최종변경일자에 더해 최종 변경순번까지 출력하고자 한다면 SQL을 어떻게 작성해야 할까?
SELECT 장비번호, 장비명, 상태코드
      , (SELECT MAX(변경일자)
         FROM 상태변경이력
         WHERE 장비번호 = P.장비번호) 최종변경일자
      , (SELECT MAX(변경순번)
         FROM 상태변경이력
         WHERE 장비번호 = P.장비번호
         AND 변경일자 = (SELECT MAX(변경일자)
                        FROM 상태변경이력
                        WHERE 장비번호 = P.장비번호)) 최종변경순번
FROM 장비 P
WHERE 장비구분코드 = 'A001'
  • 위와 같이 작성하면 상태변경이력 테이블을 여러 번 읽어야하므로 비효율적이며, PK 컬럼이 더 많아질 수록 복잡해지므로 성능도 나빠진다.
  • 아래와 같이 작성하면 PK 컬럼이 많아져도 덜 복잡해지며 이런식으로 많이 코딩한다.
SELECT 장비번호, 장비명, 상태코드
      , SUBSTR(최종이력, 1, 8) 최종변경일자
      , SUBSTR(최종이력, 9) 최종변경순번
FROM (
    SELECT 장비번호, 장비명, 상태코드
            ,(SELECT MAX(변경일자 || 변경순번)
              FROM 상태변경이력
              WHERE 장비번호 = P.장비번호) 최종이력
    FROM 장비 P
    WHERE 장비구분코드 = 'A001'
)
  • 각 장비당 이력이 많지 않으면 크게 상관없지만, 이력이 많다면 성능에 문제가 될 수 있는 패턴이다. (인덱스 컬럼을 가공했기 때문)
  • 각 장비에 속한 과거 이력 데이터를 모두 읽어야하므로 장비당 이력 레코드가 많다면 바로 직전에 본 복잡한 SQL보다 성능이 더 안 좋을 수 있다.
  • 이럴 때 SQL은 어떻게 작성해야 효과적인지는 Top N 알고리즘을 설명하는 5장에서 소개한다.

2.2.6 자동 형변환

SELECT * FROM 고객 
WHERE 생년월일 = 19821225
  • 위의 SQL같은 경우에는 인덱스를 가공하지 않았음에도 불구하고 테이블 전체 스캔을 선택했다.
  • 이유는 조건절 비교값을 숫자형으로 표현했기 때문에 나타난 현상이다. (아래와 같이 실행계획이 나타난다.)
SELECT * FROM 고객 WHERE TO_NUMBER(생년월일) = 19821225
  • 제대로 사용하려면 아래와 같이 날짜 포맷을 정학히 지정해 주는 코딩 습관이 필요하다.
SELECT * FROM 고객 WHERE 가입일자 = TO_DATE('01-JAN-2018', 'DD-MON-YYYY')
  • 숫자형과 문자형이 만날 경우 숫자형이 이기지만 연산자가 LIKE일 경우에는 다르다.
  • LIKE 자체가 문자열 비교 연산자이므로 문자형 기준으로 숫자형 컬럼이 변환된다.
  • 실무에서 일을 하다보면 LIKE 조건을 옵션 조건 처리 목적으로 사용하는 경우가 종종 있다.
  • 예를 들어 거래 데이터 조회시 계좌번호는 사용자가 입력할 수도 안 할 수도 있는 옵션 조건인데, 이를 처리하기 위해서는 아래와 같이 두 개의 SQL이 필요하다.
-- 사용자가 계좌번호를 입력할 경우
SELECT * FROM 거래
WHERE 계좌번호 = :acnt_no
AND 거래일자 between :trd_dt1 and :trd_dt2

-- 사용자가 계좌번호를 입력하지 않을 경우 
SELECT * FROM 거래
WHERE 거래일자 between :trd_dt1 and :trd_dt2
  • 이를 하나의 SQL로 처리하기 위해 다양한 방식을 사용하는데 LIKE 조건을 사용하는 방식이 그 중 하나이다.
  • 조회할 때 사용자가 계좌번호를 입력하지 않으면 :acnt_no 변수에 NULL 값을 입력함으로써 모든 계좌번호가 조회되도록 하는 것이다.
SELECT * FROM 거래
WHERE 계좌번호 LIKE :acno_no || '%'
AND 거래일자 between :trd_dt1 and :trd_dt2
  • 위의 방식을 사용하면 LIKE와 BETWEEN 조건을 같이 사용하여 인덱스 스캔 효율이 안 좋아진다.
  • 숫자형 컬럼을 LIKE 조건으로 검색하면 자동 형봔으로 인해 계좌번호가 아예 인덱스 액세스 조건으로 사용되지 못하기 때문이다.
반응형
Comments