Database/SQL 튜닝

SQL 처리 과정과 I/O

Debin 2023. 4. 27.
반응형

SQL 파싱과 최적화

프로시저 일련의 쿼리를 마치 하나의 함수처럼 실행하기 위한 쿼리의 집합이다.

프로시저를 만들어주는 DBMS 내부 엔진이 바로 SQL 옵티마이저다. 

DBMS 내부에서 프로시저를 작성하고 컴파일해서 실행 가능한 상태로 만드는 전 과정을 'SQL 최적화'라고 한다.

SQL 최적화

최적화 순서 과정은 아래와 같다.

1. SQL 파싱

SQL 파싱을 요약하면 아래와 같다.

 

  • 파싱 트리 생성: SQL 문을 이루는 개별 구성요소를 분석해서 파싱 트리를 생성한다.
  • Syntax 체크: 문법적 오류가 없는지 확인한다.
  • Semantic 체크: 의미상 오류가 없는지 확인한다. 예시로는 권한이 있는지 확인.

2. SQL 최적화

SQL 옵티마이저는 미리 수집한 시스템 및 오브젝트 통계정보를 바탕으로 다양한 실행경로를 생성해서 비교한 후 가장 효율적인 하나를 선택한다. 데이터베이스 성능을 결정하는 가장 핵심적인 엔진이다.

3. 로우 소스 생성

SQL 옵티마이저가 선택한 실행경로를 실제 실행 가능한 코드 또는 프로시저 형태로 포맷팅하는 단계다.

로우 소스 생성기가 그역할을 맡는다.

SQL 옵티마이저

SQL 옵티마이저는 사용자가 원하는 작업을 가장 효율적으로 수행할 수 있는 최적의 데이터 액세스 경로를 선택해 주는 DBMS의 핵심 엔진이다. 옵티마이저의 최적화 단계를 요약하면 아래와 같다.

 

  1. 사용자로부터 전달받은 쿼리를 수행하는 데 후보군이 될만한 실행계획들을 찾아낸다.
  2. 데이터 딕셔너리에 미리 수집해 둔 오브젝트 통계 및 시스템 통계정보를 이용해 각 실행계획의 예상비용을 산정한다.
  3. 최저 비용을 나타내는 실행계획을 선택한다.

실행계획과 비용

SQL 옵티마이저가 생성한 처리절차를 사용자가 확인할 수 있게 트리구조로 표현한 것이 실행계획이다.

이 기능을 통해 자신이 작성한 SQL이 테이블을 스캔하는지 인덱스를 스캔하는지, 인덱스를 스캔한다면 어떤 인덱스인지를 확인할 수 있고,

예상과 다른 방식으로 처리된다면 실행경로를 변경할 수 있다.

 

실행 계획을 통해 자신이 작성한 SQL이 테이블을 스캔하는지 인덱스를 스캔하는지,

인덱스를 스캔한다면 어떤 인덱스인지를 확인할 수 있고, 예상과 다른 방식으로 처리된다면 실행경로를 변경할 수 있다.

 

정말 간단한 테이블을 하나 만들고 인덱스를 걸어서 실행 계획을 살펴보자.

-- 테이블 생성 SQL - test
CREATE TABLE test
(
    `id`      INT            NOT NULL    AUTO_INCREMENT, 
    `name`    VARCHAR(45)    NULL, 
    `cost`    VARCHAR(45)    NULL, 
    `options`  VARCHAR(45)    NULL, 
     PRIMARY KEY (id)
);

CREATE INDEX idx_test_1 ON test ( name );
CREATE INDEX idx_test_2 ON test ( name, options );

다음은 조회 쿼리 앞에 explain 키워드를 붙이면 실행 계획을 확인할 수 있다.

간단한 실행 계획 확인

옵티마이저 힌트

개발자가 더 효율적인 액세스 경로를 직접 지정할 수도 있다. 이럴 때 사용하는 것이 바로 옵티마이저 힌트다.

이 힌트는 다음에 더 자세히 살펴보고 이번에는 넘어가겠다.

SQL 공유 및 재사용

소프트 파싱 VS 하드 파싱

SQL 파싱, 최적화, 로우 소스 생성 과정을 거쳐 생성한 내부 프로시저를 반복 재사용할 수 있도록 캐싱해 두는 메모리 공간을

라이브러리 캐시라고 한다.

라이브러리 캐시는 SGA(System Global Aread)에 존재한다.

