친절한 SQLP 튜닝
2장
- 인덱스 탐색 과정은 수직적 탐색과 수평적 탐색 두 단계로 이루어진다.
- 인덱스는 큰 테이블에서 소량 데이터를 검색할 때 사용한다.
- 인덱스 튜닝의 두 가지 핵심요소
- 인덱스 스캔 과정에서 발생하는 비효율 줄이는 것 = 인덱스 스캔 효율화 튜닝
- 테이블 엑세스 횟수를 줄이는 것 = 랜덤 엑세스 최소화 튜닝
- IOT, 클러스터, 파티션, Prefetch, Batch I/O 등의 기능들의 본질은 랜덤 I/O의 최소화이다.
- NL 조인이 대량 조인할 때 느린 이유도 랜덤I/O 때문이다. 이를 위해 소트머지 조인과 해시 조인이 개발되었다.
- DBMS는 일반적으로 B*Tree 인덱스를 사용한다.
- Root가 위쪽에 있고, Branch를 거쳐 맨 아래 Leaf가 있다.
- Root와 Branch 블록에 있는 각 레코드는 하위 블록에 대한 주소값을 갖는다.
- Root와 Branch 블록에는 주소값을 갖지 않는 LMC(Leftmost Child)라는 특별한 레코드가 있다.
- Leaf 블록에 저장된 각 레코드는 키값 순으로 정렬되어 있을 뿐만 아니라 테이블 레코드를 가리키는 주소값, 즉 ROWID를 갖는다.
- ROWID = 데이터 블록 주소 + 로우 번호
- 데이터 블록 주소 = 데이터 파일 번호 + 블록 번호
- 블록 번호 : 데이터 파일 내에서 부여한 상대적 순번
- 로우 번호 : 블록 내 순번
- 인덱스 수직적 탐색 : 인덱스 스캔 시작지점을 찾는 과정 = 인덱스 레코드 중 조건을 만족하는 첫 번째 레코드를 찾는 과정이다. 즉, 인덱스 스캔 시작지점을 찾는 과정이다.
- 인덱스 수평적 탐색 : 데이터를 찾는 과정 = 찾고자 하는 데이터가 더 안 나타날 때까지 인덱스 리프 블록을 수평적으로 스캔한다.
- 조건절에 만족하는 데이터를 모두 찾기 위해서, ROWID를 얻기 위해서 수평적 탐색을 수행한다.
- 인덱스 선두 컬럼을 모두 등치(=) 조건으로 검색할 때는 어느 컬럼을 인덱스 앞쪽에 두든 블록 I/O 개수가 같으므로 성능도 똑같다. 즉 , 선두 컬럼이 모두 등치 조건이면 수직적 탐색 과정의 비용이 같다는 것이다.
- B*Tree 인덱스에서 어떤 값으로 탐색하더라도 인덱스 루트에서 리프 블록에 도달하기까지 읽는 블록 수가 같다.
- 인덱스 기본 사용법은 인덱스를 Range Scan 하는 방법을 의미한다.
- 인덱스 컬럼(정확히는 선두 컬럼)을 가공하지 않아야 인덱스를 정상적으로 사용할 수 있다. '인덱스를 정상적으로 사용한다'는 표현은 리프 블록에서 스캔 시작점을 찾아 거기서부터 스캔하다가 중간에 멈추는 것을 의미한다. 즉, 리프 블록 일부만 스캔하는 Index Range Scan을 의미한다.
- 인덱스 컬럼을 가공해도 인덱스를 사용할 수는 있지만, 스캔 시작점을 찾을 수 없고 멈출 수도 없어 리프 블록 전체를 스캔해야만 한다. 즉, 일부가 아닌 전체를 스캔하는 Index Full Scan 방식으로 작동한다.
- Index Range Scan은 인덱스에서 일정 범위를 스캔한다는 뜻이다. 일정 범위를 스캔하려면 시작지점과 끝지점이 있어야 한다. 인덱스 컬럼을 가공했을 때 인덱스 스캔 시작점을 찾을 수 없기 때문에 정상적으로 사용할 수 없다.
- OR 조건식을 SQL 옵티마이저가 UNION ALL 형태로 변환할 수 있는데 이를 'OR Expansion'이라고 한다.
- use_concat 힌트를 통해 'OR Expansion'를 유도할 수 있다.
- IN 조건은 OR 조건을 표현하는 다른 방식일 뿐이다. IN 조건절에 대해서는 SQL 옵티마이저가 IN-List Iterator 방식을 사용한다. IN-List 개수만큼 Index Range Scan을 반복하는 것이다. UNION ALL 방식으로 변환한 것과 같은 효과를 얻을 수 있다.
- 인덱스를 Range Scan 하기 위한 가장 첫 번째 조건은 인덱스 선두 컬럼이 조건절에 있어야 한다는 사실이다. 가공하지 않은 상태로 말이다.
- 문제는, 인덱스를 Range Scan 한다고 해서 항상 성능이 좋은 것은 아니라는 것이다.
- '인덱스를 탄다'라는 표현은 '인덱스를 Range Scan한다'와 같은 의미인 셈이다. 그런데 인덱스를 정말 잘 타는지는 인덱스 리프 블록에서 스캔하는 양을 따져봐야 알 수 있다.
- 인덱스 컬럼을 가공해도 인덱스를 사용할 순 있지만, 찾고자 하는 데이터가 전체 구간에 흩어져 있기 때문에 Range Scan이 불가능하거나 비효율이 발생한다.
- 옵티마이저는 SQL에 ORDER BY가 있어도 정렬 연산을 따로 수행하지 않는다. 인덱스를 스캔하면서 출력한 결과 집합은 어차피 인덱스 컬럼 순으로 정렬되기 때문이다. 따라서 실행계획에 SORT 연산이 없다.
- 조건절이 아닌 ORDER BY 또는 SELECT-LIST에서 컬럼을 가공함으로 인해 인덱스를 정상적으로 사용할 수 없는 경우도 종종 있다.
- 인덱스를 이용해 정렬 연산 없이 최소 또는 최대값을 빠르게 찾을 때 인덱스 리프블록의 왼쪽(MIN) 또는 오른쪽(MAX)에서 레코드 하나(FIRST ROW)만 읽고 멈춘다.
- 인덱스는 문자열 기준으로 정렬돼 있는데 이를 숫자값으로 바꾼 값 기준으로 최소 또는 최대값, ORDER BY를 요구하면 정렬 연산을 생략할 수 없다.
- 오라클은 자동으로 형변환 처리해준다.
- 오라클에서 숫자형과 문자형이 만나면 숫자형이 이긴다. 숫자형 컬럼 기준으로 문자형 컬럼을 변환한다는 뜻이다.연산자가 LIKE 일 때는 다르다. LIKE 자체가 문자열 비교 연산자이므로 이때는 문자형 기준으로 숫자형 컬럼이 변환된다. 숫자형 컬럼을 LIKE 조건으로 검색하면 자동 형변환이 발생해 인덱스 엑세스 조건으로 사용되지 못한다.
- 날짜형과 문자형이 만나면 날짜형이 이긴다.
- DECODE(A, B, C, D)를 처리 할 때 'A = B' 이면 C를 반환하고, 아니면 D를 반환한다. 이때 반환값의 데이터 타입은 세 번째 인자 C에 의해 결정된다. 세 번째 인자가 NULL 값이면 네 번 째 인자가 숫자형이어도 문자열로 변환하고, 문자열 기준으로 MAX() 를 수행하게 된다. 950보다 5000이 크다고 판단하게 된다.
- SQL 성능 원리를 잘 모르는 개발자는 TO_CHAR, TO_DATE, TO_NUMBER 같은 형변환 함수를 의도적으로 생략하곤 한다. 이들 함수를 생략하면 연산횟수가 줄어 성능이 더 좋지 않을까 생각하기 때문이다. SQL 성능은 그런 데서 결정되는 게 아니라 블록 I/O를 줄일 수 있느냐 없느냐에서 결정된다. 형변환 함수를 생략한다고 연산 횟수가 주는 것도 아니다. 개발자가 형변환 함수를 생략해도 옵티마이저가 자동으로 생성한다.
- Index Range Scan 이외에도 Index Full Scan, Index Uinque Scan, Index Skip Scan, Index Fast Full Scan 등이 있다.
- Index Range Scan은 인덱스 루트에서 리프 블록까지 수직적으로 탐색한 후에 필요한 범위만 스캔한다.
- 선두 컬럼을 가공하지 않은 상태로 조건절에 사용하면 Index Range Scan이 무조건 가능하다. 그러므로 성능은 인덱스 스캔 범위, 테이블 엑세스 횟수를 얼마나 줄일 수 있느냐로 결정된다.
- Index Full Scan은 수직적 탐색없이 인덱스 리프 블록을 처음부터 끝까지 수평적으로 탐색하는 방식이다.
- Index Full Scan은 대개 데이터 검색을 위한 최적의 인덱스가 없을 때 차선으로 선택된다.
- 대용량 테이블이어서 옵티마이저는 Table Full Scan을 고려하지만, 대용량 테이블의 경우 Table Full Scan이 부담스러워서 옵티마이저는 인덱스 활용을 고려하지 않을 수 없다.
- /*+ first_rows */ 힌트를 사용하면 옵티마이저가 모드를 바꿔 인덱스 Full Scan을 고려한다. 이는 인덱스를 활용하므로 Sort 연산 생략 효과를 가져온다.
- Index Unique Scan은 수직적 탐색만으로 데이터를 찾는 스캔 방식으로서 Unique 인덱스를 '=' 조건으로 탐색하는 경우에 작동한다.
- Unique 인덱스가 존재하는 컬럼은 중복 값이 입력되지 않게 DBMS가 데이터 정합성을 관리한다. 따라서 해당 인덱스 키 컬럼을 모두 '=' 조건으로 검색할 때는 데이터를 한 건 찾는 순간 더 이상 탐색할 필요가 없다.
- Unique 결합 인덱스에 대해 일부 컬럼으로만 검색할 때도 Index Range Scan이 나타난다.
- 옵티마이저는 Table Full Scan보다 I/O를 줄일 수 있거나 정렬된 결과를 쉽게 얻을 수 있다면, Index Full Scan을 사용하기도 한다.
- 오라클은 인덱스 선두 컬럼이 조건절에 없어도 인덱스를 활용하는 새로운 스캔 방식이 Index Skip Scan이다. 이 스캔 방식을 조건절에 빠진 인덱스 선두 컬럼의 Distinct Value 개수가 적고 후행 컬럼의 Distinct Value 개수가 많을 때 유용하다. 예를 들어 고객 테이블에서 Distinct Value가 적은 컬럼을 '성별'이라고 할 수 있다.
- Index Skip Scan은 루트 또는 브랜치 블록에서 읽은 컬럼 값 정보를 이용해 조건절에 부합하는 레코드를 포함할 '가능성이 있는' 리프 블록만 골라서 엑세스하는 스캔 방식이다.
- 인덱스 선두 컬럼이 없을 때만 Index Skip Scan이 작동하는 것은 아니다.
- 선두 컬럼에 대한 조건절은 있고, 중간 컬럼에 대한 조건절이 없는 경우에도 Skip Scan을 사용할 수 있다.
- /*+ INDEX_SS(A 일별업종별거래_PK) */
- Index Skip Scan을 사용한다면, 업종유형코드 = '01'인 구간에서 기준일자가 '20080501'보다 크거나 같고 '20080531'보다 작거나 같은 레코드를 '포함할 가능성이 있는 리프 블록만' 골라서 엑세스할 수 있다.
- 선두컬럼이 부등호, BETWEEN, LIKE 같은 범위검색 조건일 때도 Index Skip Scan을 사용할 수 있다.
- Index Range Scan을 사용한다면, 기준일자 BETWEEN 조건을 만족하는 인덱스 구간을 모두 스캔해야 한다. Index Skip Scan을 사용한다면, 기준일자 BETWEEN 조건을 만족하는 인덱스 구간에서도 업종유형코드 = '01'인 레코드를 포함할 가능성이 있는 리프 블록만 골라서 엑세스할 수 있다. 이처럼 Index Range Scan이 불가능하거나 효율적이지 못한 상황에서 Index Skip Scan이 종종 빛을 발한다.
- 인덱스는 기본적으로 최적의 Index Range Scan을 목표로 설계해야 하며, 수행 횟수가 적은 SQL을 위해 인덱스를 추가하는 것이 비효율적일 때 이들 스캔 방식을 차선책으로 활용하는 전략이 바람직하다.
- Index Fast Full Scan이 Index Full Scan 보다 빠른 이유는, 논리적인 인덱스 트리 구조를 무시하고 인덱스 세그먼트 전체를 Multiblock I/O 방식으로 스캔하기 때문이다. 관련 힌트 : index_ffs, no_index_ffs
- Index Fast Full Scan은 물리적으로 디스크에 저장된 순서대로 인덱스 리프 블록들을 읽어들인다.
- 결과집합이 인덱스 키 순서대로 정렬되지 않는다. 쿼리에 사용한 컬럼이 모두 인덱스에 포함돼 있을 때만 사용할 수 있다는 점도 기억할 필요가 있다.
- Index Range Scan 또는 Index Full Scan과 달리, 인덱스가 파티션 돼 있지 않더라도 병렬 쿼리가 가능한 것도 중요한 특징 중 하나이다. 병렬 쿼리 시에는 Direct Path I/O 방식을 사용하기 때문에 속도가 더 빨라진다.
- Table Full Scan은 읽어야 할 익스텐트 목록을 익스텐트 맵에서 얻는다. Index Fast Full Scan도 똑같은 방식을 사용한다.
- Index Fast Full Scan 특징
- 세그먼트 전체를 스캔
- 결과집합 순서 보장 안 됨
- Multiblock I/O
- 병렬스캔 가능
- 인덱스에 포함된 컬럼으로만 조회할 때 사용 가능
- Index Full Scan 특징
- 인덱스 (논리적) 구조를 따라 스캔
- 결과집합 순서 보장
- Single Block I/O
- (파티션 돼 있지 않다면) 병렬스캔 불가
- 인덱스에 포함되지 않은 컬럼 조회시에도 사용 가능
- Index Range Scan Descending은 인덱스를 뒤에서부터 앞쪽으로 스캔하기 때문에 내림차순으로 정렬된 결과집합을 얻는다는 점만 다르다.
- 내림차순 Order by나 MAX 값을 구하고자 하는 경우 등에 사용된다.
'OLD' 카테고리의 다른 글
ABAP - MEMO (0) | 2025.02.14 |
---|---|
[SAP SD] VBFA 이용한 Sales Order - Delivery - Billing item 레벨 조회 쿼리 (0) | 2025.02.13 |
데이터베이스 기초 3. SQL로 데이터베이스, 테이블 정의(MySQL) (0) | 2024.12.17 |
데이터베이스 기초 2. 관계형 데이터베이스(Relational datamodel) (2) | 2024.12.11 |
데이터베이스 기초 1. 데이터베이스 관련 용어와 개념 (1) | 2024.12.11 |