

데이터를 저장하려면 먼저 테이블 스페이스를 생성해야한다. 테이블 스페이스는 세그먼트를 담는 컨테이너로서 여러개의 데이터파일로 구성된다.
테이블 스페이스를 생성했으면 세그먼트를 생성한다. 세그먼트는 테이블, 인덱스처럼 데이터 저장공간이 필요한 오브젝트다. 테이블, 인덱스를 생성할 때 데이터를 어떤 테이블스페이스에 저장할지를 지정한다.
세그먼트는 여러 익스텐트로 구성된다.
익스텐트는 공간을 확장하는 단위다. 테이블이나 인덱스에 데이터를 입력하다가 공간이 부족하면 해당 오브젝트가 속한 테이블스페이스로부터 익스텐트를 추가로 할당받는다.
익스텐트는 연속된 블록들의 집합이기도하다.
익스텐트 단위로 공간을 확장하지만, 사용자가 입력한 레코드를 실제로 저장하는 곳은 데이터 블록이다.
한 블록은 하나의 테이블이 독점한다. 즉, 한 익스텐트에 담긴 블록은 모두 같은 테이블 블록이다.
세그먼트 공간이 부족해지면 테이블스페이스로부터 익스텐트를 추가로 할당받는다고 했는데, 세그먼트에 할당된 모든 익스텐트가 같은 데이터파일에 위치하지 않을 수 있다.
사실 서로 다른 데이터파일에 위치할 가능성이 더 높다. 하나의 테이블페이스를 여러 데이터파일로 구성하면, 파일 경합을 줄이기위해 DBMS가 데이터를 가능한 한 여러 데이터파일로 분산해서 저장하기 때문이다. 즉, 익스텐트 내 블록들은 서로 인접한 연속된 공간이지만, 익스텐트끼리는 연속된 공간이 아닐수 있다.
데이터가 실제로 저장되는곳은 데이터블록이라했는데 이 데이터블록의 주소값은 뭘까?
이 주소값을 DBA(Data Block Address) 라고한다.
모든 데이터블록은 디스크 상에서 몇 번 데이터파일의 몇 번째 블록인지를 나타내는 고유 주소를 갖는데 그걸 DBA라고한다. 데이터를 읽고 쓰는 단위가 블록이니까 데이터를 읽으려면 이 DBA부터 확인해야 한다.
인덱스를 이용해 테이블 레코드를 읽을때는 ROWID를 이용한다. 이 ROWID = DBA + 로우 번호(블록내 순번)이므로, 읽어야 할 테이블 레코드가 저장된 DBA를 알 수 있다.
즉, 테이블을 스캔할 때는 테이블 세그먼트의 헤더에 저장된 익스텐트 맵을 통해 각 익스텐트의 첫 번째 블록 DBA를 알아와서, 연속해서 저장된 블록들을 읽으면된다.
블록단위 I/O
데이터 I/O 단위가 블록이므로 특정 레코드 하나를 읽고 싶어도 해당 블록을 통째로 읽는다.
즉, 1Byte짜리 컬럼 하나만 읽고 싶어도 블록을 통째로 읽는다.
테이블뿐만 아니라 인덱스도 블록 단위로 데이터를 읽고 쓴다.
Sequential 액세스 VS Random 액세스
논리적 또는 물리적으로 연결된 순서에 따라 차례대로 블록을 읽는 방식이다. 인덱스 리프블록은 앞뒤를 가리키는 주소값을 통해 논리적으로 서로 연결돼있다. 이 주소 값에 따라 앞/뒤로 순차적으로 스캔하는 방식이 시퀀셜 액세스이다.
세그먼트에 할당된 익스텐트 목록들을 세그먼트 헤더에 맵으로 관리하고 있다. 익스텐트 맵들은 각 익스텐트의 첫 번째 블록 주소값을 갖고있다.
따라서, 읽어야할 익스텐트 목록들을 세그먼트 헤더의 익스텐트맵에서 가져오고, 각 익스텐트의 첫번째 블록에서 연속으로 이어진 블록들을 순서대로 읽으면 테이블 Full Scan 하는것이다.
인덱스 리프 블록은 앞뒤를 가리키는 주속값을 통해 논리적으로 서로 연결돼 있다. 이 주소값에 따라서 앞 뒤로 순차적으로 읽으면 인덱스 시퀀셜 액세스하는 것이다.
논리적, 물리적인 순서를 따르지 않고, 레코드 하나를 읽기 위해 한 블록씩 접근 하는 방식이다.
SQL코드를 캐싱하는곳이 라이브러리 캐시 라면, 데이터블록을 캐싱하는 곳은 DB버퍼캐시 이다.
SQL을 수행하는 과정에 계속해서 데이터 블록을 읽는데, 자주 읽는 블록을 매번 디스크에서 읽는 것은 즉, 디스크 I/O를 하는 건 매우 비효율적이다.
데이터 캐싱 매커니즘이 필요한 이유이다. 오라클에서는 디스크에서 읽어온 데이터블록을 메모리 공유영역인 SGA의 DB버퍼캐시에 캐싱해둠으로써 같은 블록에 대한 반복적인 I/O call을 줄인다.
따라서, 데이터 블록을 읽으려고 할 때 항상 버퍼캐시부터 탐색하게되고, DB버퍼캐시는 공유메모리 영역이므로, 같은 블록을 읽는 다른 서버프로세스들도 득을 본다.
SQL문을 처리하는 과정에서
논리적 블록 I/O : 메모리 버퍼캐시에서 발생한 총 블록 I/O
물리적 블록 I/O : 디스크에서 발생한 총 블록 I/O
SQL처리 도중 읽어야 할 블록을 버퍼캐시에서 못찾았을 때만 디스크를 액세스하므로 논리적 블록I/O 중, 일부를 물리적 블록I/O 가 발생한다.
블록을 읽을 때는 해당 블록을 먼저 버퍼캐시에서 찾아보고 없을 때만 디스크에서 읽는다. 이때도 디스크에서 곧바로 읽는게 아니라 먼저 버퍼캐시에 적재하고서 읽는다. 캐시에서 찾지못한 데이터 블록은 I/O call을 통해 디스크에서 DB버퍼캐시로 적재하고서 읽는데, I/O Call을 할 때 한 번에 한 블록씩 요청(Single Block I/O)하기도하고, 여러 블록씩 요청(Multiblock I/O)하기도한다.
테이블에 저장된 데이터를 읽는 방식은 두 가지이다.
테이블 전체를 스캔하는 방식, 인덱스를 이용해서 읽는 방식이다.
모든 블록 I/O는 메모리 버퍼캐시를 경유하는데, 버퍼캐시를 해시구조로 관리하기때문에, 버퍼캐시에서 데이터를 찾아가기위해 캐시 탐색 알고리즘을 알아야한다.
![]()
버퍼캐시에서 블록을 찾을 때, 해시 알고리즘으로 특정 해시체인을 찾고, 해시체인에서 버퍼헤더를 찾아서, 버퍼헤더에서 찾은 포인터로 버퍼블록을 찾아간다.
메모리 버퍼캐시는 SGA구성요소이기 때문에 공유자원이므로 모든 프로세스가 접근할 수 있다. 하지만 하나의 버퍼블록을 여러 프로세스가 동시에 접근할 수는 없다. 특정 해시체인을 찾아 버퍼헤더를 찾는 도중에 다른 프로세스가 해시체인을 바꾸려하는걸 막기위해 버퍼캐시 체인 Latch 라는게 존재한다.
위 그림에서 해시체인의 0~4체인 앞쪽에 자물쇠가 있다고 생각하면된다. 특정 프로세스가 20번 블록을 찾기위해 0번 해시체인을 탐색하려한다면 0번 해시체인을 접근할수 있는 key를 얻어서 탐색하기시작하고, 0번해시체인에 lock을 걸어서 다른 프로세스는 접근하지못하고 줄서서 기다리게 할 수 있도록 하는 메커니즘이다.
즉, SQL의 수행속도를 높이려면 버퍼캐시 히트율도 높여야하지만 여러 프로세스가 있을때 이런 레치의 경합에 의해 속도가 느려질수도 있기때문에 캐시 경합을 줄이려면 SQL튜닝을 통해 쿼리일량 자체를 줄여야한다.