SGA는 서버 프로세스와 백그라운드 프로세스가 공통으로 액세스하는 데이터와 제어 구조를 캐싱하는 메모리 공간이다.

 

사용자가 SQL 문을 전달하면 DBMS는 SQL을 파싱한 후 해당 SQL이 라이브러리 캐시에 존재하는지부터 확인한다.

캐시에서 찾으면 곧바로 실행 단계로 넘어가지만, 찾지 못하면 최적화 단계를 거친다.

SQL을 캐시에서 찾아 곧바로 실행단계로 넘어가는 것을 소프트 파싱이라 하고, 

찾는 데 실패해 최적화 및 로우 소스 생성 단계까지 모두 거치는 것을 하드 파싱이라고 한다.

바인드 변수의 중요성

라이브러리 캐시에서 SQL을 찾기 위해 사용하는 키 값이 SQL문 그 자체이므로 아래는 모두 다른 SQL이다.

의미적으로 모두 같지만, 실행할 때 각각 최적화를 진행하고 라이브러리 캐시에서 별도 공간을 허용한다.

SELECT * FROM WHERE empno = 7900;
select * from EMP where EMPNO = 7900;
select * from emp where empno = 7900;
select * from emp where empno = 7900 ;
select * from emp where empno = 7900  ;
select * scott.emp where empno = 7900;

500만 고객을 보유한 웹 서비스의 코드에서 다음과 같이 SQL 문자열을 만든다.

String SQLStmt = "SELECT * FROM CUSTOMER WHERE LOGIN_ID = '" + login_id + "'";

어느날 이 서비스에서 대대적인 할인 이벤트를 하기로 했다.

DBMS에 발생하는 부하는 대개 과도한 I/O가 원인인데, 이날은 I/O가 거의 발생하지 않았음에도 불구하고 CPU 사용률은 급격히 올라가고, 라이브러리 캐시에 발생하는 여러 종류의 경합 때문에 로그인이 처리되지 않을 것이다.

각 고객에 대해 동시다발적으로 발생하는 SQL 파싱 때문이다.

그 순간 라이브러리 캐시를 조회해 보면, 아래와 같은 SQL로 가득 차 있다.

SELECT * FROM CUSTOMER WHERE LOGIN_ID = 'oraking'
SELECT * FROM CUSTOMER WHERE LOGIN_ID = 'kim'
SELECT * FROM CUSTOMER WHERE LOGIN_ID = 'tommy'
SELECT * FROM CUSTOMER WHERE LOGIN_ID = 'lee'

로그인 프로그램을 이렇게 작성하면,

고객이 로그인할 때마다 아래와 같이 DBMS 내부 프로시저를 하나씩 만들어서 라이브러리 캐시에 적재하는 셈이다. 

 

위 프로시저의 내부 처리는 모두 같다. 그렇다면 로그인 ID를 파라미터로 받는 프로시저 하나를 공유하면서 재사용하는 것이 마땅하다.

이처럼 파라미터 Driven 방식으로 SQL을 작성하는 방법이 제공되는데, 바인드 변수가 바로 그것이다.

아래와 같이 수정하면 된다.

String SQLStmt = "SELECT * FROM CUSTOMER WHERE LOGIN_ID = ?";

이 SQL에 대한 하드파싱은 최초 한 번만 일어나고, 캐싱된 SQL을 100만 고객이 공유하면서 재사용한다.

데이터 저장 구조 및 I/O 메커니즘

SQL이 느린 이유는 디스크 I/O 때문이다.

책에서는 I/O를 잠이라고 설명한다. OS 또는 I/O 서브시스템이 I/O를 처리하는 동안 프로세스는 잠을 자기 때문이다.

실행 중인 프로세서는 인터럽트에 의해 수시로 실행 준비 상태로 전환했다가 다시 실행 상태로 전환한다.

여러 프로세스가 하나의 CPU를 공유할 수 있지만, 특정 순간에는 하나의 프로세스만 CPU를 사용할 수 있다.

 

인터럽트 없이 열심히 일하던 프로레스도 디스크에서 데이터를 읽어야 할 땐 CPU를 OS에 반환하고 잠시 수면 상태에서 I/O가 완료되기를 기다린다. 정해진 OS 함수를 호출하고 CPU를 반환한 채 알람을 설정하고 대기 큐에서 잠을 자는 것이다.

열심히 일해야 할 프로세스가 한가하게 잠을 자고 있으니 I/O가 많으면 성능이 느릴 수 밖에 없다.

