-
불친절 SQL 1장 정리DataBase/튜닝 2024. 8. 19. 19:55
1. SQL이 느린 이유
-> 디스크 I/O 때문임! (I/O 처리하는 과정에서 프로세스가 잠을 자기 때문)
여러 프로세스가 하나의 CPU를 공유할 수 있지만, 특정 순간에는 하나의 프로세스만 CPU를 사용할 수 있다.
디스크에서 데이터를 읽어야 할 때 CPU를 OS에 반환하고 수면 상태에서 I/O 완료를 기다림
2. 데이터베이스 저장 구조(오라클)
-> 테이블 스페이스 : 여러 세그먼트를 담음
세그먼트는 여러 카테고리가 있음 (테이블, 인덱스, 파티션 등)
*세그먼트, 익스텐트,블록은 하나의 테이블에 대한 정보임
-> 세그먼트 <- 익스텐트 <- 블록의 포함관계가 있을 뿐 모두 하나의 테이블을 나타냄
-> 세그먼트 : 여러 익스텐트를 담는 저장 공간
-> 익스텐트 : 여러 블록으로 이루어짐
데이터를 저장할 블록이 꽉차면 블록 단위로 공간을 확장하는게 아니라,
익스텐트 단위로 공간을 확장함
-> 블록: 여러 레코드로 구성됨 (여러 행으로 구성)
여기서 레코드는 사용자가 입력한 (INSERT)한 값임
한 블록은 하나의 테이블이 독점함 (한 블록에 저장된 레코드는 모두 같은 테이블)
* 세그먼트에 할당된 모든 익스텐트가 같은 데이터 파일에 위치하지 않을 수 있다.
-> 즉, 위에서 설명한 저장구조는 논리적으로 같은 테이블을 가리키지만, 물리적으로 실제 저장될 때는 다른 위치에 저장될 수 있다.
(ex 익스텐트 1과 익스텐트 2는 같은 세그먼트에 할당되었지만, 익스텐트1의 내용은 파일1.db에 익스텐트2의 내용은 파일2.db에 저장될 수 있음)
3. DBA
모든 데이터 블록은 실제 물리적으로 저장된 위치를 가리키는 고유 주소값을 갖는다. 이를 DBA라고 부른다.
추후 살펴볼 인덱스의 ROWID가 DBA+로우번호로 구성된다.
즉, DBA는 물리적으로 저장되어있는 블록의 위치(ex C드라이브에 파일1.db) 로우번호는 해당 블록에서 순번이다.
*테이블을 스캔할 때 세그먼트 헤더에 저장된 익스텐트 맵을 이용한다(세그먼트에 할당된 익스텐트에 대한 정보)
익스텐트 맵을 통해 해당 익스텐트 첫 블록에 DBA를 알 수 있다.
4. 블록 단위 I/O
DB에서 데이터를 읽고 쓰는 단위는 블록이다. 하나의 데이터만 읽고 싶어도 블록 전체를 다 읽어야한다.
인덱스 또한 블록 단위로 데이터를 읽고 쓴다.
4.1 시퀀셜 엑세스 vs 랜덤 엑세스
시퀀셜 엑세스
- 논리적 혹은 물리적으로 연결된 순서에 따라 블록을 읽는다 (블록1 -> 블록2 -> 블록3)
-DB를 순차적으로 스캔하는 방식이다. (full scan 방식)
랜덤 엑세스
- 논리적 혹은 물리적 순서에 따르지 않고, 레코드 하나를 읽기 위해 한 블록씩 접근하는 방식이다.
(블록1 -> 블록 4 -> 블록 10)
- INDEX ROWID를 통해 테이블에 접근할 때 랜덤 엑세스가 발생한다.
5. 논리적 I/O와 물리적 I/O
논리적 I/O
논리적 블록 I/O는 SQL을 처리하는 과정에서 발생한 총 블록 I/O를 의미한다.
물리적 I/O
물리적 블록I/O는 디스크에서 발생한 총 블록 I/O를 의미한다.
블록을 버퍼캐시에서 찾지 못할 때만 디스크를 엑세스하므로 논리적 블록 I/O중 일부를 물리적 I/O라 한다.
*논리적 I/O는 같은 조건절이라면 매번 결과가 같지만, 물리적 I/O는 다를 수 있다 -> 이는 물리적 I/O를 하며, 읽은 블록을 버퍼캐시에 저장하기 때문이다 -> 즉, SQL을 날리면 일단 버퍼 캐시를 먼저 확인하고, 여기에서 SQL에서 SELECT한 데이터를 담은 블록을 찾지 못하면 물리적 I/O를 진행한다.
*DB 튜닝의 핵심은 전체 I/O를 의미하는 논리적 I/O 횟수를 줄여 물리적 I/O를 줄이는 것이다.
6. Single Block I/O vs MultiBlock I/O
Single Block I/O
- 한 번에 한 블록씩 요청에서 메모리에 적재하는 방식이다.
- 대량의 데이터에서 소량을 찾을 때 효율적이다.
- 인덱스 루트 블록을 읽을 때
- 인덱스 루트 블록을 통해 블랜치 블록을 읽을 때
- 브랜치 블록을 통해 리프 블록을 읽을 때 사용된다.
MultiBlock I/O
- 한 번에 여러 블록씩 메모리에 적재하는 방식이다.
- 대량의 데이터 블록을 읽을 때는 이것이 훨씬 효율적이다 -> 테이블 전체 스캔
- 캐시에서 찾지 못한 특정 블록을 읽기 위해 I/O Call을 수행할 대 인접 블록을 한꺼번에 적재한다.
*대량의 데이터를 Single Block I/O로 읽는다면 그만큼 대기 시간이 소모될 것이다.
-> 이때는 MultiBlock가 낫다 한번에 많이 읽고 대기하면 된다.
7. Table Full Scan vs Index Range Scan
- Table Full Scan (시퀀셜 엑세스 + MultiBlock I/O)
> 테이블에 속한 블록 전체를 읽어서 데이터를 찾는다.
> 대량의 데이터를 찾을 때 유리하다
- Indexd Range Scan (랜덤 엑세스 + SingleBlock I/O)
> 인덱스에서 일정량을 스캔하며 얻은 ROWID로 데이터를 찾아간다.
*읽어야할 데이터가 많을 수록 혹은 테이블의 데이터가 많을 수록 Index가 느린 이유(면접 사골)
- Index를 이용한 스캔은 랜덤 엑세스 + Single Block I/O 방식을 활용하기 때문이다.
-> 애초에 인덱스를 사용하는 이유는 많은 데이터 중 필요한 일부를 빠르게 찾기 위함이다.
- 랜덤 엑세스이자 Single Block I/O 방식이므로, 논리적 혹은 물리적으로 저장된 순서대로 데이터 블록을 한꺼번에 읽어 오는게 아니라, ROWID를 통해 한 번에 한 개씩 블록을 읽어오므로, 이 과정에서 많은 I/O가 발생한다
-> 많은 I/O는 프로세스의 많은 대기시간을 유발한다.
- 따라서 인덱스를 통해 읽어야할 데이터가 많아진다면, 그만큼 속도가 떨어질 수 밖에 없다.
- 이럴때는 테이블을 한번에 읽어서 순차적으로 데이터를 처리하는 편이 훨씬 빠르다.
-> Table Full Scan이 시퀀셜 엑세스에 MultiBlock I/O 방식이므로, 간혹 풀 스캔이 인덱스보다 빠른 이유는 여기에 있다고 할 수 있다.
8. 캐시 탐색 매커니즘
대부분의 블록 I/O는 버퍼캐시를 경유한다. (인덱스를 사용하든, Full Scan하든)
DBMS의 버퍼캐시는
이 따위로 생겼다.
- 모듈러(나머지연산)을 통해 블록을 해시 체인에 먼저 연결한다. (같은 블록 번호는 항상 같은 해시 체인에 연결 - 정렬은x)
- 블록을 탐색할 때 버퍼를 모두 full scan 하는게 아니라, 해시 체인에 연결을 먼저 확인한다 (ROWID에 블록 번호가 있으니 이걸로 나머지 연산을 해서 해시체인 번호를 선택하고 이를 쭉 읽는다고 생각하자)
- 해시 체인에 찾고자하는 블록이 없으면 물리적 I/O를 통해 이를 읽어오고 해시체인에 연결한다.
- 해시 체인에 찾고자하는 블록이 있으면, 해당 정보로 버퍼 블록을 찾아가서 블록을 읽는다
* 같은 블록 번호는 항상 같은 해시체인에 연결되지만, 항상 같은 위치의 버퍼 블록에 연결되는 것은 아니다
> 버퍼블록은 계속 지워졌다 채워졌다함! 항상 같은 위치에 저장될 수 없음 (버퍼 블록 != 해시체인)
> 해시 체인은 버퍼블록을 더 효율적으로 탐색하기 위한 장치일 뿐!
9. 체인 래치
블록을 탐색할 때 해시 체인 구조에 변경이 생기면 안된다.
따라서 해시 체인에 Lock을 건다 이를 체인 래치라 부른다. (한 프로세스가 독점)
읽고자하는 블록을 찾으면 Lock를 곧 바로 해제한다.
이때 Lock을 해제하는 순간에 같은 블록에 데이터를 읽고 쓴다면 데이터 정합성 문제가 발생할 수 있다.
따라서 오라클은 버퍼에도 Lock을 건다.
체인 래치를 해제하기 전에 버퍼에도 Lock을 걸어 블록 버퍼에 대한 접근을 일시적으로 막는다.
'DataBase > 튜닝' 카테고리의 다른 글
Spring 성능 테스트 With Ngrinder (1) - Ngrinder 설치 (0) 2024.08.22 불친절 SQL 2장 정리 (인덱스 기본) (0) 2024.08.19 조인 튜닝 - 서브쿼리 조인 (0) 2024.08.08 조인 튜닝 - 소트 머지 조인 (0) 2024.08.07 조인 튜닝 - NL 조인 (0) 2024.08.07