UPDATE의 경우 해당되는 인덱스만 찾아서 변경해주면 된다. 다만, 테이블에서 한건을 변경할 때 마다 인덱스에는 두개의 오퍼레이션이 발생(삭제 후 삽입)
DML에서 인덱스 개수가 미치는 영향이 크다.
핵심 트랜잭션 테이블에서 인덱스의 개수를 하나라도 줄이면 TPS(Transcation Per Second)는 그만큼 향상된다.
인덱스가 존재할 때와 그러지 않을 때의 insert 시간 비교
왼쪽 인덱스가 없을때, 가운데 인덱스가 2개 존재할 때, 오른쪽 일반 인덱스와 PK 제약을 모두 제거한 상태
데이터베이스에 논리적으로 의미있는 자료만 저장되게 하는 데이터 무결성 규칙
개체 무결성
참조 무결성
도메인 무결성
사용자 정의 무결성
PK, FK 제약은 Check, Not Null 제약보다 성능에 더 큰 영향을 미친다
Check, Not Null은 정의한 제약 조건을 준수하지는만 확인하면 되지만, PK, FK 제약은 실제 데이터를 조회해 봐야 하기 때문
Update, Delete 문의 실행 계획이 Select 문의 실행 계획과 다르지 않으므로 인덱스 튜닝 원리를 그대로 적용할 수 있다. 조인 튜닝 또한 동일하게 적용 가능하다
인덱스도 select와 같이 적용된다
Redo 로깅과 DML 성능
데이터파일과 컨트롤 파일에 가해지는 모든 변경사항을 Redo 로그에 기록한다
Redo 로그는 트랜잭션 데이터가 어떤 이유에서건 유실되었을 때, 트랜잭션을 재현함으로써 유실 이전 상태로 복구하는데 사용된다
DML이 수행될 때마다 Redo 로그를 생성하므로 Redo 로깅은 DML 성능에 영향을 미친다
Redo 로그 목적
Database Recovery : Media Fail 발생 시 데이터베이스를 복구하기 위해 사용(온라인 Redo 로그를 백업해 둔 Archived Redo 로그 이용) → Media Recovery
Cache Recovery (Instnace Recovery 시 roll forward 단계) : 캐시에 저장된 변경사항이 디스크 상의 데이터 블록에 기록되지 않은 상태로 인스턴스가 비정상적으로 종료되면, 그때까지의 작업내용을 모두 잃게 된다.(버퍼캐시는 휘발성) 트랜잭션 데이터 유실에 대비하기 위해 Redo 로그를 남긴다
Fast Commit : 변경된 메모리 버퍼 블록을 디스크 상의 데이터 블록에 반영하는 작업은 랜덤 액세스 방식으로 이루어지므로 매우 느리지만 로그는 Append 방식으로 기록하므로 상대적으로 빠르다. 따라서 변경사항을 Append 방식으로 빠르게 로그 파일에 기록하고 변경된 메모리 버퍼 블록과 데이터 파일 블록 간 동기화는 DBWR을 이용해 나중에 배치 방식으로 일괄 수행한다. 아직 디스크에 기록되지 않았지만 Redo 로그를 믿고 커밋을 완료하는 것을 Fast Commit 이라고 한다
Undo 로깅과 DML 성능
Redo는 트랜잭션을 재현함으로써 과거를 현재 상태로 되돌리는 데 사용 Undo는 트랜잭션을 롤백함으로써 현재를 과거로 돌리는데 사용
Undo 데이터 사용 조건
Transaction Rollback : 트랜잭션에 의한 변경사항을 최종 커밋하지 않고 롤백할 때 Undo 데이터 사용
Transaction Recovery (Instance Recovery 시 rollback 단계) : Instance Crash 발생 후 Redo를 이용해 roll forward 단계가 완료되면 최종 커밋되지 않은 변경사항까지 모두 복구되므로 Undo 데이터를 사용하여 아직 커밋되지 않았던 트랜잭션들을 모두 롤백해야 한다
Read Consistency : 읽기 일관성에 사용
MVCC(Multi Version Concurrency Control) 모델
current 모드: 디스크에서 캐시로 적재된 원본 블록을 현재 상태 그대로 읽는 방식
consistent 모드: 쿼리가 시작된 이후에 다른 트랜잭션에 의해 변경된 블록을 만나면 원본 블록으로부터 복사본(CR Copy) 블록을 만들고 거기에 Undo 데이터를 적용함으로써 쿼리가 시작된 시점으로 되돌려 읽는 방식
SCN(System Commit Number) 마지막 커밋이 발생한 시점정보
블록 SCN: 각 블록이 마지막으로 변경된 시점을 관리하기 위해 블록 헤더에 기록됨
쿼리 SCN: 모든 쿼리는 Global 변수인 SCN 값을 먼저 확인하고 읽기 작업 시작, 이를 쿼리 SCN이라고 한다
데이더를 읽다가 블록 SCN이 쿼리 SCN보다 더 큰 블록을 만나면 복사본 블록을 만들고 Undo 데이터를 적용함으로써 쿼리가 시작된 시점으로 되돌려서 읽는다
Undo 데이터가 다른 트랜잭션에 의해 재사용됨으로써 쿼리 시작 시점으로 되돌리는 작업에 실패할 때 Snapshot too old 에러 발생
Lock과 DML 성능
Lock은 DML 성능에 매우 크고 직접적인 영향을 미친다. Lock을 필요 이상으로 자주, 길게 사용하거나 레벨이 높을수록 DML 성능은 느려진다
트랜잭션 격리 수준
Read Uncommited
Read Commited (기본)
Reapeatable Read
Serializable
트랜잭션의 저장 과정 (Write Ahead Logging)
DML 문을 실행하면 Redo 로그버퍼에 변경사항 기록
버퍼블록에서 데이터를 변경(추가,수정,삭제), 버퍼캐시에서 블록을 찾지 못하면 데이터파일에서 읽는 작업부터 수행
커밋
LGWR 프로세스가 Redo 로그버퍼 내용을 로그파일에 일괄 저장
DBWR 프로세스가 변경된 버퍼블록들을 데이터파일에 일괄 저장
Log Force at Commit : 서버 프로세스가 커밋을 발행했다고 신보를 보낼때 LGWR 프로세스가 로그파일에 저장하므로 적어도 커밋시점에는 Redo 로그버퍼 내용이 로그파일에 기록됨
데이터베이스 Call
Parse Call : SQL 파싱과 최적화를 수행하는 단계. SQL과 실행계획을 라이브러리 캐시에서 찾으면, 최적화 단계 생각 가능
Execute Call: SQL을 실행하는 단계. DML은 이 단계에서 모든 과정이 종료. SELET 문은 Fetch 단계를 거친다
Fetch Call: 데이터를 읽어서 사용자에게 결과집합을 전송하는 과정. SELECT문에서만 나타나며 전송할 데이터가 많으면 Fetch Call 여러번 발생
User Call, Recursive Call
Call 이 발생하는 위치에 따라 User Call과 Recursive Call로 나뉜다
User Call: 네트워크를 통해 DBMS 외부로부터 인입되는 Call 3-Tier 아키텍처에서 User Call은 WAS(또는 AP서버) 서버에서 발생하는 Call
Recursive Call: DBMS 내부에서 발생하는 Call
커밋과 성능
: 커밋을 자주하는 경우 트랜잭션 원자성에 문제가 생긴다
: 매우 오래 걸리는 트랜잭션을 한 번도 커미하지 않고 진행하면 Undo 공간 부족으로 인해 시스템에 부작용을 초래할 수 있다
: 적당한 주기로 커밋을 하는것이 좋다
커밋을 자주하면 수행 시간이 오래걸리게 되는데, 이는 User Call에서는 더 많은 시간이 걸리게 된다
One SQL
Insert Into Select
수정가능 조인 뷰
Merge 문
실무에서 One SQL로 구현이 쉽지 않다
Array Processing 기능을 활용하면 One SQL을 쓰지 않고도 Call 부하를 획기적으로 줄일 수 있다
PL/SQL
Java
인덱스 및 제약 해제를 통한 대량 DML 튜닝
동시 트랜잭션 없이 대량 데이터를 적재하는 배치 프로그램에서는 인덱스 및 제약 조건을 해제함으로써 큰 성능 개선 효과를 얻을 수 있다
PK 제약 및 인덱스 생성 (총 인덱스 2개)
PK제약과 인덱스 해제
PK제약에 Unique 인덱스를 사용한 경우
PK 제약 해제 및 인덱스 삭제
인덱스 비활성화
PK 제약 설정 및 인덱스 활성화
2.PK제약에 Non-Unique 인덱스 사용
PK제약을 Unusable한 상태에서는 데이터를 입력할 수 없다
PK 인덱스를 Drop 하지 않고 Unusable 상태에서 데이터를 입력하려면 PK 제약에 Non-Unique 인덱스를 사용하면 된다
Non-Unique 인덱스 생성 및 PK 제약으로 지정
PK 제약 해제 및 인덱스 비활성화
PK 제약 설정 및 인덱스 활성화
수정가능 조인 뷰
전통적인 UPDATE
전통적인 UPDATE문은 다른 테이블과 조인이 필요할 때 비율을 완전히 해소할 수 없다
수정가능 조인 뷰
수정가능 조인 뷰를 활용하면 참조 테이블과 두 번 조인하는 비효율을 없앨 수 있다
수정가능 조인 뷰에서는 1쪽 집합에 PK 제약을 설정하거나 Unique 인덱스를 생성해야 수정가능 조인뷰를 통한 입력/수정/삭제가 가능하다
키 보존 테이블
: 조인된 결과집합을 통해서도 중복 값 없이 Unique 하게 식별이 가능한 테이블 Unique한 1쪽 집합과 조인되는 테이블이어야 조인된 결과집합을 통한 식별이 가능
ORA-01779 오류 회피 : Group By 한 집합과 조인한 테이블은 키가 보존되므로 해당 문제를 해결할 수 있다
Merge 문
DW Merge 예시
Merge문은 Source 테이블 기준으로 Target 테이블과 Left Outer 방식으로 조인해서 조인 성공하면 Update, 실패하면 Insert 한다
Update 와 Insert를 선택적으로 처리 가능
where 절을 통한 추가 조건절 기술 가능
이미 저장된 데이터를 조건에 따라 지우는 기능 제공
Delete의 경우
Merge문을 수행한 결과가 Null 이면 삭제하지 않는다
조인에 성공한 데이터만 삭제할 수 있다
Direct Path I/O
DW, 배치 프로그램에서는 대량 데이터를 처리하기 때문에 버퍼캐시를 경유하는 I/O 메커니즘이 오히려 성능을 떨어뜨릴 수 있다
그래서 버퍼캐시를 경유하지 않고 곧바로 데이터 블록을 읽고 쓸 수 있는 Direct Path I/O 기능을 제공한다
동작하는 경우
병렬 쿼리로 Full Scan을 수행할 때
병렬 DML을 수행할 때
Direct Path Insert 를 수행할 때
Temp 세그먼트 블록들을 읽고 쓸 때
direct 옵션을 지정하고 export를 수행할 때
nocache 옵션을 지정한 LOB 컬럼을 읽을 때
Direct Path Insert
INSERT가 느린 이유
데이터를 입력할 수 있는 블록을 Freelist에서 찾는다
Freelist에서 할당 받은 블록을 버퍼캐시에서 찾는다
버퍼캐시에 없으면, 데이터 파일에서 읽어 버퍼캐시에 적재한다
INSERT 내용을 Undo 세그먼트에 기록한다
INSERT 내용을 Redo 로그에 기록한다
Direct Path Insert 적용 방법
INSERT … SELECT 문에 append 힌트 적용
parallel 힌트를 이용해 병렬 모드로 INSERT
direct 옵션을 지정하고 SQL Loader(sqlldr)로 데이터 적재
CTAS 문 수행
Direct Path Insert 방식이 빠른 이유
Freelist를 참조하지 않고 HWM(High-Water Mark) 바깥 영역에 데이터를 순차적으로 입력한다
블록을 버퍼캐시에서 탐색하지 않는다
버퍼캐시에 적재하지 않고, 데이터파일에 직접 기록한다
Undo 로깅을 하지 않는다
Redo 로깅을 안하게 할 수 있다(nologging 모드 상태일 때)
Direct Path Insert 주의점
exclusive 모드 TM Lock 발생
Freelist를 조회하지 않고 HWM 바깥 영역을 입력하므로 테이블에 여유공간이 있어도 재사용하지 않는다
병렬 DML
INSERT는 append 힌트로 Direct Path Write 방식을 유도할 수 있지만, UPDATE, DELETE 는 불가능하다
병렬 DML로 Direct Path Write 방식 사용가능
병렬 DML 활성화 방법
아래 힌트 적용 시 대상 레코드 찾는 작업(insert는 select 쿼리, update/delete 는 조건절 검색)과 데이터 추가/변경/삭제 병렬로 진행
병렬 DML을 활성화 하지 않고 수행하면 대상 레코드를 찾는 작업만 병렬로 진행되어 추가/변경/삭제 는 QC(Query Coordinator)가 혼자 담당하므로 병목 발생
병렬 INSERT는 append 힌트를 적용하지 않아도 Direct Path Insert 방식을 사용한다
하지만 병렬 DML이 작동하지 않을 경우를 대비해 apeend 힌트를 같이 사용하는 것이 좋다
병렬 DML이 작동하지 않더라도 QC 가 Direct Path Insert를 사용하면 어느정도 만족할 만한 성능을 낼 수 있기 때문
enable_parallel_dml 힌트를 통해 활성화 가능(12c이후)
* 병렬 DML도 Direct Path Write 방식을 사용하므로 데이터 입력/수정/삭제 때 Exclusive 모드 TM Lock 발생
병렬 DML 동작 확인 방법
UPDATE 가 PX COORDINATOR 아래 쪽애 나타나면 UPDATE를 각 병렬 프로세스가 처리
루트 블록 스캔 과정에서 특정 레코드를 찾았다면 그것이 가리키는 리프 블록으로 내려가면 안되고 그 이전 레코드가 가리키는 리프 블록 으로 내려가야 한다. 레코드의 값보다 크거나 같은 값들이 리프 블록에 존재하므로, 이전 레코드에 조건에 포함되는 레코드가 존재할 수 있기 때문이다
루트 블록에서 C1, C2가 B,3 이전의 레코드의 리프블록으로 내려가서 B, 3인 레코드를 찾고 B, 4인 레코드를 만나는 순간 스캔을 멈춘다
루트 블록에서 C1, C2가 B, 3 이전의 레코드의 리프블록으로 내려가 B, 3인 레코드를 찾고 C1이 C인 값을 만나면 스캔을 멈춘다. C2는 스캔을 멈추는데는 역할을 하지 못했지만 스캔의 시작점을 결정하는데는 역할을 했다
C2는 수직적 탐색(스캔 시작지점)을 찾는데는 역할을 하지 못했으나 스캔을 멈추는데는 역할을 했다
C2는 수직적 탐색(스캔의 시작지점)을 찾는데 역할을 했고, 스캔을 멈추는데도 역할을 했다. 즉, 스캔량을 줄이는데 역할을 했다
C1 조건은 스캔의 시작과 끝을 결정하는데 역할을 했으나, C2는 그렇지 못했다
C2는 스캔량을 줄이는데 거의 역할을 못했다
좌측은 성능검으로 시작하는 용어를 스캔할 때,
우측은 성능으로 시작하고 4번째 값이 선 인 단어를 스캔할 때의 경우이다
인덱스 선행 컬럼이 조건절에 없거나 = 조건이 아니면 인덱스 스캔 과정에서 비효율이 발생한다
인덱스를 7463개의 블록을 읽으며서 10개의 블록을 찾았고, 이 블록을 가지고 10개의 블록에 테이블 엑세스를 했다
단, 버퍼 피닝에 의해 같은 블록에 있는 데이터는 추가로 블록에 엑세스 하지 않아 7463블록에 8블록을 추가로 읽어 총 7471개의 블록을 읽었다
선두 컬럼 : 인덱스 구성상 ‘맨 앞쪽’에 있는 컬럼을 지칭할 때 사용
선행 컬럼 : 어떤 컬럼보다 ‘상대적으로 앞쪽’에 놓인 컬럼을 지칭할 때 사용
비용 = 인덱스 수직적 탐색 비용 + 인덱스 수평적 탐색 비용 + 테이블 랜덤 액세스 비용 = 인덱스 루트와 브랜치 레벨에서 읽은 블록 수 + 인덱스 리프 블록을 스캔하는 과정에 읽는 블록 수 + 테이블 액세스 과정에 읽는 블록 수
테이블과 달리 인덱스에는 같은 값을 갖는 레코드들이 서로 군집해 있다
첫번째 나타나는 범위 검색 조건까지만 만족하는 인덱스 레코드는 모두 연속해서 모여 있지만, 그 이하 조건까지 만족하는 레코드는 비교 연산자 종류에 상관없이 흩어진다
인덱스 선행 컬럼이 모두 = 조건일 때 필요한 범위만 스캔하고 멈출 수 있는 것은, 조건을 만족하는 레코드가 모두 한데 모여 있기 때문!
Between 을 IN-List로 전환
IN-Llist를 사용하는 경우 IN-List 내 존재하는 개수 만큼 수직적 탐색이 발생한다
IN-List 항목 개수가 늘어날 수 있다면 BETWEEN을 IN-List로 전환하는 방식은 사용하기 곤란하다
이런 경우는 NL조인이나 서브쿼리로 구현하면 된다
BETWEEN 조건을 IN-List 조건으로 전환할 때 IN-List 개수가 많으면 수직적 탐색이 많이 발생한다
이는 BETWEEN 조건으로 리프 블록을 많이 스캔하는 비효율보다 IN-List 개수만큼 브랜치 블록을 반복 탐색하는 비효율이 더 클 수 있다
NUM_INDEX_KEYS 힌트 : 인덱스의 지정한 번호의 컬럼까지만 액세스 조건으로 사용하라는 의미
해당 힌트를 쓰지 않고 컬럼을 가공하여 같은 효과를 낼 수 있다
BETWEEN과 LIKE 비교
BETWEEN의 경우 판매월이 201912이고 판매구분이 B인 첫번째 레코드를 만나면 스캔을 멈춘다
LIKE는 판매월이 201912인 레코드를 모두 스캔하고나서야 멈춘다
OR 조건 활용한 옵션 처리
인덱스 선두 컬럼에 대해 옵션 조건에 OR 조건을 사용하면 인덱스를 사용할 수 없다
인덱스에 포함되지 않는 컬럼의 경우는 OR 조건을 사용해도 괜찮다
OR 조건을 활용한 옵션 조건 처리
인덱스 액세스 조건으로 사용 불가
인덱스 필터 조건으로도 사용 불가
테이블 필터 조건으로만 사용 가능
OR 조건 사용의 장점은 NULL 허용 컬럼이어도 사용할 수 있다는 것
LIKE/BETWEEN 패턴을 사용하고자 할 때 점검해야할 것
인덱스 선두 컬럼
NULL 허용 컬럼 이때 고객ID가 NULL 허용 컬럼이면 결과집합에 오류가 생긴다
숫자형 컬럼
가변 길이 컬럼
UNION ALL 을 사용하면 코딩량은 늘지만 고객ID를 입력할 때와 그러지 않을 때에 대해 인덱스를 사용할 수 있다
PL/SQL 사용자 정의 함수는 생각보다 느리다
그 이유는
가상머신 상에서 실행되는 인터프리터 언어
호출 시마다 컨텍스트 스위칭 발생
내장 SQL에 대한 Recursive Call 발생 *
효율적인 인덱스 구성을 통한 함수 호출 최소화
조건절에 PL/SQL 함수를 사용했을 때 Full Scan으로 읽으면 해당 함수가 테이블 건수만큼 수행된다
하지만 다른 조건절과 같이 있으면 함수는 조건절을 만족하는 건 수 만큼만 실행되게 되므로 좀 더 효율적이게 된다
마지막으로 함수가 = 로 동등 비교하게 되는 컬럼이 포함된 인덱스를 사용하여 해당 컬럼이 인덱스 액세스 조건으로 사용되면 함수는 한번만 수행된다
인덱스 구성 시 중요한 두가지 선택 기준
조건절에 항상 사용하거나, 자주 사용하는 컬럼을 선정한다
= 조건으로 자주 조회하는 컬럼을 앞쪽에 둔다
스캔 효율성 이외의 판단 기준
수행 빈도
업무상 중요도
클러스터링 팩터
데이터량
DML 부하
저장 공간
인덱스 관리 비용 등
I/O를 최소화 하면서 소트 연산을 생략하기 위한 인덱스 구성 공식
= 연산자로 사용한 조건절 컬럼 선정
ORDER BY 절에 기술한 컬럼 추가
= 연산자가 아닌 조건절 컬럼은 데이터 붆포를 고려해 추가 여부 결정
IN-LIst 는 = 이 아니다
IN-List 가 = 이 되기 위해 IN-List Iterator 로 풀리게 되면 UNION ALL로 결과 집합을 묶기 때문에 Sort 연산을 생략 할 수 없다
인덱스를 통해 찾은 ROWID를 통해 데이터 가져올 때 버퍼캐시에서 데이터를 찾기 위해 DBA(데이터파일번호 + 블록번호)를 해시 함수에 입력하여 해시 체인을 찾은 후 해시 체인에서 버퍼 헤더를 찾는다
버퍼 헤더는 메모리의 주소값을 가지고 있기 때문에 그 포인터를 이용해 버퍼 블록을 찾는다
인덱스로 테이블 블록을 엑세스할 때는 리프 블록에서 읽은 ROWID를 분해하여 DBA 정보를 얻는다
테이블 Full Scan을 할 때는 익스텐트 맵을 통해 읽을 블록들의 DBA 정보를 얻는다
ROWID가 가리키는 테이블 블록을 버퍼캐시에서 먼저 찾고, 못 찾으면 디스크에서 블록을 읽어 버퍼캐시에 적재한 후 읽는다
인덱스 클러스터링 팩터(CF)
특정 컬럼을 기준으로 같은 값을 갖는 데이터가 서로 모여있는 정보
CF가 좋은 컬럼에 생성한 인덱스는 검색 효율이 매우 좋다
인덱스 클러스터링 팩터가 가장 좋은 상태
인덱스 클러스터링 팩터가 가장 안 좋은 상태
CF가 좋은 컬럼에 생성한 인덱스가 검색 효율이 좋은 이유
인덱스 ROWID로 테이블을 액세스할 때, 래치 획득과 해시 체인 스캔 과정을 거쳐 어렵게 찾아간 테이블 블록에 대한 포인터를 바로 해제하지 않고 일단 유지하는데, 이를 버퍼 Pinning이라고 한다
이를 통해 다음 인덱스 레코드를 읽을 때 같은 테이블 블록인 경우 래치 획득과 해시 체인 스캔 과정을 생략하고 바로 테이블 블록을 읽을 수 있어 논리적인 블록 I/O 과정을 생략할 수 있기 땜누에 블록 I/O가 적게 발생한다
인덱스 손익 분기점
Index Range Scan에 의한 테이블 액세스가 Table Full Scan보다 느려지는 지점
인덱스를 이용한 테이블 액세스가 Table Full Scan 보다 더 느려지게 만드는 가장 핵심적인 두 가지 요인
Table Full Scan은 시퀀셜 액세스이나 인덱스 ROWID를 이용한 테이블 액세스는 랜덤 액세스 방식
Table Full Scan은 Multiblock I/O인 반면 인덱스 ROWID를 이용한 테이블 액세스는 SingleBlock I/O 방식
인덱스 손익 분기점은 보통 5 ~ 20%의 낮은 수준에서 결정된다(CF에 따라 크게 달라진다)
온라인 프로그램 : 소량 데이터를 읽고 갱신, 인덱스를 효과적으로 활용하는 것이 중요
배치 프로그램 튜닝 : 항상 전체범위 처리 기준으로 튜닝 필요, 처리대상 집합 중 일부를 빠르게 처리하는 것이 아닌 전체를 빠르게 처리하는 것을 목표로 한다
Covered 쿼리 : 인덱스만 읽어서 처리하는 쿼리
이때 사용하는 인덱스를 Covered 인덱스라고 부른다
Include 인덱스
include로 포함된 컬럼은 리프 블록에만 저장한다
따라서 수직탐색에는 사용할 수 없다
다만, 수평탐색에서는 필터조건으로 사용할 수 있다(테이블 랜덤 액세스 횟수 줄이는데 사용 가능)
위에 두 인덱스가 있을 때 인덱스 스캔량은 emp_x02가 더 적다, sal 도 액세스 조건으로 사용하기 때문
emp_x02는 소트 연산 생략이 가능하지만 emo_x01은 불가능하다
include 인덱스는 랜덤 액세스를 줄이는 용도로 개발되었다
인덱스 구조 테이블(오라클에서는 IOT(Index-Organized Table), MS 는 클러스터형 인덱스)
테이블을 인덱스 구조로 만드는 구문
일반 테이블은 heap 구조
힙구조는 데이터를 입력할 때 랜덤 방식을 사용(순서 없음), IOT 구조는 인덱스 구조 테이블이므로 정렬 상태를 유지(인위적으로 클러스터 팩터를 좋게 만드는 방법)
클러스터 테이블
인덱스 클러스터 테이블
클러스터 키 값이 같은 레코드를 한 블록에 모아서 저장하는 구조, 한 블록에 다 담을 수 없을 때는 새로운 블록을 할당해서 클러스터 체인으로 연결
여러 테이블 레코드를 같은 블록에 저장할 수 있는데, 이런 경우는 “다중 테이블 클러스터” 라고 부른다
일반 테이블은 하나의 데이터 블록을 여러 테이블이 공유할 수 없음
인덱스 클러스터 테이블 구성
1. 클러스터 생성
2. 인덱스 생성
3. 클러스터 테이블 생성
클러스터 인덱스도 B*Tree 인덱스 구조이지만 테이블 레코드를 일일이 가리키지 않고 해당 키 값을 저장하는 첫 번째 데이터 블록을 가리킨다
일반 테이블 인덱스 레코드는 테이블 레코드와 1:1 이지만 클러스터 인덱스는 테이블 레코드와 1:M 관계를 갖는다
클러스터 인덱스의 키 값은 항상 Unique 하다
해시 클러스터 테이블
인덱스를 사용하지 않고 해시 알고리즘을 사용해 클러스터를 찾아간다
클러스터 생성 시 아래와 같이 hashkeys ‘숫자’ 를 입력하여 생성한다
부분 범위 처리
전체 쿼리 결과집합을 쉼 없이 연속적으로 전송하지 않고 사용자로부터 Fetch Call이 있을 때마다 일정량씩 나누어 전송
정렬 조건이 있을 때 부분 범위 처리
정렬이 있는 경우 모든 데이터를 읽어 정렬을 마치고 나서야 전송을 시작할 수 있기 때문에 전체 범위 처리가 된다
단, 정렬절에 있는 컬럼이 선두인 인덱스가 존재한다면 부분 범위 처리가 가능하다.(인덱스가 정렬되어있기 때문에 정렬을 하지 않아도 되므로 가능)
모든 DBMS는 데이터를 조금씩 나누어 전송한다. 다만 부분 범위 처리를 프로그램에 적용하고 안하고는 개발자의 목이다
부분 범위 처리가 의미가 있기 위해서는 멈출 수 있어야 한다
DB에 직접 접속하는 2-Tier 환경에서는 구현이 가능하지만, 클라이언트와 DB 사이에 WAS, AP 서버 등 이 존재하는 n-Tier 환경에서는 클라이언트가 특정 DB 커넥션을 독점할 수 없기 때문에 SQL 조회 결과를 클라이언트에게 모두 전송하고 커서를 닫아야 한다
배치 I/O
읽는 블록마다 건건이 I/O Call을 발생시키는 것이 아닌 테이블 블록에 대한 디스크 I/O Call을 미루었다가 읽을 블록이 일정량 쌓이면 한꺼번에 처리하는 방식
OLTP(OnLine Transaction Processing)에서는 디스크 I/O 중에서도 랜덤 I/O가 특히 중요
인덱스는 정렬되어있기 때문에 범위 스캔(Range Scan) 가능 하다
DBMS 의 인덱스는 일반적으로 B*Tree(Balanced Tree) 인덱스를 사용한다
인덱스 내에는 가장 왼쪽 첫 번째 레코드를 가리키는 LMC(LeftMost Child) 레코드가 존재한다
LMC가 가리키는 주소의 블록에는 키 값을 가진 첫 번째 레코드 보다 작거나 같은 레코드가 저장되어있다
리프 블록에 저장된 각 레코드는 키 값 순으로 정렬돼 있을 뿐 아니라 테이블 레코드를 가리키는 주소값(ROWID)을 갖는다
ROWID = 데이터 블록 주소 + 로우 번호
ROWID를 알면 테이블 레코드를 찾아갈 수 있다
ROWID = 데이터 블록 주소 + 로우 번호
데이터 블록 주소 = 데이터 파일 번호 + 블록 번호
블록 번호 = 데이터파일 내에서 부여한 상대적 순번
로우 번호 = 블록 내 순번
인덱스 탐색 : 인덱스 탐색은 두 가지로 나뉜다
수직적 탐색 - 인덱스 스캔 시작지점을 찾는 과정 - 수직적 탐색 과정에서 찾고자 하는 값보다 크거나 같은 값을 만나면, 바로 직전 레코드가 가리키는 하위 블록으로 이동 한다
수평적 탐색 - 데이터를 찾는 과정 - 스캔 시작점으로 부터 찾고자 하는 데이터가 더 안 나타날 때까지 인텍스 리프 블록을 수평적으로 스캔
결합 인덱스 : 두 개 이상의 컬럼을 결합해서 만든 인덱스
인덱스는 B*Tree(Balanced Tree, 루트에서 리프 블록까지 읽는 블록 수 가 같음) 이므로 컬럼 순서에 따라 일량의 차이가 나지는 않는다
인덱스의 기본 사용 방법은 인덱스를 Range Scan 하는 방법을 의미
인덱스 컬럼을 가공하지 않아야 인덱스를 정상적으로 사용(Range Scan)할 수 있다
Like를 이용한 중간 값 검색을 할 때도 Range Scan할 수 없다
OR 으로 검색하는 경우 Range Scan 불가능 → Or Expansion을 사용하면 Range Scan 가능(use_concat 힌트) or 를 union all sql로 변환 하여 실행하기 때문
* 이유: 인덱스 스캔 시작점을 찾을 수 없기 때문!
IN 조건절의 경우 OR 조건을 표현하는 다른 방식으로 SQL 옵티마이저가 IN 개수 만큼 Index Range Scan을 반복한다.(IN-List Iterator)
인덱스를 정상적으로 사용한다 = 리프 블록에서 스캔 시작점을 찾아서 스캔하다가 중간에 멈추는 것을 의미
인덱스 Range Scan을 위한 첫 번째 조건 : 선두 컬럼이 가공되지 않은 상태로 조건절에 있어야 한다
인덱스는 데이터가 정렬되어있기 때문에 인덱스를 이용하여 소팅 연산을 생략하는 효과를 볼 수 있다
ORDER BY 의 정렬 요소를 가공하면 정렬 연산을 생략할 수 없다
ORDER BY 에 인덱스 요소를 가공하여 사용하였기 때문에 별도 정렬 연산이 발생한다
ORDER BY 에 인덱스인 주문번호가 들어간 것 같지만 실제로는 TO_CHAR로 변환된 주문번호가 들어가기 때문에 소팅 연산이 발생 한다 이 경우 테이블 alias 인 A를 ORDER BY 주문번호에 붙여주면 정렬 연산을 생략할 수 있다
인덱스는 정렬되어 있기 때문에 인덱스 구성요소에 대한 MIN ,MAX를 구하는 경우 인덱스 리프블록의 왼쪽(MIN), 오른쪽(MAX)에서 레코드 하나(FIRST ROW)만 읽고 멈춘다
가공 후 MIN, MAX를 사용하는 경우에 대해서는 정상적으로 동작하지 못한다
스칼라 서브쿼리의 수 만큼 테이블을 여러번 읽어야하기 때문에 비효율적이고 SQL도 복잡해진다
인덱스를 사용하기 위해 조건절을 지정하는 경우 자동 형변환을 조심해야한다
이 경우 인덱스를 정상적으로 사용하지 못하는데, 그 이유는 생년월일 데이터가 TO_NUMBER 로 자동 형변환이 일어났고 이에 따라서 인덱스 컬럼이 가공되어 인덱스를 정상적으로 사용하지 못한 것이다
자동 형변환
문자 < 숫자
문자 < 날짜
LIKE를 사용하는 경에는 문자형 기준으로 숫자형 컬럼이 변환
자동 형변화 시 주의사항
숫자형 컬럼과 문자형 컬럼을 비교하면 문자형 컬럼이 숫자로 변환된다
이때 문자형이 숫자로 변환이 불가능하는 경우에는 에러가 발생한다
Index Range Scan
B*Tree 인덱스의 가장 일반적이고 정상적인 형태의 액세스 방식
인덱스 루트에서 리프 블록까지 수직적 탐색 후 필요한 범위만 스캔
* 인덱스의 선두 컬럼을 가공하지 않은 상태로 조건절에 사용!
Index Full Scan
수직적 탐색 인덱스 리프 블록을 처음부터 끝까지 수평적으로 탐색하는 방식
데이터 검색을 위한 최적의 인덱스가 없을 때 차선으로 선택된다
인덱스 선두 컬럼이 조건절에 없는 상황에서 조회 대상 테이블이 대용량 테이블인 경우 Full Scan에 따른 부담이 크다면 옵티마이저가 Index Full Scan을 고려한다
Index Full Scan을 하면 Range Scan과 마친가지고 결과 집합이 인덱스 컬럼 순으로 정렬되어있다. 따라서 Sort 연산을 생략할 목적으로 사용할 수 있다
Index Full Scan을 사용할 때 아래와 같이 대부분의 레코드에 대해 테이블 엑세스가 발생하는 경우 Full Scan 보다 오히려 불리할 수 있다 다만 아래의 경우는 결과 집합의 일부만 빠르게 출력할 목적으로 first_rows 힌트를 사용했기 때문에 Index Full Scan을 사용했다
* 부분범위 처리가 가능한 상황에서는 극적인 성능 개선 효과를 가져다 줄 수 있다
Index Unique Scan
수직적 탐색만으로 데이터를 찾는 스캔 방식으로 Unique 인덱스를 = 조건으로 탐색하는 경우 동작(인덱스에 포함된 모든 컬럼에 대해 조건절에 = 조건으로 검색할 때)
Index Skip Scan
인덱스 선두 컬럼이 조건절에 없어도 사용할 수 있는 방법(9i에 등장) 조건절에 빠진 인덱스 선두 컬럼의 Distinct Value 개수가 적고 후행 컬럼의 Distinct Value 개수가 많은 때 유용 Distinct Value가 적은 두 개의 선두컬럼이 모두 조건절에 없는 경우, 중간 컬럼이 없는 경우도 사용 가능
Index Fast Full Scan
논리적인 인덱스 트리 구조를 무시하고 인덱스 세그먼트 전체를 Multiblock I/O 방식으로 스캔하는 방법(물리적으로 디스크에 저장된 순서대로 인덱스 리프 블록들을 읽는다.)
- 결과 집합이 정렬되지 않음.
- 쿼리에 사용한 컬럼이 모두 인덱스에 포함되어있을 때만 사용 가능
- 인덱스가 파티션 돼 있지 않아도 병렬 쿼리가 가능
병렬 쿼리 시 에는 Direct Path I/O 방식 사용으로 I/O 속도가 더 빨라진다
Index Range Scan Descending
Index Range Scan과 동일 방식으로 스캔하나 인덱스 뒤에서 부터 앞쪽으로 스캔하기 때문에 결과 집합이 내림차 순으로 된 결과를 얻을 수 있다.(index_desc 힌트로 유도 가능)