데이터베이스 저장 구조

데이터를 저장하려면 먼저 테이블 스페이스를 생성해야 한다.

테이블 스페이스는 세그먼트를 담는 콘테이너로서, 여러 개의 데이터 파일로 구성된다. 

테이블 스페이스

  • 세그먼트는 테이블, 인덱스처럼 데이터 저장 공간이 필요한 오브젝트다. 테이블, 인덱스를 생성할 대 데이터를 어떤 테이블 스페이스에 저장할지 결정한다. 테이블 또는 인덱스가 파티션 구조라면, 각 파티션이 하나의 세그먼트가 된다.
  • 세그먼트는 여러 익스텐트로 구성된다. 익스텐트는 공간을 확장하는 단위다. 테이블이나 인덱스에 데이터를 입력하다가 공간이 부족해지면 해당 오브젝트가 속한 테이블스페이스로부터 익스텐트를 추가로 할당받는다. 익스텐트는 연속된 블록들의 집합이기도 하다.
  • 익스텐트는 단위로 공간을 확장하지만, 사용자가 입력한 레코드를 실제로 저장하는 공간은 블록이다. 한 블록은 하나의 테이블이 독점한다. 즉, 한 블록에 저장된 레코드는 모두 같은 테이블 레코드다.
  • 한 익스텐트도 하나의 테이블이 독점한다. 즉, 한 익스텐트에 담긴 블록은 모두 같은 테이블 블록이다.

블록은 수많은 데이터 로우를 가진다.

세그먼트 공간이 부족해지면 테이블스페이스로부터 익스텐트를 추가로 할당받는데,

세그먼트에 할당된 모든 익스텐트가 같은 데이터 파일에 위치하지 않을 수 있다. 서로 다른 데이터파일에 위치할 가능성이 더 높다.

하나의 테이블 스페이스를 여러 데이터파일로 구성하면, 파일 경합을 줄이기 위해 DBMS가 가능한 한 여러 데이터파일로 분산해서 저장하기 때문이다.

 

정리하자면 익스텐트 내 블록은 서로 인접한 연속된 공간이지만, 익스텐트끼리는 연속된 공간이 아니다.

 

  • 블록: 데이터를 읽고 쓰는 단위
  • 익스텐트: 공간을 확장하는 단위. 연속된 블록 집합
  • 세그먼트: 데이터 저장 공간이 필요한 오브젝트(테이블, 인덱스, 파티션, LOB 등)
  • 테이블 스페이스: 세그먼트를 담는 콘테이너
  • 데이터파일: 디스크 상의 물리적인 OS 파일

블록 단위 I/O

블록은 DBMS가 데이터를 읽고 쓰는 단위다.

데이터 I/O가 블록이므로 특정 레코드 하나를 읽고 싶어도 해당 블록을 통째로 읽는다.

1Byte짜리 컬럼을 하나만 읽고 싶어도 블록을 통째로 읽는다.

인덱스도 마찬가지로 블록 단위로 데이터를 읽고 쓴다.

시퀀셜 액세스 VS 랜덤 액세스

테이블 또는 인덱스 블록을 액세스하는 방식으로는 시퀀셜 액세스와 랜덤 액세스, 두 가지가 있다.

시퀀셜 액세스는 논리적 또는 물리적으로 연결된 순서에 따라 차례대로 블록을 읽는 방식이다.

인덱스 리프 블록은 앞뒤를 가리키는 주소값을 통해 논리적으로 서로 연결돼 있다.

이 주소 값에 따라 앞 또는 순차적으로 스캔하는 방식이 시퀀셜 액세스다.

 

테이블 블록 간에는 서로 논리적인 연결고리를 갖고 있지 않다. 그럼 어떻게 시퀀셜 방식으로 액세스할까?

오라클 같은 경우 세그먼트에 할당된 익스텐트 목록을 세그먼트 헤더에 맵(map)으로 관리하는데,

익스텐트 맵을 각 인스텐트의 첫 번째 블록 주소 값을 가진다.

읽어야할 익스텐트 목록을 익스텐트 맵에서 얻고, 각 익스텐트의 첫 번째 블록 뒤에 연속해서 저장된 블록을 순서대로 읽으면,

그것이 곧 Full Scan Scan이다.

 

랜덤 액세스는 추후에 더 자세히 살펴보겠다.

논리적 I/O VS 물리적 I/O

디스크 I/O가 결국 SQL 성능을 결정한다.

