diff --git "a/jin/[DB] SQL\354\235\230 \354\262\230\353\246\254 \352\263\274\354\240\225\352\263\274 Optimizier, \353\215\260\354\235\264\355\204\260 \354\240\200\354\236\245 \353\260\217 IO.md" "b/jin/[DB] SQL\354\235\230 \354\262\230\353\246\254 \352\263\274\354\240\225\352\263\274 Optimizier, \353\215\260\354\235\264\355\204\260 \354\240\200\354\236\245 \353\260\217 IO.md" new file mode 100644 index 0000000..3d45668 --- /dev/null +++ "b/jin/[DB] SQL\354\235\230 \354\262\230\353\246\254 \352\263\274\354\240\225\352\263\274 Optimizier, \353\215\260\354\235\264\355\204\260 \354\240\200\354\236\245 \353\260\217 IO.md" @@ -0,0 +1,332 @@ +# 1. SQL의 처리 과정과 I/O +# 1.1 SQL 파싱과 최적화 + +DB 옵티마이저는 왜 필요한걸까? 그리고 안에서 어떤 일들이 일어날까?
+ +## 1.1.1 Structured Query Language +SQL이란 Structured Query Language의 줄임말이다. "구조적" 질의 언어라는 뜻을 가지고 있는데, SQL문을 생각해보면 아래와 같이 절차적이 아닌 구조적으로 작성 되어 있다.
+ +```sql +SELECT G.TITLE, G.DESCRIPTION, L.NAME +FROM GOAL G, LIFEMAP L +WHERE G.LIFEMAP_ID = L.ID +ORDER BY G.CREATED_AT DESC +``` + +SQL은 기본적으로 구조적이지만, 결과물의 집합을 만들 때는 절차적일 수 밖에 없다. DB의 어딘가에서 SQL을 읽어낸 다음 결과물의 집합을 만들기 위해 여러가지 행위를 수행할 것이다.
+어떤 테이블을 가져온 다음, 어떤 인덱스를 사용하고, 어떻게 스캔하라 등 절차적인 명령을 내릴 것이다. 즉, 일종의 프로시저가 필요한 셈인데, 이런 프로시저를 만드는 DBMS의 엔진이 SQL 옵티마이저이다. **우리가 쿼리를 작성하면 옵티마이저가 대신해 데이터를 가져오기 위한 코드를 짜 준다고 생각하면 된다.** 그리고 DBMS 내부에서 프로시저를 작성해 실행 가능한 상태로 컴파일 하는 이 과정을 **"SQL 최적화"** 라고 부르는 것이다.
+ + +## 1.1.2 SQL 최적화 +DBMS는 SQL을 실행되기 전 위해 SQL 파싱과 최적화 과정를 시행한 다음 로우 소스를 만들어낸다. + +1. `SQL 파싱` : 클라이언트로부터 SQL을 전달 받은 다음, SQL 파서가 파싱을 시작한다.
+ 1. 파싱 트리 생성 : 우리가 사용하는 PL들에서도 똑같이 일어나는 과정으로, SQL문을 이루는 요소들을 분석해 파싱 트리를 생성한다. + 2. Syntax 체크 : 트리를 만들었으니, 문법적 오류를 검사한다. 사용할 수 없는 키워드를 사용하거나 순서가 잘못됐다면 이때 검출된다. + 3. Semantic 체크 : 의미상 오류가 없는지 확인한다. 문법 자체는 옳은데, 존재하지도 않는 테이블이나 컬럼에 쿼리를 날릴 수도 있고, 권한이 없을 수도 있다. 이 모든 의미적 오류를 체크한다. +2. `SQL Optimizier` : Optimizier가 수행. Optimizier는 미리 수집해둔 시스템 및 통계 정보들을 활용해서 다양한 실행경로를 생성한다. 그리고 그 중 가장 효율적인 실행 경로를 선택한다. 마치 네비게이션이 길을 찾기 위해 여러 경로를 탐색하고 Best 경로를 찾는 과정과 동일하다. Optimizier가 수행하는 이 최적화 과정이 DB 성능을 결정하는 가장 핵심적인 부분이다. +3. `로우 소스 생성` : Row-Source Generator가 수행한다. SQL Optimizier가 선택한 실행경로를 실제 실행 가는한 코드나 프로시저로 포맷팅해 Raw Source를 생성한다. + +## 1.1.3 SQL Optimizier와 실행계획 +SQL Optimizier는 DBMS의 핵심 엔진으로, **사용자가 원하는 작업을 가장 효율적으로 수행할 수 있는 최적의 데이터 Access 경로를 선택한다.** 정말 자동차 네비게이션과 그 역할이 비슷하다.

