데이터베이스 아키텍처, SQL 처리 과정, 데이터베이스 I/O 메커니즘을 알아보자
데이터베이스(Database)
: 디스크
에 저장된 데이터 집합(Datafile, Redo Log File, Control File 등)
인스턴스(Instance)
: SGA 공유 메모리 영역
과 이를 액세스 하는 프로세스 집합
을 합쳐서 부름
하나의 인스턴스
가 하나의 데이터베이스만
액세스하지만, RAC(Real Application Cluster)
환경에선 여러 인스턴스
가 하나의 데이터베이스
를 액세스 가능
=> 하나의 인스턴스
가 여러 데이터베이스
를 액세스X
하나의 인스턴스
당 최고 3만 2767개의 데이터베이스 정의
해 사용 가능
기본적으로 master, model, msdb, tempdb
등의 시스템 데이터베이스
가 만들어지며, 여기에 사용자 데이터베이스
를 추가로 생성
하는 구조
데이터베이스
하나
를 만들 때마다 주 데이터 파일
(main)과 트랜잭션 로그 파일
이 하나씩 생김
=> 확장자는 각각 mdf
, ldf
, 보조 데이터 파일
은 데이터가 많을 경우 추가 가능하며 확장자는 ndf
SQL Server
는 쓰레드(Thread)
기반 아키텍처이므로 프로세스
대신 쓰레드
라는 표현 사용
프로세스
는 서버 프로세스
와 백그라운드 프로세스 집합
으로 나뉨
=> 서버 프로세스
: 전면
에서 사용자
로부터 전달받은 각종 명령 처리
=> 백그라운드 프로세스
: 뒤
에서 묵묵히 할당받은 역할 수행
서버 프로세스
: 사용자 프로세스
와 통신하면서 사용자의 각종 명령
을 처리
=> SQL Server
에선 Worker 쓰레드
가 같은 역할 담당
=> SQL을 파싱
하고 필요하면 최적화
수행
=> 커서를 열
어 SQL을 실행
하면서 블록
을 읽어 이 데이터를 정렬
해 클라이언트가 요청한 결과 집합
을 만들어 네트워크
를 통해 전송
하는 작업 처리
=> 스스로 처리하도록 구현되지 않은 기능
(ex. 데이터 파일
로부터 DB 버퍼 캐시
로 블록 적재
, Dirty블록
을 캐시에서 밀어냄으로써 Free블록
확보, Redo 로그 버퍼
를 비우는 일)은 OS
와 I/O 서브시스템
, 백그라운드 프로세스
가 대신 처리하도록 시스템 Call
을 통해 요청
클라이언트
가 서버 프로세스
와 연결하는 방식
전용 서버 방식
리스너
가 서버 프로세스(쓰레드)
를 생성
해주고, 이 서버 프로세스
가 단 하나
의 사용자 프로세스
를 위해 전용 서비스
를 제공SQL을 수행
할 때마다 연결 요청
시 서버 프로세스
의 생성과 해제
도 반복하므로 DBMS에 큰 부담과 성능 저하OLTP성 애플리케이션
에선 Connection Pooling 기법
을 필수적으로 사용해야 함 (ex. 50개 서버 프로세스
와 연결된 50개 사용자 프로세스를 공유해 반복 재사용
)공유 서버 방식
하나의 서버 프로세스
를 여러 사용자 세션
이 공유
Connection Pooling 기법
을 DBMS 내부에 구현해 놓은 것으로 생각하면 쉬움여러 개
의 서버 프로세스
를 띄어 놓고 이를 공유해 반복 재사용
사용자 프로세스
는 서버 프로세스
와 직접 통신X
, Dispatcher 프로세스
를 거침사용자 명령
이 Dispatcher
에게 전달되면, Dispatcher
는 이를 SGA
에 있는 요청 큐(Request Queue)
에 등록서버 프로세스
가 요청 큐
에 있는 사용자 명령
을 꺼내서 처리하고, 그 결과를 응답 큐(Response Queue)
에 등록응답 큐
를 모니터링
하던 Dispatcher
가 응답 결과를 발견하면 사용자 프로세스
에게 전송블록(=페이지)
: DBMS에서 I/O 단위
=> 데이터를 읽고 쓸
때의 논리적인 단위
=> Oracle
은 2KB, 4KB, 8KB, 16KB, 32KB 등 다양한 블록 크기 사용 가능
=> SQL Server
는 8KB
사용
=> 블록 단위 I/O
는 하나의 레코드에서 한 칼럼만 읽을 때도 레코드가 속한 블록 전체
를 읽게 됨을 뜻함
=> 가장 중요한 성능지표는 액세스하는 블록 개수
이며, 옵티마이저의 판단
에 가장 큰 영향을 미치는 것도 액세스해야 할 블록 개수
익스텐트
: 테이블스페이스
로부터 공간을 할당하는 단위
=> 테이블
이나 인덱스
에 데이터를 입력하다가 공간이 부족
해지면 해당 오브젝트
가 속한 테이블스페이스(물리적으론 데이터파일)
로부터 추가적인 공간 할당
=> 정해진 익스텐트 크기
의 연속된 블록
할당 (ex. 블록 크기가 8KB
인 상태에서 64KB 단위로 익스텐트
할당하도록 정의시, 공간 부족할 때마다 테이블스페이스
로부터 8개의 연속된 블록
을 찾아(찾지 못하면 새로운 익스텐트 추가) 세그먼트에 할당
=> 익스텐트 내 블록
은 논리적으로 인접
, 익스텐트끼리
는 서로 인접X
=> SQL Server
의 익스텐트는 항상 64KB
, 2개 이상의 오브젝트
가 나누어 사용 가능
=> Oracle
은 한 익스텐트
에 속한 모든 블록을 단일 오브젝트
가 사용
=> SQL Server
의 2가지 타입 익스텐트
균일(Uniform) 익스텐트
: 64KB 이상
의 공간을 필요로 하는 테이블이나 인덱스를 위해 사용, 8개의 페이지 단위로 할당된 익스텐트를 단일 오브젝트
가 모두 사용혼합(Mixed) 익스텐트
: 한 익스텐트에 할당된 8개의 페이지를 여러 오브젝트
가 나누어 사용, 모든 테이블이 처음에는 혼합 익스텐트로 시작
하지만 64KB를 넘으면서 2번째부터 균일 익스텐트
를 사용하게 됨세그먼트
: 테이블
, 인덱스
, Undo
처럼 저장공간
을 필요
로 하는 데이터베이스 오브젝트
=> SQL Server
의 힙 구조/인덱스 구조
오브젝트가 여기 속함
=> 저장공간
을 필요
로 한다는 것은 한 개 이상
의 익스텐트를 사용
함을 뜻함
=> 테이블 생성
시 내부적으로는 테이블 세그먼트
가 생성
=> 인덱스 생성
시 내부적으로는 인덱스 세그먼트
가 생성
=> 다른 오브젝트는 세그먼트
와 1:1 관계
, 파티션
은 1:M 관계
(파티션 테이블(또는 인덱스)
생성시, 내부적으로 여러 개의 세그먼트
가 만들어짐)
=> 한 세그먼트
는 자신이 속한 테이블스페이스
내 여러 데이터 파일
에 걸쳐 저장 가능, 즉 세그먼트에 할당
된 익스텐트
가 여러 데이터 파일에 흩어져
저장되며 그래야 디스크 경합
을 줄이고 I/O 분산 효과
를 얻음
테이블스페이스(=파일 그룹)
: 세그먼트
를 담는
컨테이너
=> 여러 데이터 파일
로 구성
=> 데이터
는 물리적
으로 데이터 파일
에 저장 (사용자가 직접 데이터 파일 선택X)
=> 사용자는 세그먼트
를 위한 테이블스페이스
를 지정
할 뿐, 실제 값을 저장할 데이터파일을 선택
하고 익스텐트를 할당
하는 것은 DBMS
몫
=> 각 세그먼트
는 정확히 한 테이블스페이스
에만 속함, 한 테이블스페이스
에는 여러 세그먼트
가 존재
=> 특정 세그먼트
에 할당된 모든 익스텐트
는 해당 세그먼트
와 관련된 테이블스페이스 내
에서만 찾아짐 ( 한 세그먼트
가 여러 테이블스페이스
에 걸쳐 저장X
)
=> 한 세그먼트
가 여러 데이터 파일
에 걸쳐 저장O
(한 테이블스페이스
가 여러 데이터 파일
로 구성
되기 때문)
SQL Server
는 한 익스텐트
에 속한 모든 페이지를 2개 이상 오브젝트가 나누어 사용 가능
임시(Temporary) 데이터 파일
: 대량의 정렬
이나 해시
작업을 수행하다가 메모리 공간 부족
으로 인해 중간 결과 집합
을 저장
하는 용도
=> 임시
로 저장했다가 자동
으로 삭제됨
=> Redo 정보 생성X
로 나중에 파일에 문제 발생 시 복구X
, 백업 필요X
=> Oracle
은 임시 테이블스페이스
를 여러 개 생성해두고, 사용자마다 별도로 지정 가능
=> SQL Server
는 단 하나
의 tempdb 데이터베이스
사용, tempdb
는 전역 리소스
로서 시스템에 연결된 모든 사용자
의 임시 데이터
저장
Redo로그/트랜잭션 로그
: DB 버퍼 캐시
에 가해지는 모든 변경사항
을 기록
하는 파일
=> 변경된 메모리 버퍼 블록
을 디스크
상의 데이터 블록에 기록하는 작업은 Random I/O
방식이므로 느림
=> 로그 기록
은 Append 방식
으로 이뤄지므로 빠름
=> 버퍼 블록에 대한 변경사항을 데이터 파일에 건건이 기록하기보다, 로그 파일
에 Append방식
으로 기록하는 방식 사용, 버퍼 블록
과 데이터 파일
간 동기화
는 적절한 수단(DBWR, Checkpoint
)을 이용해 나중
에 배치(Batch)
방식으로 일괄 처리
=> Fast Commit
: 사용자의 갱신 내용이 메모리상의 버퍼 블록
에만 기록된 채 아직 디스크에 기록되지 않았더
라도 Redo 로그
를 믿고 빠르게 커밋을 완료
=> 인스턴스 장애
가 발생하더라도, 로그 파일
을 이용해 언제든 복구 가능
하므로 안심
하고 커밋 완료
Online Redo 로그
: 마지막 체크포인트
이후부터 사고 발생 직전
까지 수행됐던 트랜잭션들을 Redo 로그
를 이용해 재현
(캐시 복구
)
=> 캐시에 저장
된 변경사항이 아직 데이터 파일에 기록되지 않은 상태
에서 정전 등 인스턴스가 비정상 종료
되면, 작업내용을 모두 잃게 되는데, 유실에 대비하기 위해 Online Redo 로그
사용
=> 최소 두 개 이상
의 파일로 구성
=> 현재 사용중인 파일이 꽉
차면 다음 파일
로 로그 스위칭(log switching)
발생
=> 계속 로그를 쓰다가 모든 파일
이 꽉 차면 다시 첫 번째 파일부터 재사용
하는 라운드 로빈(round-robin) 방식
사용
트랜잭션 로그
: Oracle의 Online Redo 로그
와 대응
=> 주 데이터 파일
마다(데이터베이스
마다) 트랜잭션 로그 파일
이 하나
씩 생김
=> 확장자는 ldf
=> 내부적으로 가상 로그 파일
이라는 더 작은 단위의 세그먼트
로 나뉘며, 가상 로그 파일의 개수
가 너무 많아지지 않도록
(즉 조각화가 발생하지 않도록
) 옵션 지정 (ex. 로그 파일을 애초에 넉넉한 크기로 만들어 자동 증가가 발생하지 않도록 하거나, 증가하는 단위를 크게 지정)
Archived(=Offline) Redo 로그
: Online Redo 로그
가 재사용되기 전
에 다른 위치
로 백업
해 둔 파일
=> 디스크
가 깨지는 등 물리적인 저장 매체
에 문제가 생긴 경우, 데이터베이스(또는 미디어) 복구
를 위해 사용
메모리 구조
는 시스템 공유 메모리 영역
과 프로세스 전용 메모리 영역
으로 구분
시스템 공유 메모리 영역
: 여러 프로세스(쓰레드)
가 동시
에 액세스
할 수 있는 메모리 영역
=> Oracle
- System Global Area(SGA)
, SQL Server
- Memory Pool
=> 공유 메모리
를 구성하는 캐시 영역
에는 DB 버퍼 캐시
, 공유 풀
, 로그 버퍼
가 있음
=> 그 외에 Large 풀
, 자바 풀
등 포함, 시스템 구조
와 제어 구조
를 캐싱
하는 영역
포함
=> 여러 프로세스
와 공유되므로 내부적으로 래치(Latch)
, 버퍼 Lock
, 라이브러리 캐시 Lock/Pin
같은 액세스 직렬화 메커니즘
사용
프로세스 전용 메모리 영역
: 서버 프로세스
가 자신만의 전용 메모리 영역
을 가짐
=> Process Global Area(PGA)
=> 데이터
를 정렬
하고, 세션
과 커서
에 대한 상태 정보 저장
용도
=> SQL Server
는 쓰레드
기반이라 프로세스 전용 메모리 영역 갖지X (쓰레드
는 전용 메모리 영역
을 가질 수 없고
, 부모 프로세스
의 메모리 영역
을 사용
하기 때문)
DB 버퍼 캐시(DB Buffer Cache)
: 데이터 파일
로부터 읽어 들인 데이터 블록
을 담는 캐시 영역
=> 인스턴스에 접속한 모든 사용자 프로세스는 서버 프로세스
를 통해 DB 버퍼 캐시
의 버퍼 블록
을 동시에
액세스 가능 (버퍼 Lock을 통해 직렬화
)
=> Direct Path Read 메커니즘
이 작동하는 경우 제외, 모든 블록 읽기
는 버퍼 캐시
를 통함
=> 읽고자 하는 블록을 먼저 버퍼 캐시
에서 찾아보고 없을 때 디스크
에서 읽음
=> 디스크
에서 읽을 때도 먼저 버퍼 캐시에 적재
한 후에 읽음
=> 데이터 변경
도 버퍼 캐시
에 적재된 블록
을 통해 이뤄지며, 변경된 블록(Dirty 버퍼 블록)
을 주기적으로 데이터 파일
에 기록하는 것은 DBWR 프로세스
의 몫
=> 디스크 I/O
: 물리적
으로 액세스 암(Arm)
이 움직이면서 헤드
를 통해 이뤄짐
=> 메모리 I/O
: 전기적 신호
, 디스크 I/O
에 비교할 수 없을 정도로 빠름
버퍼 블록의 상태
-Free 버퍼
: 인스턴스 기동 후 아직 데이터가 읽히지 않아 비어 있는 상태(Unused 버퍼)
이거나, 데이터가 담겼지만 데이터파일
과 동기화
돼 있는 상태여서 언제든지 덮어 써도 무방한 버퍼(Clean 버퍼)
블록. 데이터 파일
로부터 새로운 데이터 블록
을 로딩하려면 먼저 Free 버퍼
를 확보해야 함. Free 상태
인 버퍼에 변경
이 발생하면 그 순간 Dirty 버퍼
로 상태 변경
-Dirty 버퍼
: 버퍼에 캐시된 이후 변경
이 발생했으나, 아직 디스크
에 기록되지 않아 데이터 파일 블록과 동기화
가 필요한 버퍼 블록. 이 블록이 다른 데이터 블록을 위해 재사용
되려면 디스크
에 먼저 기록돼야 하며, 디스크에 기록
하는 순간 Free 버퍼
로 상태 변경
-Pinned 버퍼
: 읽기
또는 쓰기
작업이 현재 진행 중
인 버퍼 블록
LRU 알고리즘
: 버퍼 캐시는 유한한 자원으로, 모든 데이터를 캐싱할 수 없음. 사용 빈도가 높은 데이터 블록 위주
로 버퍼 캐시
가 구성되도록 LRU(Least Recently Used)
알고리즘 사용. 모든 버퍼 블록 헤더
를 LRU 체인
에 연결해서 사용빈도 순으로 위치를 옮겨가다가, Free 버퍼
필요시 액세스 빈도가 낮은 쪽
부터 데이터 블록에서 밀어냄
공유 풀(Shared Pool)
은 딕셔너리 캐시
와 라이브러리 캐시
로 구성, LRU 알고리즘
사용
=> SQL Server
에서는 프로시저 캐시(Procedure Cache)
라고 함
딕셔너리 캐시
=> 데이터베이스 딕셔너리(Dictionary)
: 테이블, 인덱스 같은 오브젝트
, 테이블스페이스
, 데이터 파일
, 세그먼트
, 익스텐트
, 사용자
, 제약
에 관한 메타 정보
를 저장하는 곳
=> 딕셔너리 캐시
: 딕셔너리 정보
를 캐싱
하는 메모리 영역
=> ex. 주문 테이블을 예로, 입력한 주문 데이터
는 데이터 파일
에 저장됐다가 버퍼 캐시
를 경유해 읽히지만, 테이블 메타 정보
는 딕셔너리
에 저장됐다가 딕셔너리 캐시
를 경유해 읽힘
라이브러리 캐시
=> 라이브러리 캐시(Library Cache)
: 사용자가 수행한 SQL문
과 실행계획
, 저장 프로시저
를 저장해 두는 캐시영역
=> 실행계획(Execution Plan)
: 사용자가 SQL 명령어
를 통해 결과 집합 요청시, 이를 최적
으로 수행하기 위한 처리 루틴 ( 빠른 쿼리 수행을 위해 내부적으로 생성한 일종의 프로시저 같은 것)
=> 하드 파싱(Hard Parsing)
: 쿼리 구문
을 분석해서 문법 오류
및 실행 권한
체크, 최적화(Optimization) 과정
을 거쳐 실행계획 생성
, SQL 실행엔진
이 이해할 수 있는 형태로 포맷팅
등의 전 과정
=> 특히 최적화
과정은 하드 파싱
을 무겁게 만드는 가장 결정적 요인이므로, 같은 SQL 처리시 해당 작업 반복 수행은 매우 비효율적
=> 같은 SQL
에 대한 반복적인 하드파싱
을 최소화
하기 위한 캐시 공간
이 바로 라이브러리 캐시 영역
DB 버퍼 캐시
에 가해지는 모든 변경사항
을 로그파일
에 기록하는데, 로그 엔트리
도 파일에 곧바로 기록하는 것이 아니라 먼저 로그 버퍼(Log Buffer)
에 기록
=> 일정량
을 모았다가 기록
하면 훨씬 빠르기 때문
서버 프로세스
가 데이터 블록 버퍼
에 변경
을 가하기 전, Redo 로그 버퍼
에 먼저 기록
해두면 주기적으로 LGWR 프로세스
가 Redo 로그 파일
에 기록
변경이 가해진 Dirty 버퍼
를 데이터 파일
에 기록하기 전에 항상 로그 버퍼
를 먼저 로그 파일에 기록
해야만 함
=> 인스턴스 장애
가 발생시 로그 파일
에 기록된 내용을 재현
해 캐시 블록
을 복구
하고, 최종적
으로 커밋되지 않은 트랜잭션
은 롤백
해야 함
=> 로그 파일
에 없는
변경내역이 이미 데이터 파일
에 기록돼 있으면 사용자가 최종 커밋하지 않은 트랜잭션이 커밋
되는 결과를 초래
=> Write Ahead Logging
: 버퍼 캐시 블록
을 갱신하기 전
에 변경사항
을 먼저 로그 버퍼
에 기록해야 함. Dirty 버퍼
를 디스크
에 기록하기 전에 해당 로그 엔트리
를 먼저 로그 파일
에 기록하는 것
=> Log Force at Commit
: 로그 버퍼
를 주기적으로 로그 파일
에 기록해야 하는데, 늦어도 커밋 시점
에는 로그 파일
에 기록해야 함. 메모리 상
의 로그 버퍼
는 언제든 유실
될 가능성이 있기 때문
=> 로그를 이용한 Fast Commit
이 가능한 이유는 로그
를 이용해 언제든 복구 가능
하기 때문이므로, 로그 파일에 기록했음이 보장
돼야 안심하고 커밋 완료 가능
PGA
: 프로세스
에 종속적
인 고유 데이터를 저장하는 용도로 사용
=> 래치 메커니즘
이 필요 없어
SGA
보다 훨씬 빠름
User Global Area(UGA)
=> 전용 서버
방식으로 연결하면 프로세스
와 세션
이 1:1관계
를 갖지만, 공유 서버
방식으로 연결하면 1:M관계
를 가짐. 즉, 세션
이 프로세스
개수보다 많아질 수
있는 구조로, 하나의 프로세스
가 여러 세션
을 위해 일함. 이때, 각 세션
을 위한 독립적인 메모리 공간
이 필요한데 이를 UGA(User Global Area)
라고 함
=> 전용 서버 방식
이라고 해서 UGA
를 사용하지 않는 것은 아니며, 전용 서버 방식
으로 연결시 UGA
는 PGA
에 할당
되고, 공유 서버 방식
으로 연결시 SGA
에 할당
Call Global Area(CGA)
=> 하나의 데이터베이스 Call
을 넘어서 다음 Call
까지 계속 참조
돼야 하는 정보는 UGA
에 담고, Call이 진행되는 동안
만 필요한 데이터
는 CGA
에 담는다
=> CGA
는 Parse Call
, Execute Call
, Fetch Call
마다 매번 할당
=> Call이 진행
되는 동안 Recursive Call
이 발생시 그 안에서도 Parse, Execute, Fetch
단계별로 CGA
가 추가 할당
=> CGA에 할당된 공간
은 하나의 Call이 끝
나자마자 해제
돼 PGA
로 반환
Sort Area
=> 데이터 정렬
을 위해 사용
=> 소트 오퍼레이션
이 진행되는 동안 공간이 부족
해질 때마다 청크(Chunk) 단위
로 조금씩 할당
=> workarea_size_policy
를 auto
로 설정 시 오라클이 내부적으로 크기
결정
=> PGA내
에서 Sort Area
가 할당되는 위치는 SQL 문 종류
와 소트 수행 단계
에 따라 다름
=> DML 문장
: 하나의 execute call
내에서 모든 데이터 처리
를 완료
하므로 sort Area가 CGA
에 할당
=> SELECT 문장
: 수행 중간 단계
에 필요한 Sort Area는 CGA
에 할당, 최종 결과 집합
을 출력하기 직전 단계에 필요한 Sort Area는 UGA
에 할당
=> SQL Server
는 데이터 정렬
을 Memory Pool
안에 있는 버퍼 캐시
에서 수행, 세션 관련 정보
는 Memory Pool
안에 있는 Connection Context 영역
에 저장
SQL(Structured Query Language)
: 구조적 질의 언어
=> 구조적(Structured)
이고 집합적(set-based)
이고 선언적(declarative)
인 질의 언어
=> 원하는 결과 집합을 구조적/집합적
으로 선언하지만, 그 결과 집합을 만드는 과정은 절차적
일 수 밖에 없어 프로시저
가 필요한데, 이 프로시저
를 만들어내는 DBMS 내부 엔진
이 바로 SQL 옵티마이저
SQL 옵티마이저(Optimizer)
: 사용자가 원하는 작업을 가장 효율적
으로 수행할 수 있는 최적의 데이터 액세스 경로
를 선택해주는 DBMS 핵심 엔진
옵티마이저 최적화 단계
쿼리
를 수행하는 데 후보군
이 될만한 실행계획
들을 찾아냄데이터 딕셔너리(Data Dictionary)
에 미리 수집해둔 오브젝트 통계
및 시스템 통계정보
를 이용해 각 실행계획
의 예상비용
을 산정최저 비용
을 나타내는 실행계획 선택
옵티마이저
가 특정 실행계획을 선택하는 근거
를 알아보자
--테스트용 테이블
create table t
as
select d.no, e.*
from scott.emp e, (select rownum no from dual connect by level<=1000) d;
--인덱스 생성
create index t_x01 on t(deptno, no);
create index t_x02 on t(deptno, job, no);
통계정보
를 수집
exec dbms_stats.gather_table_stats(user,'t');
-- 실행계획 확인
select /*+ gather_plan_statistics */ * from t
where deptno=10
and no=1;
select * from table(dbms_xplan.display_cursor(null,null,'ALL'));
t_x01
인덱스를 선택했을 땐 cost
가 2
이다.
select /*+ gather_plan_statistics index(t t_x02) */ * from t
where deptno=10
and no=1;
select * from table(dbms_xplan.display_cursor(null,null,'ALL'));
t_x02
인덱스를 사용하니 cost
가 7
가 됐다.
select /*+ gather_plan_statistics full(t) */ * from t
where deptno=10
and no=1;
select * from table(dbms_xplan.display_cursor(null,null,'ALL'));
Table Full Scan
을 하니 Cost
가 27
이 됐다.
즉, 옵티마이저
가 인덱스
를 선택하는 근거는 비용
이며, 비용(Cost)
은 쿼리를 수행하는 동안 발생할 것으로 예상되는 I/O 횟수
또는 예상 소요시간
을 표현한 것
=> 실행경로
를 선택하기 위해 옵티마이저
가 여러 통계정보
를 활용해 계산해 낸 값
옵티마이저 힌트
: 통계정보
가 정확X
또는 기타 다른 이유
로 옵티마이저
가 잘못된 판단
을 할 수 있어, 직접 인덱스를 지정
하거나 조인 방식을 변경
함으로써 더 좋은 실행계획으로 유도
힌트 기술 방법
SELECT /*+ LEADING(e2 e1) USE_NL(e1) INDEX(e1 emp_emp_id_pk) USE_MERGE(j) FULL(j) */
e1.first_name, e1.last_name, j.job_id, sum(e2.salary) total_sal
FROM employees e1, employees e2, job_history j
WHERE e1.employee_id = e2.manager_id
AND e1.employee_id = j.employee_id
AND e1.hire_date = j.start_date
GROUP BY e1.first_name, e1.last_name, j.job_id
ORDER BY total_sal;
index 힌트
에는 인덱스명
대신 칼럼명
지정 가능select /*+ leading(e2 e1) use_nl(e1) index(e1 (employee_id)) ... */
힌트가 무시되는 경우
문법적
으로 안 맞게 힌트 기술의미적
으로 안 맞게 힌트 기술 (ex. 서브쿼리에 unnest와 push_subq 같이 기술)잘못된 참조
사용 : 없는 테이블, 별칭(Alias)
사용, 없는 인덱스명
지정논리적
으로 불가능
한 액세스 경로 : 조인절에 등치 조건
이 하나도 없는데 해시 조인
으로 유도하거나 null 허용 컬럼
에 인덱스
를 이용해 전체 건수
를 세려고 시도하는 등select /*+ index(e emp_ename_idx) */ count(*) from emp e;
버그
=> 옵티마이저
는 힌트
를 선택 가능한 옵션 정도가 아닌, 사용자로부터 주어진 명령어
로 인식
=> Oracle
은 힌트 잘못 기술하거나 잘못된 참조시 에러 발생 X
=> SQL Server
는 에러
발생
힌트 종류
테이블 힌트
: 테이블명
다음에 WITH절
을 통해 지정fastfirstrow
, holdlock
, nolock
등조인 힌트
: FROM절
에 지정하며 두 테이블 간 조인 전략에 영향loop
, hash
, merge
, remote
등쿼리 힌트
: 쿼리
당 맨 마지막
에 한번
만 지정할 수 있는 쿼리 힌트, OPTION 절
이용모든 DBMS에서 I/O
는 블록(페이지)
단위로 이뤄짐
=> 하나의 레코드
를 읽더라도 레코드가 속한 블록
전체를 읽음
=> 블록 단위 I/O
는 버퍼 캐시
와 데이터 파일 I/O
에 모두 적용
데이터 파일
에서 DB 버퍼 캐시
로 블록
을 적재
할 때데이터 파일
에서 블록
을 직접
읽고 쓸 때버퍼 캐시
에서 블록
을 읽고 쓸 때버퍼 캐시
에서 변경된 블록
을 다시 데이터 파일
에 쓸 때디스크
를 경유한 데이터 입출력
: 디스크
의 액세스 암(Arm)
이 움직이면서 헤드
를 톨해 데이터 읽고 쓰므로 느림
메모리
를 통한 입출력
: 전기적 신호
에 불과하므로 빠름
모든 DBMS는 읽고자 하는 블록
을 먼저 버퍼 캐시
에서 찾아보고, 없을 경우
에만 디스크
에서 읽어 버퍼 캐시
에 적재
한 후 읽기/쓰기 작업
수행
물리적인 디스크 I/O
가 필요할 때면, 서버 프로세스
는 시스템
에 I/O Call
을 하고 잠시 대기 상태
에 빠지며, 디스크 I/O 경합
이 심할수록 대기 시간
도 길어짐
메모리
는 한정된 자원
이므로, 디스크 I/O를 최소화
하고 버퍼 캐시 효율
을 높이는 것이 I/O 튜닝
의 목표
버퍼 캐시 히트율(BCHR, Buffer Cache Hit Ration)
: 버퍼 캐시 효율
을 측정하는 지표로, 전체 읽은 블록
중에서 메모리 버퍼 캐시
에서 찾은 비율
=> 물리적인 디스크 읽기
를 수반하지 않고 곧바로 메모리
에서 블록을 찾은 비율
BCHR= (버퍼 캐시에서 곧바로 찾은 블록 수 / 총 읽은 블록 수) x 100
call count cpu elapsed disk query current rows
------ ---- ----- ------ ---- ----- ------ ----
Parse 15 0.00 0.08 0 0 0 0
Execute 44 0.03 0.03 0 0 0 0
Fetch 44 0.01 0.13 18 822 0 44
------ ---- ----- ------ ---- ----- ------ ----
total 103 0.04 0.25 18 822 0 44
disk 항목
: 디스크
를 경유한 블록 수
Query
+ Current
: 버퍼 캐시
에서 읽은 블록 수
총 읽은 블록 수(Query+Current
가 이미 디스크로부터 읽은 블록 수
를 포함
같은 블록
을 반복적
으로 액세스하는 형태의 SQL은 논리적인 I/O요청
이 많이 발생함에도 불구하고 BCHR
이 높게 나타남
=> BCHR
의 한계점
=> ex. NL 조인
에서 Inner 테이블
을 반복적으로 룩업
하는 경우, 작은 테이블
을 반복 액세스하면 모든 블록
이 메모리
에서 찾아져 BCHR
은 높지만, 일량이 작지 않고
, 블록
을 찾는 과정에서 래치 경합
과 버퍼 Lock 경합
까지 발생하면 메모리 I/O 비용
이 디스크 I/O 비용
보다 커짐
=> 논리적인 블록 읽기
를 최소화
네트워크 속도
도 SQL 성능에 영향
=> 네트워크 전송량이 많도록 SQL 작성하면, 결코 좋은 성능 X
=> 네트워크 전송량
을 줄이려고 노력
RAC
에선 인스턴스 간
캐시 된 블록
을 공유하므로 메모리 I/O 성능
에도 네트워크 속도
가 영향
파일 시스템 버퍼 캐시
는 최소화
해야 함
=> 데이터베이스 자체적
으로 캐시 영역
을 갖고 있으므로 이를 위한 공간을 크게 할당
하는 것이 더 효과적
근본적인 해결책은 논리적인 블록 요청 횟수
를 최소화
하는 것
시퀀셜 액세스
: 레코드간 논리적 또는 물리적인 순서
를 따라 차례대로
읽어 나가는 방식
=> 인덱스 리프 블록
에 위치한 모든 레코드는 포인터를 따라 논리적으로 연결
돼 있고, 이 포인터를 따라 스캔하는 것(ex. ⑤번)
=> 테이블 레코드 간에는 포인터로 연결되지는 않으나 테이블을 스캔
할 때는 물리적으로 저장된 순서대로
읽어 나가므로 이것 또한 시퀀셜 액세스 방식
랜덤 액세스
: 레코드간 논리적, 물리적인 순서를 따르지 않고
, 한 건
을 읽기 위해 한 블록씩 접근
하는 방식(ex. ④번, ⑥번)
블록 단위 I/O
를 하더라도 한번 액세스
할 때 시퀀셜 방식
으로 그 안에 저장된 모든 레코드
를 읽는다면 비효율X
하나의 레코드
를 읽으려고 한 블록씩 랜덤 액세스
하면 매우 비효율
I/O 튜닝의 핵심 원리
시퀀셜 액세스
에 의한 선택 비중
을 높임랜덤 액세스 발생량
을 줄임시퀀스 액세스 효율성
을 높이려면, 읽은 총 건수
중 결과 집합
으로 선택되는 비중
을 높여야 함
=> 같은 결과
를 얻기 위해 얼마나 적은 레코드
를 읽느냐
로 판단
-- drop table t
create table t
as
select * from all_objects
order by dbms_random.value;
select count(*) from t;
select /*+ gather_plan_statistics */
count(*)
from t where owner like 'SYS%';
select * from table(dbms_xplan.display_cursor(null,null,'ALLSTATS LAST'));
9337
레코드를 읽기 위해 17754
레코드를 읽어 52%가 선택되었다. 읽은 블록 수는 241
개다.
select count(*) from t where owner like 'SYS%' and object_name = 'ALL_OBJECTS';
17754
의 레코드를 읽고 1개
의 레코드를 선택했다.(위 사진은 두번째 실행해서 1로 나옴)
Table Full Scan
비효율이 높다.
읽은 블록 수도 똑같이 241개다.
=> 테이블 스캔
하면서 읽은 레코드
가 대부분 필터링
되고 일부만 선택된다면 인덱스
이용
create index t_idx on t(owner, object_name);
select /*+ index(t t_idx) */ count(*)
from t where owner like 'SYS%'
and object_name = 'ALL_OBJECTS';
1개
의 레코드를 구하기 위해 45개
의 블록을 읽었다.
인덱스 구성 칼럼 순서를 바꾸고 테스트 해보자
drop index t_idx;
create index t_idx on t(object_name, owner);
select /*+ index(t t_idx) */ count(*)
from t
where owner like 'SYS%'
and object_name = 'ALL_OBJECTS' ;
단 2개의 인덱스 블록(루트와 리프)
만 읽었다.
drop index t_idx;
create index t_idx on t(owner);
select object_id from t where owner = 'SYS' and object_name = 'ALL_OBJECTS';
8934
건을 읽어 그 횟수만큼 테이블 랜덤 액세스
했다. 최종적으로 한 건이 선택된 것에 비해 너무 많은 랜덤 액세스가 발생
drop index t_idx;
create index t_idx on t(owner, object_name);
select object_id from t
where owner = 'SYS'
and object_name = 'ALL_OBJECTS'
인덱스
로부터 1건
을 출력했으므로, 테이블
도 1번 방문
한다.
실제 발생한 테이블 랜덤 액세스
는 (3-2) 1번
이다.
Single Block I/O
: 한 번
의 I/O Call에 하나의 데이터 블록
만 읽어 메모리에 적재
=> 인덱스
를 통해 테이블 액세스시, 기본적으로 인덱스
와 테이블
모두 이 방식 사용
MultiBlock I/O
: I/O Call이 필요한 시점에, 인접한 블록
들을 같이 읽어 메모리에 적재
=> Table Full Scan
처럼 물리적으로 저장된 순서에 따라 읽을 때는 인접한 블록들을 같이 읽는 것이 유리
=> 익스텐트 범위
를 넘어서까지 읽지는 않음
=> 인접한 블록
: 한 익스텐트 내
에 속한 블록들
인덱스 스캔
시에는 Single Block I/O 방식
이 효율적
=> 인덱스 블록
간 논리적 순서(이중 연결 리스트 구조)
는 데이터 파일에 저장된 물리적인 순서
와 다르기 때문
=> 물리적으로 한 익스텐트에 속한 블록들을 메모리에 올렸는데, 그 블록들이 논리적 순서로는 한참 뒤쪽에 위치해 사용되지 못한 채 버퍼 상에서 밀려
나는 일이 발생
대량의 데이터
를 MultiBlock I/O 방식
으로 읽을 때 Single Block I/O보다 유리한 이유
=> I/O Call
발생 횟수를 줄여
주기 때문
--single block I/O
alter system flush buffer_cache; -- 디스크 I/O 발생하도록 버퍼 캐시 Flushing
create table t
as select * from all_objects;
alter table t add constraint t_pk primary key(object_id);
select /*+ index(t) */ count(*) from t where object_id > 0;
37개의 인덱스 블록
을 디스크
에서 읽으면서 37번의 I/O Call(db file sequential read 대기 이벤트)
발생
--Multiblock I/O
alter system flush buffer_cache; -- 디스크 I/O 발생하도록 버퍼 캐시 Flushing
select /*+ index_ffs(t) */ count(*) from t where object_id > 0;
똑같이 37개의 블록
을 디스크
에서 읽었는데 I/O Call이 9번(db file scattered read 대기 이벤트)
에 그침
Single Block I/O 방식
으로 읽은 블록들은 LRU 리스트
상 MRU 쪽(end)
으로 위치하므로 한 번 적재되면 버퍼 캐시
에 비교적
오래 머뭄
MultiBlock I/O 방식
으로 읽은 블록들은 LRU 리스트
상 LRU 쪽(end)
로 연결되므로 적재된 지 얼마 지나지 않아 1순위
로 버퍼캐시
에서 밀려남
논리적인 I/O 요청 횟수
를 최소화
하는 것이 I/O 효율화 튜닝
핵심
애플리케이션 측면
에서의 I/O 효율화 원리
최소 블록
만 읽도록 SQL 작성최적의 옵티마이징 팩터
제공옵티마이저 힌트
를 사용해 최적의 액세스 경로
로 유도동일한 데이터
를 중복 액세스 하지 않고 필요한 최소 블록
만 읽도록 작성
select a.카드번호 , a.거래금액 전일_거래금액 , b.거래금액 주간_거래금액 ,
c.거래금액 전월_거래금액 , d.거래금액 연중_거래금액
from ( -- 전일거래실적
select 카드번호, 거래금액
from 일별카드거래내역
where 거래일자 = to_char(sysdate-1,'yyyymmdd') ) a ,
( -- 전주거래실적
select 카드번호, sum(거래금액) 거래금액
from 일별카드거래내역
where 거래일자 between to_char(sysdate-7,'yyyymmdd')
and to_char(sysdate-1,'yyyymmdd')
group by 카드번호 ) b ,
( -- 전월거래실적
select 카드번호, sum(거래금액) 거래금액
from 일별카드거래내역
where 거래일자 between to_char(add_months(sysdate,-1),'yyyymm') || '01'
and to_char(last_day(add_months(sysdate,-1)),'yyyymmdd')
group by 카드번호 ) c ,
( -- 연중거래실적
select 카드번호, sum(거래금액) 거래금액
from 일별카드거래내역
where 거래일자 between to_char(add_months(sysdate,-12),'yyyymmdd')
and to_char(sysdate-1,'yyyymmdd')
group by 카드번호 ) d
where b.카드번호 (+) = a.카드번호
and c.카드번호 (+) = a.카드번호
and d.카드번호 (+) = a.카드번호
전일 데이터
를 총 4번 액세스 한 셈이 되는 SQL문이다.
select 카드번호 ,
sum( case when 거래일자 = to_char(sysdate-1,'yyyymmdd') then 거래금액 end ) 전일_거래금액 ,
sum( case when 거래일자 between to_char(sysdate-7,'yyyymmdd') and to_char(sysdate-1,'yyyymmdd') then 거래금액 end ) 주간_거래금액 ,
sum( case when 거래일자 between to_char(add_months(sysdate,-1),'yyyymm') || '01'
and to_char(last_day(add_months(sysdate,-1)),'yyyymmdd') then 거래금액 end ) 전월_거래금액 ,
sum( 거래금액 )연중_거래금액
from 일별카드거래내역
where 거래일자 between to_char(add_months(sysdate,-12),'yyyymmdd')
and to_char(sysdate-1,'yyyymmdd')
group by 카드번호
having sum( case when 거래일자 = to_char(sysdate-1,'yyyymmdd') then 거래금액 end ) > 0
과거 1년치 데이터
를 한 번만 읽고 모두 구할 수 있다.
인덱스 구성
: 가장 기본적인 옵티마이징 팩터DBMS
가 제공하는 기능
활용 : 인덱스, 파티션, 클러스터, 윈도우 함수
등을 적극 활용해 옵티마이저가 최적으로 선택할 수 있도록 함옵티마이저 모드 설정
: 옵티마이저 모드(전체 처리속도 최적화
, 최초 응답속도 최적화
)와 그 외 옵티마이저 행동에 영향을 미치는 일부 파라미터
를 변경해 주는 것이 도움이 될 수 있음통계정보
: 옵티마이저에게 정확한 정보
를 제공-- Oracle
select /*+ leading(d) use_nl(e) index(d dept_loc_idx) */ *
from emp e, dept d
where e.deptno = d.deptno
and d.loc = 'CHICAGO';
--SQL Server
select *
from dept d with (index(dept_loc_idx)), emp e
where e.deptno = d.deptno
and d.loc = 'CHICAGO'
option (force order, loop join)