SQL을 수행하는 과정에서 데이터 블록을 읽는데, 자주 읽는 블록을 매번 디스크에서 읽는 것은 매우 비효율적이다.

모든 DBMS에 데이터 캐싱이 메커니즘이 필수인 이유다.

라이브러리 캐시 말고도 DB 버퍼 캐시라는 중요한 구성요소가 있다.

라이브러리 캐시가 SQL과 실행 계획, DB 저장형 함수/프로시저를 캐싱하는 '코드 캐시'라고 한다면,

DB 버퍼캐시는 '데이터 캐시'라고 할 수 있다.

디스크에서 어렵게 읽은 데이터 블록을 캐싱해 둠으로써 같은 블록에 대한 반복적인 I/O Call을 줄이는데 목적이 있다.

 

서버 프로세스와 데이터 파일 사이에 버퍼캐시가 있으므로 데이터 블록을 읽을 땐 항상 버퍼캐시부터 탐색한다. 

운 좋게 캐시를 찾는다면 바쁜 시간에 프로세스가 잠(I/O Call)을 자지 않아도 된다.

 

논리적 블록 I/O는 SQL을 처리하는 과정에 발생한 총 블록 I/O를 말한다.

일반적으로 메모리상의 버퍼 캐시를 경유하므로 메모리 I/O가 곧 논리적 I/O라고 생각해도 무방하다.

물리적 블록 I/O는 디스크에서 발생한 총 블록 I/O를 말한다.

SQL 처리 도중 읽어야할 블록을 버퍼 캐시에서 찾지 못할 때만 디스크를 액세스하므로 논리적 블록 I/O 중 일부를 물리적으로 I/O 한다.

참고로 메모리 I/O에 비해 디스크 I/O는 10,000배 정도 느리다.

 

Direct Path Read 방식으로 읽는 경우를 제외하면 모든 블록은 DB 버퍼 캐시를 경유해서 읽는다.

따라서 논리적 I/O 횟수는 일반적으로 DB 버퍼캐시에서 블록을 읽은 횟수와 일치한다.

논리적 I/O가 메모리 I/O와 완전 일치하는 개념은 아니지만, 결과적으로 수치는 같다.

 

DB 버퍼캐시에서 블록을 찾지 못해 디스크에서 읽은 블록 I/O가 물리적 I/O다.

데이터 입력이나 삭제가 없어도 물리적 I/O는 SQL을 실행할 때마다 다르다.

첫 번째 실행할 때보다 두 번째 실행할 때 줄어들고, 세 번째 실행할 땐 더 줄어든다.

연속해서 실행하면 DB 버퍼캐시에서 해당 테이블 블록의 점유율이 점점 높아지기 때문이다.

한참 후에 다시 실행하면 반대로 물리적 I/O가 늘어난다. DB 버퍼캐시가 다른 테이블 블록으로 채워지기 때문이다.

버퍼 캐시 히트율

버퍼캐시 효율을 측정하는 데 전통적으로 가장 많이 사용해 온 지표는 버퍼캐시 히트율이다.

Buffer Cache Hit Ratio 줄여서 BCHR라고 하며, 구하는 공식은 아래와 같다.

BCHR = ( 캐시에서 곧바로 찾은 블록 수 / 총 읽은 블록 수) * 100
           = ( (논리적 I/O - 물리적 I/O) / 논리적 I/O) * 100
           = ( 1 - (물리적 I/O) / (논리적 I/O)) * 100

BCHR은 읽은 전체 블록 중에서 물리적인 디스크 I/O를 수반하지 않고 곧바로 메모리에서 찾은 비율을 나타낸다.

온라인 트랜잭션을 주로 처리하는 애플리케이션이라면 시스템 레벨에서 평균 99% 히트율을 달성해야 한다.

핵심 트랜잭션이 시스템 전체 부하의 대부분을 차지하므로 열심히 튜닝하면 99%는 결코 달성하기 어려운 수치가 아니다. (?!)

 

BCHR에서 중요한 성능 원리는 바로 물리적 I/O가 성능을 결정하지만,

실제 SQL 성능을 향상하려면 물리적 I/O가 아닌 논리적 I/O를 줄여야 한다는 사실이다.

위 식을 아래와 같이 변경할 수 있다.

물리적 I/O = 논리적 I/O * (100% - BCHR)

데이터를 insert, update, delete 하지 않는다면 조건절의 변수가 동일하다면 논리적 I/O는 일정하다.