+ +차를 빌려 멀리 여행을 간다고 생각해보자. 우리는 네비게이션에 목적지를 입력한다. 그러면 네비게이션은 다양한 "비용"을 계산해 여러 경로를 만들어 준다. 보통 운전자에게 이 비용은 "시간"이다. 네비게이션은 여러 경로를 확인한 다음 가장 "빨리" 가려면 어떤 경로를 이용해야 하는지 알려준다. 우리는 여러 경로들 중 원하는 경로를 선택할 수 있다.

+ +SQL Optimizer도 똑같다. 사용자가 어떤 작업을 수행하겠다고 말하면 Optimizier는 "데이터 딕셔너리"라는 곳에 미리 수집해둔 시스템 및 통계 정보들을 활용해서 다양한 실행경로를 생성한다. 그리고 각 경로의 "비용"을 계산해 최소 비용을 갖는 경로를 찾아내는데 **DBMS가 이때 "비용"으로 여기는 것은 I/O 횟수 혹은 실행 시간이다.**
+결국 Application 개발시에도 가장 병목이 되는 지점중 하나가 바로 I/O이다. Disk에서 데이터를 가져오건, 외부 서버에서 데이터를 가져오건, I/O는 요청을 받기까지 보통 오랜 시간이 소요되며, 그동안 스레드를 멈춰버리기 때문에 거의 대부분의 병목의 원인은 I/O이다. 그래서 DBMS가 비용을 계산할 때 I/O 횟수를 중요하게 여기는 것이다.

+ +그렇게 만들어진 경로들을 실행계획 - Execution Plan이라고 부른다. 네비게이션에서 처럼 "이런 쿼리는 이런 경로로 가겠습니다"라고 알려주는 역할을 한다. 각종 DBMS 콘솔을 사용해 봤다면 알겠지만, 실행계획은 쉽게 확인해볼 수 있다. 수행 예상 시간은 어떤지, I/O는 얼마나 일어날 것 같은지, Table Full Scan인지 인덱스를 활용하는지 등 쿼리 수행 계획을 알려준다.
+ +### 이 또한 예상 값일 뿐이다. +주의해야 할 점은 이 또한 예상값일 뿐이라는 것을 알아야 한는 것이다. 내비게이션과 똑같이 옵티마이저가 알려주는 예상 Cost는 정말 예상치일 뿐이다. 실제 결과는 다를 수 있다. + + +## 1.1.4 Optimizer 힌트 +우리가 여행을 할 때 어딘가를 경유해서 가고 싶을 수도 있다. 어떤 휴게소에 있는 호두과자가 꼭 먹고 싶다던지, 어떤 고속도로의 길가에 심어진 꽃 나무를 보고 싶을 수도 있다. 이럴 때 우리는 내비게이션에 경유지를 선택한다. 혹은 내비가 더 느린 길을 알려줄 때도 있다. 어떤 동내에서 오랜 시간 일해온 택배 기사님이나 택시 기사님들은 내비도 모르는 좋은 지름길을 알 수도 있다. 이런 경우 그 경로를 사용하라고 내비에게 알려줄 수 있다면 참 좋을 것이다.

+ +Optimizer에도 똑같이 꼭 거쳐야 할 곳들에 대한 지시를 내릴 수 있다. 이를 "옵티마이저 힌트"라고 부른다.
+옵티마이저도 완벽하지 않기 때문에, 어떤 인덱스를 꼭 사용할 것, 어떤 Join을 꼭 사용할 것 등을 지정해줄 수 있다. 업무 특성이나 데이터의 특성을 이해하고 있는 것은 사용자이므로, "어떻게 가라" 힌트를 주는 것이다.