결국 물리적 I/O는 BCHR에 의해 결정된다.

BCHR은 시스템 상황에 따라 달라지므로 물리적 I/O는 결국 시스템 상황에 의해 결정되는 통제 불가능한 외생변수다.

따라서 SQL 성능을 높이기 위해서 할 수 있는 일은 논리적 I/O를 줄이는 것이다.

 

그럼 논리적 I/O를 어떻게 줄일 수 있을까?

SQL을 튜닝해서 읽는 총 블록 개수를 줄이면 된다.

논리적 I/O는 항상 일정하게 발생하지만, SQL 튜닝을 통해 줄일 수 있는 통제 가능한 내생 변수다.

논리적 I/O를 줄임으로써 물리적 I/O를 줄이는 것이 곧 SQL 튜닝이다.

 

정리하자면 BCHR 공식을 이루는 물리적 I/O는 외생변수다.

메모리를 증설해서 DB 버퍼캐시 크기를 늘리는 방법외에 이것을 직접 줄일 방법은 없다. 반면, 논리적 I/O는 통제 가능한 내생변수다.

SQL을 튜닝해서 논리적 I/O를 줄이면 물리적 I/O도 줄고, 그만큼 성능도 향상된다. 

 

BCHR도 함정이 존재하는데, BCHR이 SQL 성능을 좌우하지만, BCHR이 높다고 해서 효율적인 SQL을 의미하지는 않는다.

같은 블록을 비효율적으로 반복해서 읽으면 BCHR이 높아지기 때문이다.

Single Block I/O VS Multi Block I/O

한 번에 한 블록씩 요청해서 메모리에 적재하는 방식을 Single Block I/O라고 한다.

한 번에 여러 블록씩 요청해서 메모리에 적재하는 방식을 Multi Block I/O라고 한다.

 

인덱스를 이용할 때는 기본적으로 인덱스와 테이블 블록 모두 Single Block I/O를 사용한다.

인덱스는 소량 데이터를 읽을 때 주로 사용하므로 이 방식이 효율적이다.

 

  • 인덱스 루트 블록을 읽을 때
  • 인덱스 루트 블록에서 얻은 주소 정보로 브랜치 블록을 읽을 때
  • 인덱스 브랜치 블록에서 얻은 주소 정보로 리프 블록을 읽을 때
  • 인덱스 리프 블록에서 얻은 주소 정보로 테이블 블록을 읽을 때

반대로 많은 데이터를 읽을 때는 Multiple Block I/O 방식이 효율적이다.

그래서 인덱스를 이용하지 않고 테이블 전체를 스캔할 때 이 방식을 사용한다. 

테이블이 클수록 Multi Block I/O 단위도 크면 좋다. 프로세스가 잠자는 횟수를 줄여주기 때문이다.

 

읽고자 하는 블록을 DB 버퍼캐시에서 찾지 못하면 해당 블록을 디스크에서 읽기 위해 I/O Call을 한다.

그동안 프로세스는 대기 큐에서 잠을 잔다.

대용량 테이블이면 수많은 블록을 디스크에서 읽는 동안 여러 차례 잠을 잘 텐데,

기왕에 잠을 자려면 한꺼번에 많은 양을 요청해야 잠자는 횟수를 줄이고 성능을 높일 수 있다.

대용량 테이블을 Full Scan 할 때 Multi Block I/O 단위를 크게 설정하면 성능이 좋아지는 이유다.

 

정리하면, Multi Block I/O는 캐시에서 찾지 못한 특정 블록을 읽으려고 I/O Call 할 때 디스크 상에 그 블록과 '인접한' 블록들을 한꺼번에 읽어 캐시에 미리 적재하는 기능이다. 

Table Full Scan VS Index Range Scan

테이블에 저장된 데이터를 읽는 방식은 두 가지다.

테이블 전체를 스캔해서 읽는 방식인 Table Full Scan과 인덱스를 용해서 읽는 방식인 Index Range Scan이 있다.

전자는 말 그대로 테이블에 속한 블록 전체를 읽어서 사용자가 원하는 데이터를 찾는 방식이다.

후자는 인덱스에서 '일정량'을 스캔하면서 얻은 ROWID(디스크 저장 주소)로 테이블 레코드를 찾아가는 방식이다.

 

인덱스를 사용하면 흔히 조회 성능이 높아진다고 생각하는데 아닌 경우도 많다.

특히 많은 데이터를 처리하는 집계용 SQL과 배치 프로그램이 그렇다.

이런 애플리케이션을 Table Full Scan로 유도하면 속도가 빨라진다고 한다.

 

인덱스를 이용해 많은 데이터를 읽을 때 왜 성능이 느린지 알아보자.

Table Full Scan은 시퀀셜 액세스와 Multi Block I/O 방식으로 디스크를 읽는다.

한 블록에 속한 모든 레코드를 한 번에 읽어 들이고,

캐시에서 못 찾으면 한 번의 I/O Call을 통해 인접한 수십 ~ 수백 개 블록을 한꺼번에 I/O하는 메커니즘이다.

이 방식을 사용하는 SQL은 스토리지 스캔 성능이 좋아지는 만큼 성능도 좋아진다.

 

시퀀셜 액세스와 Multi Block I/O가 아무리 좋아도 수십 ~ 수백 건의 소량 데이터 찾을 때 얘기다.

수백만 ~ 수천만 건 데이터를 스캔하는 건 비효율적이다. 큰 테이블에서 소량 데이터를 검색할 때는 반드시 인덱스를 사용해야 한다.

Index Range Scan을 통한 테이블 액세스는 랜덤 액세스와 Single Block I/O 방식으로 디스크 블록을 읽는다.

캐시에서 블록을 못 찾으면, '레코드 하나를 읽기 위해 매번 잠을 자는 I/O 메커니즘'이다.

따라서 많은 데이터를 읽을 때는 Table Full Scan보다 불리하다.

또한 이 방식은 읽었던 블록을 반복해서 읽는 비효율이 임ㅆ다.

많은 데이터를 읽을 때 물리적인 블록 I/O 뿐만 아니라 논리적인 블록 I/O 측면에서도 불리하다.

 

인덱스는 큰 테이블에서 아주 적은 일부 데이터를 빨리 찾기 위한 도구일 뿐이므로 모든 성능 문제를 인덱스로 해결하면 안된다.

읽을 데이터가 일정량을 넘으면 인덱스보다 Table Full Scan이 유리하다.

테이블 탐색 메커니즘

Direct Path I/O를 제외한 모든 블록 I/O는 메모리 버퍼캐시를 경유한다.

구체적으로, 아래 오퍼레이션은 모두 버퍼캐시 탐색 과정을 거친다.

 

  • 인덱스 루트 블록을 읽을 때
  • 인덱스 루트 블록에서 얻은 주소 정보로 브랜치 블록을 읽을 때
  • 인덱스 브랜치 블록에서 얻은 주소 정보로 리프 블록을 읽을 때
  • 인덱스 리프 블록에서 얻은 주소 정보로 테이블 블록을 읽을 때
  • 테이블 블록을 Full Scan 할 때

버퍼 캐시에서 블록을 찾을 때 해시 알고리즘으로 버퍼 헤더를 찾고, 거기서 얻은 포인터로 버퍼 블록을 액세스 하는 방식을 사용한다.

 

  • 같은 입력 값은 항상 동일한 해시 체인에 연결됨.
  • 다른 입력 값이 동일한 해시 체인에 연결될 수 있음
  • 해시 체인 내에서는 정렬이 보장되지 않음.

버퍼캐시는 SGA의 구성요소이므로 공유 자원이다.

따라서 두 개 이상 프로세스가 동시에 접근하려고 할 때 문제가 발생한다. 동시에 접근하면 블록 정합성에 문제가 생길 수 있기 때문이다.

따라서 자원을 공유하는 것처럼 보여도 내부에선 한 프로세스씩 순차적으로 접근하도록 구현해야 하며,

이를 위한 직렬화 메커니즘이 필요하다. 쉽게 말해 '줄 세우기'다. 이런 줄서기가 가능하도록 지원하는 메커니즘이 래치다.

SGA를 구성하는 서브 캐시마다 별도의 래치가 존재한다.

버퍼 캐시에는 캐시버퍼 체인 래치, 캐시 버퍼 LRU 체인 래치 등이 작동한다.

 

캐시 버퍼 체인뿐만 아니라 버퍼 블록 자체에도 직렬화 메커니즘이 존재한다. 바로 버퍼 Lock다.

이런 직렬화 메커니즘에 의한 캐시 경합을 줄이려면, SQL 튜닝을 통해 쿼리 일량(논리적 I/O) 자체를 줄여야 한다.

 

참고 자료

친절한 SQL 튜닝 1장

반응형

댓글