+ +사용법은 주석 기호에 +를 붙인다. (다른 방법도 있지만, 실수하기 쉬워 권하지 않는다.)
+ +```sql +SELECT /*+ INDEX(G GOAL_PK) */ + G.TITLE, G.DESCRIPTION, L.NAME +FROM GOAL G, LIFEMAP L +WHERE G.LIFEMAP_ID = L.ID +ORDER BY G.CREATED_AT DESC +``` + +(참고로 오라클 문법이다.)
+ + +오라클 힌트는 그 종류가 아주 많다. 다 이해하려고 하기 보단 주의점과 자주 사용하는 힌트를 알아두자. 나도 "이런걸 할 수 있구나" 정도로 이해하는 것이 목표이다.
+ +## 1.1.5 옵티마이저 힌트 사용시 주의해야 할 점 +오라클 힌트를 사용할 때의 주의점을 알아보자. + +1. 힌트의 인자들 사이엔 콤마 - `,`가 있고, 힌트와 힌트 사이엔 없어야 한다. + ```sql + /*+ INDEX(G GOAL) INDEX(L, L_XXX) */ -> 문제 없음 + /*+ INDEX(G), INDEX(L) */ -> 첫 번째 힌트만 적용된다. + ``` +2. 테이블 지정시 스키마명까지 명시하면 안된다. 아래 힌트는 무효다. + ```sql + SELECT /*+ FULL(SCOTT.EMP) */ + FROM EMP + ``` +3. FROM 절 옆에 ALIAS를 지정한 경우, 힌트에도 ALIAS를 사용해야 한다. 아래 힌트는 무효다. + ```sql + SELECT /*+ FULL(EMP) */ + FROM EMP E + ``` +4. 옵티마이저는 신이 아니다 - 중요한 쿼리이고, 힌트를 줄 거면 빈틈없이 기술하자. + 옵티마이저도 실수할 수 있다. 아주 정확하게 최선의 경우를 늘 찾아내지 않기 때문에, 옵티마이저의 작은 실수가 큰 손실을 끼치는 경우 직접 **힌트를 주는 것도 좋다. 그리고 주려면 빈틈없이 주자.** 무슨 말이냐면, "서울에서 부산까지 갈건데 중간에 대전 성심당을 거쳐줘" 정도로 힌트를 준다면 아마도 서울-성심당, 그리고 성심당-부산의 루트를 내비가 자유롭게 짤 것이다. 만약 작은 실수라도 용납되지 않는 상황의 경우 "성심당-부산"의 루트에서 옵티마이저가 얼마든지 잘못 판단할 수 있다. 따라서 이런 경우 힌트를 아주 자세하게 줘야 한다. 어떤 고속도로를 타고, 어느 도로를 이용하고.. 정확하게 알려줄 필요가 있다. + + +## 1.1.6 자주 사용하는 힌트들 +자주 사용하는 쿼리 힌트들을 간략하게 알아보자.
+ +### 1. 최적화 +1. `/*+ ALL_LOWS */` : 전체 처리속도 최적화 +2. `/*+ FIRST_ROWS(N) */` : 최초 N건 응답속도 최적화 + +### 2. 엑세스 방식 +1. `/*+ FULL *` / :인덱스를 타지 말고 테이블 풀스캔 수행 +2. `/*+ INDEX */` : 인덱스를 사용할 것 +3. `/*+ INDEX_DESC */` : 인덱스를 ORDER BY DESC 역순으로 실행할 것 +4. `/*+ INDEX_FFS */` : INDEX FAST FULL SCAN +5. `/*+ INDEX_SS */` : INDEX SKIP SCAN + +### 3. 조인 순서 힌트 +1. `/*+ ORDERED */` : FROM절에 나열된 테이블 순서대로 조인 +2. `/*+ LEADING */` : 내가 힌트절에 열거한 테이블 순서대로 조인 + - 예시 : `/*+ LEADING (A B C) */` -> A, B, C 순서대로 조인 +3. `/*+ SWAP_JOIN_INPUTS */` : 해시조인의 경우, BUILD INPUT를 명시적으로 선택 + - 예시 `/*+ SWAP_JOIN_INPUTS(A) */` -> 해시 조인의 경우 BUILD INPUT과 PROBE에 대한 순서 선택이 가능하다. + +### 4. 조인 방식 힌트 +1. `/*+ USE_NL *` / :NL JOIN (NESTED LOOP - 중첩루프) +2. `/*+ USE_MERGE */` : SORT MERGE JOIN +3. `/*+ USE_HASH */` : HASH JOIN +4. `/*+ NL_SJ */` : NL SEMI JOIN +5. `/*+ MERGE_SJ */` : SORT MERGE SEMI JOIN +6. `/*+ HASH_SJ */` : HASH SEMI JOIN + +### 5. 서브 쿼리 팩토링 +1. `/*+ MATERIALIZE */` : WITH 문으로 정의한 집합을 물리적으로 생성하 + - 예시 : WITH /*+ MATERIALIZE*/ T AS (SELECT ...) +2. `/*+ INLINE */` : WITH 문으로 정의한 집합을 물리적으로 생성 X, INLINE 처리 + - 예시 : WITH /*+ INLINE*/ T AS (SELECT ...) + +### 6.쿼리 변환 +1. `/*+ MEERGE */` : 뷰 머징 유도 +2. `/*+ NO_MERGE */` : 뷰 머징 방지 +3. `/*+ UNNEST */` : 서브쿼리 UNNESTING 유도 +4. `/*+ NO_UNNEST */` : 서브 쿼리 UNNESTING 방지 +5. `/*+ PUSH_PRED */` : 조인 조건 PUSHDOWN 유도 +6. `/*+ NO_PUSH_PRED */` : 조인 조건 PUSHDOWN 방지 +7. `/*+ USE_CONCAT */` : OR 또는 IN-LIST조건을 OR-EXPANSION으로 유도 +8. `/*+ NO_EXPAND */` : OR 또는 IN-LIST 조건에 대한 OR-EXPANSION방지 + +### 7.병렬처리 +1. `/*+ PARALLEL */` : 테이블 스캔, DML 병렬방식으로 처리하도록 할때 사용.. 단일 대형 테이블의 접근시 정말 많이 쓴다. + - 예시 : /*+ PARALLEL(T1 4)*/ +2. `/*+ PARALLEL_INDEX */` : 인덱스 스캔을 병렬방식으로 처리하도록 유도 +3. `/*+ PQ_DISTRIBUTE */` : 병렬수행시 데이터 분배방식 결정 + - 예시 : `PQ_DISTRIBUTE(T1 HASH(--BUILD INPUT) HASH(--PROBE TABLE))` + +### 8. 그외 기타 +1. `/*+ APPEND*/` : DIRECT PATH INSERT유도로 INSERT 문에 주로 많이 쓴다 +2. 1. `/*+ DRIVING_SITE */` : DB LINK REMOTE쿼리에 대한 최적화 및 실행 주체 지정 (LOCAL 또는 REMOTE) +3. `/*+ PUSH_SUBQ */` : 서브쿼리를 가급적 빨리 필터링하도록 유도 +4. `/*+ NO_PUSH_SUBQ */` : 서브쿼리를 가급적 늦게 필터링 하도록 유도 + + +# 1.2 SQL 공유와 재사용 + +이제까지 SQL 파싱과 최적화에 대해 알아봤다. 어떤 루트가 가장 효율적인지 알고 선택하는 방식의 최적화를 알아보았고, 이번엔 온 개발 영역에서 흔히 쓰이는 "캐싱" 방식에 대해 알아보자. SQL은 어떻게 캐싱될 수 있고, 우린 어떻게 활용할 수 있을까? + + +## 1.2.1 Hard Parsing vs Soft Parsing +![image](https://github.com/depromeet/amazing3-be/assets/71186266/71de1cdc-b49b-47ff-af3c-ea75f2d0a1d6) + +
+ +서버 프로세스와 백그라운드 프로세스는 SGA(System Global Area)라는 공통의 공간에 데이터와 제어 구조를 캐싱한다. 위 그림에서 빨간색 네모로 표시 되어 있다.
+그리고 앞서 설명한 3가지 최적화 과정(SQL 파싱, 최적화, 로우 소스 생성)을 거치며 생성된 **내부 프로시저를 캐싱해 두는 메모리 공간을 Library Cache라고 부른다.** 위 그림의 Shered Pool 안에 들어있다.
+ +**DBMS는 사용자의 SQL을 최적화 하기 전에 우선 해당 SQL이 Library Cache에 존재하는지 먼저 확인한다.** **만약 캐시 공간에 SQL이 존재한다면 바로 실행 단계로 넘어가게 되는데, 이를 Soft Parsing이라고 한다.**
+캐시가 없다면 최적화 과정을 거치는데 이 과정을 Hard Parsing이라고 한다. + + +### 1.2.2 "Hard" 파싱의 어려움.. +이 최적화 과정은 왜 "Hard"라고 불릴까? Soft에 비해 어려우면 얼마나 어렵다고 Hard라고 부르는걸까?
+ +1. **경우의 수가 너무 많다.** +2. **SQL은 텍스트 그 자체가 식별자 처럼 쓰인다.** 쿼리는 한 톨이라도 다르면 서로 다른 쿼리로 여겨진다. + +#### 1. 수많은 경우의 수 +단순하게 생각해보자. 어떤 쿼리문이 있고, 테이블을 3개 조인하고 있다고 생각해보자. 조인 순서를 바꿔가며 테스트 해본다면 가능성이 몇 가지인가? 3! (3 팩토리얼) 갯수이다. 4개 일때는 4! = 24, 5개 일때는 5! = 120 가지의 가능성이 있다. 또한 조인 방식도 여러개이고, 테이블 스캔과 인덱스 스캔 방식도 여러개이다.
+SQL 옵티마이저가 고려하는 정보는 정말 많다. 대충 +1. 테이블, 컬럼, 인덱스 구조와 메타 데이터 +2. 각종 오브젝트의 통계 자료 +3. 시스템 통계 자료 +4. 옵티마이저 파라미터 등.. + +이렇게 많은 경우의 수를 모두 따지다 보니, 결코 가벼운 연산이라 부를 수 없다. 이런 상황을 보면 꼭 꼭 캐싱하고 싶지 않나? 이래서 캐싱을 하는 것이다. + +#### 2. SQL은 이름이 없다! :star: :star: +프로시저나 트리거와 달리 SQL은 이름이 없다!
+SQL 텍스트 전체가 통째로 이름과 같은 역할을 하기 떄문에, 토씨 하나, 대문자 소문자, 키워드 순서가 다르면 아예 다른 SQL로 치부된다. WHERE문을 WHERE로 쓰냐 where로 쓰냐 조차 다른 경우로 본다. 중간에 주석이 달린 경우도 다른 경우이다. 당연히 조건값이 다른 경우도 다른 경우이다. 예를 들어 id가 3인 유저를 찾는 쿼리를 보자. (WHERE id = 3) 그리고 나머지 부분은 전부 똑같은데 숫자 3만 4로 바뀌어도 다른 쿼리이다. SQL ID라는 개념을 통해 SQL에 식별자를 부여해도 이는 똑같다.
+ +## 1.2.2 바인드 변수의 중요성 +이러한 하드 파싱의 두번째 어려움 떄문에 바인드 변수가 매우 중요하다.
+만약 아래와 같이 여러 쿼리가 발생했다고 생각해보자. + +```sql +SELECT * FROM USER WHERE ID = 1 +SELECT * FROM USER WHERE ID = 2 +SELECT * FROM USER WHERE ID = 3 +SELECT * FROM USER WHERE ID = 4 +SELECT * FROM USER WHERE ID = 5 +``` + +만약 앱단에서 String을 조작해 위와 같이 쿼리를 날렸다면, 5번 전부 다른 퀄리로 인식 되어 5번의 하드 파싱이 발생할 것이다. **이때, ID 값만 비워두고 바인드 변수를 활용하면 캐싱을 활용할 수 있다.** 이 둘의 부하 차이는 당연히 어마어마하다. 서비스 특성상 요청이 몰리는 순간에 만약 바인드 변수를 제대로 활용하지 않았더라면, 디비는 부하를 견디지 못하고 터져버릴지도 모른다.
+하드 파싱과 소프트 파싱을 이해하고, 각각 언제 발생하는지 생각해보자. 그리고 바인드 변수를 잘 활용해 하드 파싱을 최대한 피해보자. + + +# 1.3 데이터 저장 구조 및 I/O 메커니즘 +I/O 튜닝 과정을 이해하기 전에 우선 I/O가 왜 오래 걸리는지를 이해해보자. DB는 어떻게 데이터를 저장하고, 메모리나 디스크에서 어떻게 데이터를 읽어오는가? 이 과정은 왜 오래 걸리는가?

+ + +Applciation에서 성능을 개선할 때와 비슷하게, 사실상 I/O 튜닝 작업이 SQL 튜닝에서 큰 비중을 차지한다. 결국 외부와의 I/O, Disk를 다녀오는 I/O 등은 느리다. 바빠 죽겠는데 스레드를 쿨쿨 재워 버린다.
+스레드는 디스크에서 데이터를 읽어야 할 때 잠시 CPU를 OS에 반환하고 Wating 상태에 들어가게 된다.
+이러한 I/O Call은 Single Block I/O 기준으로 평균 10ms 정도 소요된다. 초당 100 블록 쯤 읽는다. 최근의 스토리지는 이보다 수십배 정도 더 빠르게 읽어낼 수도 있지만, 여전히 드리긴 느리다.
+ +Application에서도 대부분의 병목은 외부 서비스와의 I/O나 디비와의 I/O였는데, DB도 똑같이 Disk와의 I/O가 가장 방해요소이다.
+ +## 1.3.2 DB 데이터 저장 구조 +데이터를 저장하기 위해선 일단 Table Space가 생성 되어야 한다. Table Space는 이름 그대로 Table Segment를 담는 공간이다. 여러 디스크 상의 물리적인 OS 데이터 파일로 구성된다. 아래의 그림과 같다.
+ + +Extents와 Block에 대해 몇가지 꼭 구분해서 알아둘 게 있다. +1. Extents는 연속된 Block들의 집합이며, Segment는 여러개의 Extents로 구성된. +2. 이 Extents는 공간을 확장하는 범위로, 익스텐트 공간이 부족해지면 해당 오브젝트가 속한 Table Space에게 추가로 익스텐트를 할당 받는다. +3. 단, 레코드가 실제로 저장되는 공간 자체는 익스텐트가 아니라 Data Block이다. 헷갈리지 말자. +4. Block들은 연속된 공간에 할당되어 있다. 하지만 Extents 끼리는 연속된 공간에 할당되어 있지 않다. +5. 우리가 OS시간에 배운 "Page"이 여기서 말하는 Block과 같은 개념이라고 보면 된다. +6. 한 블블록은 하나의 테이블이 독점한다. 한 블록에 저장된 레코드는 모두 같은 테이블 레코드이다. +7. 한 익스텐트도 하나의 테이블이 독점한다. 즉, 한 익스텐트에 담긴 블록들은 모두 같은 테이블 블록이다. + +### 정리 + +- Block : 데이터를 읽고 쓰는 단위 +- Extents : 공간을 확장하는 단위, 연속된 블록 집합 +- Segment : 데이터 저장공간이 필요한 오브젝트들 - 테이블, 인덱스, 파티션, LOB 등..
자신에게 할당된 Extents의 Map을 가지고 있음 +- Table Space : 세그먼트를 담는 컨테이너이다. +- Data File : 디스크상의 물리적인 OS File + + + + +## 1.3.3 Block 단위 I/O +DBMS는 Block 단위로 데이터를 읽고 쓴다. OS의 Page와 비슷한데, 큰 데이터를 전부 메모리에 올릴 수 없으니 작은 Block 단위로 데이터를 읽고 쓰는 것이다.
+ +따라서 딱 한줄의 Row를 읽고 싶더라도 블록 단위로 읽을 수 밖에 없다. 오라클은 보통 8KB 크기의 블록을 사용하기 때문에 단 1 Byte만 읽더라도 8Byte를 읽게 된다. +테이블 뿐만 아니라 인덱스도 똑같다. + +## 1.3.4 시퀀셜 Access +테이블 또는 인덱스 블록을 엑세스 하는 방식으로는 시퀀셜 엑세스와 랜덤 엑세스 두 가지가 있다. 시퀀셜 엑세스 방식만 설명하겠다.
+논리적 or 물리적으로 연결된 순서에 따라 차례대로 읽는 방식이다. 오라클 인덱스 자료구조는 B+Tree로 리프 블록이 앞뒤로 서로를 가리키고 있는데, 앞 또는 뒤 방향으로 순차적으로 스캔하는 행위를 시퀀셜 엑세스라고 한다.
+그런데, 테이블 블록들은 서로 논리적 연결고리를 갖고 있지 않은데 어떻게 시퀀셜 방식으로 엑세스할까? 오라클은 세그먼트에 할당된 익스텐트의 목록을 세그먼트 헤더에 Map으로 관리한다. **즉, 각 세그먼트는 자신에게 할당된 익스텐트 목록을 Map의 형태로 가지고 있다.** 익스텐스 Map은 각 익스텐트의 첫번째 블록 주소 값을 갖는데, 이걸 이용해서 전부 스캔하면 그것이 Full Table Scan이다. + + +## 1.3.5 논리적 물리적 I/O +### DB 버퍼 캐시 +1. DB 버퍼캐시: 데이터 캐시. 디스크에서 읽은 Data Block을 캐싱한다. 같은 블록을 호출하는 경우 I/O를 줄일 수 있다. +2. 라이브러리 캐시 : SQL과 실행계획, DB 저장형 함수나 프로시저를 저장하는 "코드 캐시" + +
+ +DB 버퍼 캐시도 라이브러리 캐시와 마찬가지로 SGA의 가장 중요한 구성요소 중 하나이다. **디스크에서 읽은 Data Block을 캐싱하므로, 데이터 블록을 읽기 전에 버퍼 캐시부터 탐색한다. 버퍼 캐시는 공유 메모리 영역에 있어 같은 블록을 읽는 다른 프로세스들도 이득을 보게 된다.**
+ +### 논리적 I/O란 무엇인가 +- 물리적 블록 I/O : 디스크에서 발생한 총 블록 I/O를 말한다 +- 논리적 블록 I/O : SQL 처리 과정에서 발생한 총 블록 I/O를 말한다. + +
+ +논리적 블록 I/O는 우리가 아는 다른 "논리적"과 조금 다른 개념으로 물리적 블록 I/O가 포함된 개념이다.
+정말 처리과정에서 발생한 모든 블록 I/O를 말하기 때문이다. 사실상 웬만한 요청은 메모리에 있는 버퍼 캐시를 경유하기 때문에, 메모리 I/O와 사실상 같은 개념이라고 말한다.
+ +물리적 I/O는 그 요청들 중 DB 버퍼 캐시 Miss로 인해 디스크까지 다녀오는 경우를 카운트한 것이다.
+책에서는 자전거를 타서 어딘가로 갈 때 바퀴가 회전해야 하는 횟수를 (회전당 이동 거리가 정해져 있을 때) 논리적 I/O로, 매번 바람이나 도로 상황에 따라 달라질 수 있는 페달 밟는 횟수를 물리적 I/O로 비유했는데 비유를 함으로써 더 어려워 진거 같다.
+예외적으로 Direct Path Read와 같은 방식으로 읽는 경우엔 메모리를 거치지 않아 이때는 논리적 I/O 횟수와 메모리 I/O 횟수가 일치하지 않을 수도 있다. + + +### 버퍼 캐시 히트율 +버퍼 캐시의 효율을 측정하는 좋은 방법으로, 온라인 트랜잭션을 처리하는 어플리케이션이라면 평균 99% 히트를 달성해야 한다. 핵심 트랜잭션을 기준으로 튜닝하면 어려운 수치는 아니라고 할 수 있다. 공식은 아래와 같다. + + +공식을 통해 튜닝 힌트를 얻을 수 있다. 물리적 I/O가 성능을 결정하지만, **실제로 성능 향상은 논리적 I/O를 줄여야 한다.** 위 공식을 변형하면 더 잘보인다. +- 물리적 I/O = 논리적 I/O x (100% - BCHR) + +논리적 I/O는 쿼리 조건절에 넣는 변수가 어떻게 바뀌어도 웬만하면 항상 일정하기 떄문에 **물리적 I/O는 히트율에 따라 결정된다. 물리적 I/O는 결국 시스템 상황에 의해 결정되는 통제 불가능한 변수로 보는 것이 맞고, 그래서 논리적 I/O를 줄이는게 낫다는 것이다.**
+그 효과도 크다. 예를 들어 BCHR이 평균 70%이고, 어떤 논리적 I/O가 10,000라고 하면 위 공식에 의해 물리적 I/O는 대략 3,000이다. 이때 논리적 I/O를 10분의 1 수준으로 줄이면 물리적 I/O도 300까지 줄어들고 성능이 10배 향상된다. + +### 그래서 논리적 I/O는 어떻게 줄일 수 있는가? +결국 읽어내는 총 블록 갯수를 줄이면 된다. **결국 논리적 I/O를 줄여 물리적 I/O를 줄이는 것이 SQL 튜닝이다.** 통제 가능한 변수를 통제한다. + +## 1.3.6 많은 데이터를 읽을 때, Index Range Scan 보다 Table Full Scan이 빠른 이유 +예전에 MySQL을 공부하며 인덱스를 간단하게만 배울 때는 그냥 테이블의 50% 이상의 데이터를 가져올 때는 알아서 인덱스 레인지 스캔 대신 Table Full Sacn을 수행한다고 배웠다. 옵티마이저는 똑똑하다. 어련히 이 방법이 더 효율적이므로, 이렇게 가져왔을 것이다.
+그러나 저자의 경험에 따르면 개발자들이 옵티마이저에 Table Full Scan이라고 써 있기만 하면, 항상 튜닝을 요청한다고 했다.
+그 회사에서 사용한 툴이 Table Full Scan시 빨간색으로 표기해 마치 문제가 되는 것 처럼 경고한다는 이유도 있었지만, 기본적으로 많은 개발자들이 Table Full Scan이면 무조건 좋지 않은 것으로 인식한다고 했다.
+과연 실제로 그런지? 어떤 때 Index Range Scan이 나은지, Full Table Scan이 나은 상황은 언제인지 확인해보자. + + +### Signle-Blocking I/O와 Multi-Blocking I/O +일단 Signle-Blocking I/O와 Multi-Blocking I/O의 차이를 이해할 수 있어야 한다.
+메모리 캐시에 원하는 데이터 블록 없다면, 결국 디스크에서 불러올 수 밖에 없다. 이때, 연산에 따라 DB는 필요한 딱 하나의 블록을 가져오거나 (Single Block), 혹은 쭉 스캔하여 여러 블록의 데이터를 가져온다. (Multiblock)
+마치 인부가 벽돌을 나를 때, 하나만 나를지 여러개를 나를지의 차이이다. 많은 블럭을 가져와야 하는 경우 어떤 때는 가져올 블럭을 식별하는 시간 (Scan)이 더 걸리더라도 한번에 가져오는 것이 나을 수도 있다.

+ +어떤 경우에 Single Block I/O가 발생할까? 주로 소량의 데이터를 가져올 때이다. +1. `인덱스 루트 블록`을 읽을 때 +2. `인덱스 루트 블록`에서 얻은 주소 정보로 -> `브랜치 블록`을 읽을 때 +3. `인덱스 브랜치 블록`에서 얻은 주소 정보로 -> `리프 블록`을 읽을 때 +4. `인덱스 리프 블록`에서 얻은 주소 정보로 -> `테이블 블록`을 읽을 때 + +
+ +결국 한번의 인덱스를 통한 Range Scan 이용하는 과정에서 여러번의 Single Block I/O가 발생하게 된다.
많은 데이터 블록을 읽을 때는 Multiblock 방식이 유리한데, Full Scan의 경우 당연히 이쪽이 유리하다. 이때 Multiblock I/O 단위를 크게 설정하면 한번에 데이터를 많이 가져올 수 있어 성능이 좋아진다.

+ +이제 왜 인덱스를 통해 데이터를 가져오는 것이 항상 빠르다고 할 수 없는지 알 수 있다. 어떤 레코드를 500개 가져오는 상황을 생각해보자. 그리고 하나의 블록엔 레코드가 500개 있다고 생각해보자.
+ +**인덱스를 사용하면 총 500번의 중복 작업을 하게 된다!** 하나 하나 데이터는 빠르게 찾을 수 있겠지만, 무려 500번의 I/O가 발생하게 된다.
+반대로 Full Scan을 하게 되면, 데이터 자체를 찾는데엔 오랜 시간이 걸리고, 스캔하는데 시간이 걸리겠지만 단 한번만의 I/O만에 데이터를 가져올 수 있다!
+DBMS의 주요 병목은 I/O에 의해 발생하는 것이므로, 인덱스를 사용하는 경우가 더 느릴 수도 있다! **Table Full Scan이 Index Range Scan 보다 유리한 경우는 분명 있다는 것이다!**
+따라서, Full Scan이면 웬만하면 인덱스를 사용하는 방향으로 튜닝하겠다는 생각은 버려야 한다. + + +## 1.3.8 캐시 탐색 매커니즘 + +Direct Path I/O 외의 모든 블록 I/O는 메모리 버퍼 캐시를 경유하게 된다. +1. 인덱스를 사용하는 동안 +2. 테이블 블록을 Full Scan 하는 동안 +모두 메모리 버퍼 캐시를 경유한다. + +
+ +버퍼 캐시는 HashMap이 버퍼 블록 래퍼런스를 가리키는 형태로 구현되어 있다.
+버킷에 각 버킷별로 링크드 리스트 형태와 같은 해시 체인을 가지고 있다. 보통의 해시 탐색과 같은 순서로 탐색이 진행된다. + +1. Data Block Adress를 해시 함수에 넣어 버킷을 식별한다. +2. 해당 해시 체인을 순회하면서 올바른 노드를 찾으면, 그곳에 "버퍼 헤더"가 있다. +3. 이 버퍼 헤더에서 포인터를 얻을 수 있고, 이 포인터는 데이터가 캐싱된 버퍼 블록을 가리키고 있다. + +### 버퍼 캐시 접근 직렬화 +버퍼 캐시 또한 SGA의 구성요소로 메모리에 올라간 공유 자원에 해당한다. 따라서 동시에 접근하면 정합성에 문제가 생길 수 있다. **따라서 요청들을 직렬화하는데, 이때 Latch를 활용한다.** 이를 "Hash Chain Latch"라고 부른다.
+ +**이 랫치가 잠그는 범위는 하나의 Hash Chain이다.** 즉, 체인마다 Latch가 있다. 랫치를 통해 한번에 하나의 프로세스만 체인에 진입할 수 있도록 돕는다. 체인에서 노드를 찾으면, 즉시 랫치는 해제되고, 다른 프로세스가 진입할 수 있다.
+ +이때, 노드를 찾자마자 반환되는 거지, 버퍼 블록을 방문했을 때 반환되는 것이 아니므로, 다른 프로세스가 같은 버퍼 블록에 접근할 수도 있다. **그래서 버퍼 블록 자체에도 "Buffer Lock"이 존재한다.** **Chain Latch를 해제하기 전에 먼저 Buffer Lock을 걸어 직렬화 문제를 해결한다.**