├── 1장_SQL_처리_과정과_IO ├── 1-2_seyun.md ├── 1.1_sql_파싱과_최적화_유효정.pdf ├── 1.3_최락준.md ├── 1장_SQL_처리_과정과_I:O_황준호.md ├── 1주_김민석.md ├── images │ └── 1-3-1.png └── 조수진.md ├── 2장_인덱스_기본 ├── 2.2_인덱스_기본_사용법_김광훈.md ├── 2.3_인덱스_확장기능_사용법_황준호.md ├── 2주_김민석.md └── 조수진.md ├── 3장_인덱스_튜닝 ├── 3.1_테이블_엑세스_최소화_김광훈.md ├── 3.3.1~3.3.4_인덱스_스캔_효율화_유효정.pdf ├── 3.3.5~3.4.내용_요약_김광훈.md ├── 3.3.5~3.4_황준호.md ├── 3.3_최락준.md ├── 3장_인덱스_튜닝.md ├── 3주_김민석.md ├── 4주_김민석.md ├── images │ ├── 3-43.jpg │ └── 3-44.jpg └── 조수진.md ├── 4장_조인_튜닝 ├── .empty ├── 4.1_NL_조인_김광훈.md └── 6주_김민석.md ├── 5장_소트_튜닝 ├── .empty ├── 5.1~5.1_내용_요약_김광훈.md ├── 5.1~5.4_황준호.md ├── 5.3_인덱스를_이용한_소트연산_생략_유효정.pdf └── 5장_김민석.md ├── 6장_DML_튜닝 ├── 6.1_기본DML튜닝_김광훈.md ├── 6.3_최락준.md ├── 6.4_Lock과_트랜잭션_동시성_제어_황준호.md ├── 6장-1_김민석.md ├── images │ ├── partition1.jpeg │ ├── partition2.jpeg │ └── partition3.jpeg └── 조수진.md ├── 7장_SQL_옵티마이저 ├── .empty └── 7.1_김민석.md ├── README.md └── img ├── pic3-3.png ├── pic3-47.png ├── pic_1-12.png ├── pic_1-27.png ├── pic_1-4.png └── table3-8.png /1장_SQL_처리_과정과_IO/1-2_seyun.md: -------------------------------------------------------------------------------- 1 | ## 1.2. SQL 공유 및 재사용 2 | 3 | ### 소프트 파싱 vs 하드 파싱 4 | 5 | - 프로시저 : 6 | - 라이브러리 캐시 (Library Cache) : SQL 파싱, 최적화, 로우 소스 생성 과정을 거쳐 생성한 내부 프로시저를 반복 재사용할 수 있도록 캐싱해 두는 메모리 공간 7 | - SGA(System Global Area) : 서버 프로세스와 백그라운드 프로세스가 공통으로 액세스하는 데이터와 제어 구조를 캐싱하는 메모리 공간 8 | 9 | ![image](https://user-images.githubusercontent.com/53366407/140854035-742dfedd-29cb-4f1b-80df-83ad2c6b4a5c.png) 10 | 11 | - 소프트 파싱(Soft Parsing) : SQL을 캐시에서 찾아 곧바로 실행단계로 넘어가는 것 12 | - 하드 파싱(Hard Parsing): 찾는데 실패 해 최적화 및 로우 소스 생성 단계까지 모두 거치는 것 13 | 14 | ![image](https://user-images.githubusercontent.com/53366407/140854048-0df941c1-6d87-420d-9838-25b8c648f3cc.png) 15 | 16 | --- 17 | 18 | - PostreSQL은 어떤가? 19 | - PostreSQL에서는 Library Cache라는 개념이 존재 하지 않는다. 20 | - 대신 Buffer가 존재하는데 아래와 같다. 21 | - Shared Buffer : 사용자가 요청한 데이터 블록을 저장하는 공간 22 | - WAL(Write Ahead Log) Buffer : 데이터 변경 사항을 저장하는 버퍼 23 | - 여기서 Shared Buffer에 존재하면 소프트 파싱이 되고, 아니면 하드 파싱이 된다. 24 | 25 | ![image](https://user-images.githubusercontent.com/53366407/140854077-d67ef985-78bd-4b8d-b7e4-67d0e9b252c2.png) 26 | 27 | ### SQL 최적화 과정은 왜 하드(Hard)한가 28 | 29 | - 옵티마이저가 SQL을 최적화할 때도 데이터베이스 사용자들이 보통 생각하는 것보다 훨씬 많은 일을 수행한다. 즉, 수많은 경우의 수가 존재한다. 30 | - 아래와 같은 정보를 사용해 옵티마이저가 경로를 찾는다. 31 | - 테이블, 컬럼, 인덱스 구조에 관한 기본 정보 32 | - 오브젝트 통계 : 테이블 통계, 인덱스 통계, (히스토그램을 포함한) 컬럼 통계 33 | - 시스템 통계 : CPU 속도, Single Block I/O 속도, Multiblock I/O 속도 등 34 | - 옵티마이저 관련 파라미터 35 | - 이러한 작업을 매번 하면 과부하이기 떄문에, 캐시를 두고 소프트 파싱을 진행하는 것이다. 36 | 37 | ### 바인드 변수의 중요성 38 | 39 | - 이름없는 SQL 문제 40 | - 사용자 정의 함수/프로시저, 트리거, 패키지 등은 생성할 떄부터 이름을 갖음. 41 | - 즉, 컴파일한 상태로 딕셔너리에 저장되며 사용자가 삭제 하지 않는 이상 영구적으로 보관된다. 42 | - 그러나 SQL은 이름이 존재하지 않는다. 그 이유는 SQL 자체가 이름이며, 텍스트 중 작은 하나라도 수정되면 그 순간 다른 객체가 새로 탄생하는 구조이기 때문이다. 43 | - 일회성 SQL도 많고 무효화된 SQL도 많기 때문에 모두 저장하기에는 많은 공간이 필요하기 때문이다. 44 | 45 | - 공유 가능 SQL 46 | - SQL을 캐시에서 찾기 위해 사용하는 키 값은 SQL문 그 자체다. 동일한 결과를 보여주는 SQL이라고 해도 철자하나라도 다르면 다른 SQL이라고 보는 것이다. 47 | - 아래와 같이 작성한다고 해보자. 48 | 49 | ```jsx 50 | SELECT * FRO CUSTOMER WHERE LOGIN_ID = '" + login_id + "'"; 51 | ``` 52 | 53 | - 100만명이 한번에 조회를 하면 모두 하드파싱을 하게 될 것이다. 54 | - 그래서 프로시저 하나를 공유하면서 재사용하는 것이 마땅하기 때문에 바인드 변수를 사용하게 되는 것이다. 55 | 56 | ```jsx 57 | SELECT * FROM CUSTOMER WHERE LOGIN_ID = ? 58 | ``` 59 | -------------------------------------------------------------------------------- /1장_SQL_처리_과정과_IO/1.1_sql_파싱과_최적화_유효정.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/1장_SQL_처리_과정과_IO/1.1_sql_파싱과_최적화_유효정.pdf -------------------------------------------------------------------------------- /1장_SQL_처리_과정과_IO/1.3_최락준.md: -------------------------------------------------------------------------------- 1 | # 1.3 데이터 저장 구조 및 I/O 메커니즘 2 | 3 | ## 1.3.1 SQL이 느린 이유 4 | 5 | > sql이 느린 이유 : **디스크 I/O** 6 | 7 | 하나의 CPU는 한 번에 하나의 프로세스를 처리한다. 8 | 그런데 그 프로세스가 I/O작업을 하게 되면 I/O작업이 끝날 때까지 대기 상태로 머문다. 9 | 여러 프로세스들과 경합에서 자원을 할당받은 프로세스가 열심히 일해야 하는 시간에 대기하는 것이다. 10 | sql의 지연은 바로 이 대기 시간때문에 발생한다. 11 | 12 | --- 13 | 14 | ## 1.3.2 데이터베이스 저장 구조 15 | 16 | 데이터베이스의 저장 구조는 다음과 같이 논리적 구조와 물리적 구조로 나눌 수 있다. 17 | 18 | ![](images/1-3-1.png) 19 | 20 | ``` 21 | * 블록 : 데이터를 읽고 쓰는 단위 22 | * 익스텐트 : 세그먼트의 저장 공간을 확장하는 단위 23 | * 세그먼트 : 데이터 저장공간이 필요한 오브젝트(테이블, 인덱스, LOB 등) 24 | * 테이블 스페이스 : 세그먼트를 담는 컨테이너 25 | ``` 26 | 27 | * 하나의 세그먼트에 해당하는 익스텐트는 같은 데이터파일에 위치하지 않을 수 있다. 28 | * 이는 파일 경합을 줄이기 위해 여러 데이터파일로 분산해서 저장하기 때문이다. 29 | 30 | --- 31 | 32 | ## 1.3.3 블록 단위 I/O 33 | 34 | 데이터베이스에서 데이터를 읽고 쓰는 단위 : `블록` 35 | 36 | * 하나의 레코드를 읽을 때도 블록을 통째로 읽는다. 37 | * 오라클의 기본 블록 크기는 8kb 이다. 38 | 39 | --- 40 | 41 | ## 1.3.4 시퀀셜 액세스 vs 랜덤 액세스 42 | 43 | ### (1) 시퀀셜 액세스 44 | 45 | > 물리적 혹은 논리적 순서에 따라 블록을 읽는 방식 46 | 47 | 인덱스 리프 블록의 경우 48 | * 인덱스 리프 블록은 앞뒤를 가리키는 주소값을 통해 논리적으로 연결되어 있다. 49 | * 주소값 대로 순차적으로 블록을 읽는다. 50 | 51 | 테이블 블록의 경우 52 | * **익스텐트 목록을 세그먼트 헤더에 맵**으로 관리. 이 맵에는 각 **익스텐트의 첫 번째 블록 주소 값이 저장**되어 있다. 53 | * 각 익스텐트의 첫 번째 블록부터 순서대로 읽는 것이 Full Table Scan이다. 54 | 55 | ### (2) 랜덤 액세스 56 | 57 | > 논리적/물리적 순서를 따르지 않고, 레코드 하나를 읽기 위해 한 블록씩 접근하는 방식 58 | 59 | --- 60 | 61 | ## 1.3.5 논리적 I/O vs 물리적 I/O 62 | 63 | ### DB 버퍼 캐시 64 | 65 | * 디스크에서 읽어 들인 데이터를 `DB 버퍼 캐시`에 저장한다 66 | * 이를 통해 반복적인 디스크 I/O Call을 줄일 수 있다. 67 | 68 | ### 논리적 I/O 69 | 70 | * DB 버퍼 캐시를 통해 데이터를 가져오는 것을 논리적 I/O라고 한다. 71 | * 주의할 점은 논리적 I/O는 DB 버퍼 캐시에서 **바로 찾은** 경우를 의미하지 않는다. 72 | * 디스크 I/O를 통해 데이터를 가져오더라도 DB 버퍼 캐시를 경유하기 때문에 결국 논리적 I/O는 **총 읽은 블록 수**가 된다. 73 | 74 | ### 물리적 I/O 75 | 76 | * DB 버퍼 캐시에 데이터가 없어 디스크에 접근하여 데이터를 가져오는 것을 물리적 I/O라고 한다. 77 | * 논리적 I/O는 전기적 신호인데 반해 물리적 I/O 는 실제로 디스크에서 데이터를 가져오기 때문에 논리적 I/O에 비해 훨씬 느리다. 78 | 79 | ### 버퍼캐시 히트율(BCHR) 80 | 81 | 버퍼캐시의 효율을 측정하는 지표로 공식은 다음과 같다. 82 | ``` 83 | BCHR = ( 캐시에서 곧바로 찾은 블록 수 / 총 읽은 블록 수 ) * 100 84 | = ( (논리적 I/O - 물리적 I/O) / 논리적 I/O) * 100 85 | ``` 86 | 87 | * 버퍼캐시 히트율을 높이는데 가장 중요한 것은 물리적 I/O 를 줄이는 것이 아니다. 88 | * **논리적 I/O 를 줄이는 것**이 버퍼캐시 히트율에 가장 큰 영향을 미친다.(논리적 I/O는 분모이기 때문에 아무리 물리적 I/O가 적다한들 소용 없다. 89 | * 즉 sql의 성능 향상을 위해서는 `총 읽은 블록 수`를 줄이는 것이 핵심이다. 90 | 91 | --- 92 | 93 | ## 1.3.6 Single Block I/O vs Multiblock I/O 94 | 95 | ### Single Block I/O 96 | 97 | * 한번의 I/O Call에서 하나의 block을 가져오는 것. 98 | * 일반적으로 인덱스를 사용할 때는 Single Block I/O 를 사용한다. 99 | * 따라서 소량 데이터를 읽을 때 효율적이다. 100 | 101 | ### Multiblock I/O 102 | 103 | * 한번의 I/O Call에서 여러개의 block을 가져오는 것. 104 | * 대용량 데이터를 한꺼번에 가져올 때 효율적이다. 105 | 106 | --- 107 | 108 | ## 1.3.7 Table Full Scan vs Index Range Scan 109 | 110 | ### Table Full Scan 111 | 112 | * 시퀀셜 액세스 + Multiblock I/O 113 | * 즉 데이터를 한꺼번에 순서대로 읽는 방식이다. 114 | 115 | ### Index Range Scan 116 | 117 | * 랜덤 액세스 + Singleblock I/O 118 | * 데이터에 임의로 접근하여 작은 단위로 읽는 방식이다. 119 | 120 | ### Table Full Scan vs Index Range Scan 121 | 122 | * Table Full Scan 이라고 무조건 느린 것이 아니고, Index Range Scan 이라고 무조건 빠르지 않다. 123 | * **대량의 데이터를 가져와 한번에 작업**하는 경우 (배치, 통계 등)에서는 한번에 많이 처리하는 `Table Full Scan` 방식이 빠르다. 124 | * **대용량의 데이터에서 소량의 데이터를 검색**하는 경우(검색 등)에는 임의로 접근할 수 있는 `Index Range Scan` 방식이 빠르다. 125 | 126 | --- 127 | 128 | ## 1.3.8 캐시 탐색 매커니즘 129 | 130 | ### 메모리 공유자원에 대한 액세스 직렬화 131 | 132 | * 버퍼시는 SGA의 구성요소이므로, 버퍼캐시에 저장된 데이터는 공유된다. 133 | * 이 때 버퍼캐시의 사용은 동시에 이뤄지지 않고 순차적으로 접근하도록 구현되어 있는데, 이를 `래치`라 한다. 134 | * 래치는 일종의 버퍼 캐시 자물쇠이다. 해당 자격이 있는 프로세스만이 데이터에 접근하도록 하는 것이다. 135 | * 이러한 (잠그는) 방식은 결국 여러 프로세스가 접근 할 때 캐시 I/O 의 지연을 발생 시킨다. 136 | * 이러한 지연을 줄이는 방법 또한 결국 `논리적 I/O 수를 줄이는 것`이다. -------------------------------------------------------------------------------- /1장_SQL_처리_과정과_IO/1장_SQL_처리_과정과_I:O_황준호.md: -------------------------------------------------------------------------------- 1 | ## 1장. SQL 처리 과정과 I/O 2 | 3 | ### 1.1 SQL 파싱과 최적화 4 | 5 | - SQL (Structured Query Language) 6 | - 구조적, 집합적, 선언적 질의 언어 7 | - 전체적인 과정 8 | - 사용자 --SQL--> 옵티마이저 --실행계획--> 프로시저 9 | - SQL 최적화 10 | - DBMS 내부에서 프로시저를 작성하고 컴파일해서 실행 가능한 상태로 만드는 전 과정 11 | - SQL이 실행되는 과정 12 | 1. SQL 파싱 13 | - 파싱 트리 생성 (SQL 개별 구성요소를 분석해서 파싱 트리 생성) 14 | - Syntax 체크 (문법 오류 확인) 15 | - Semantic 체크 (의미상 오류 확인. 없는 테이블인지, 없는 컬럼인지, 권한은 있는지 등) 16 | 2. SQL 최적화 17 | - 옵티마이저가 다양한 실행 경로 중 하나를 선택한다 18 | 3. 로우 소스 생성 19 | - 옵티마이저가 선택한 실행 경로를 실제 실행 가능한 코드 또는 프로시저로 포맷팅 20 | - 로우 소스 생성기가 수행함 21 | - SQL 옵티마이저 22 | - 최적의 데이터 액세스 경로를 선택해주는 DBMS의 핵심 엔진 23 | - 단계 : 후보 실행계획을 찾음 -> 각 실행계획의 예상비용 산정(데이터 딕셔너리에 미리 수집해둔 통계 및 시스템 통계 정보 이용) -> 최저비용 선택 24 | - 옵티마이저 힌트 25 | - 옵티마이저가 항상 최적의 실행계획을 찾는 건 아니다. SQL이 복잡할수록 실수할 가능성이 크다 26 | - 직접 사용할 인덱스를 결정해줄 수 있다 27 | - 일부만 지정해주고 나머지는 옵티마이저가 알아서 선택하도록 해줄수도 있다 28 | 29 | ### 1.2 SQL 공유 및 재사용 30 | 31 | - 소프트 파싱 vs 하드 파싱 32 | 33 | - DBMS가 SQL을 파싱한 후 해당 SQL이 라이브러리 캐시에 존재하는지 확인하여 34 | - 있으면? 곧바로 실행 (소프트 파싱) 35 | - 없으면? 최적화 -> 로우소스 생성 -> 실행 (하드 파싱) 36 | 37 | - SQL 최적화를 할때 옵티마이저가 사용하는 정보 38 | 39 | - 테이블, 컬럼, 인덱스 구조 40 | - 오브젝트 통계 (테이블 통계, 인덱스 통계, 컬럼 통계) 41 | - 시스템 통계 (CPU속도, Single Block I/O 속도, Multiblock I/O 속도) 42 | - 옵티마이저 관련 파라미터 등등 43 | 44 | - 이렇게 많은 정보를 사용하여 무거운 연산을 통해 도출한 내부 프로시저를 한 번만 사용한다면 비효율적이다. 그래서 라이브러리 캐시가 필요하다 45 | 46 | - 바인드변수를 잘 활용해야 한다 47 | 48 | - 예를 들어 다음과 같이 라이브러리 캐시가 남으면 안되고, 49 | 50 | ```sql 51 | SELECT * FROM CUSTOMER WHERE LOGIN_ID = 'hwang' 52 | SELECT * FROM CUSTOMER WHERE LOGIN_ID = 'kim' 53 | SELECT * FROM CUSTOMER WHERE LOGIN_ID = 'lee' 54 | SELECT * FROM CUSTOMER WHERE LOGIN_ID = 'park' 55 | ... 56 | ``` 57 | 58 | - 다음과 같이 남아야 한다 59 | 60 | ```sql 61 | SELECT * FROM CUSTOMER WHERE LOGIN_ID = :1 62 | ``` 63 | 64 | ### 1.3 데이터 저장 구조 및 I/O 메커니즘 65 | 66 | - SQL이 느린 이유는 거의 I/O 때문 67 | 68 | - 데이터베이스 저장 구조 69 | 70 | - 데이터 파일 : 디스크 상의 물리적인 OS 파일 71 | - 테이블 스페이스 : 세그먼트를 담는 컨테이너 72 | - 세그먼트 : 데이터 저장공간이 필요한 오브젝트 73 | - 익스텐트 : 공간을 확장하는 단위 74 | - 블록 : 데이터를 읽고 쓰는 단위 75 | 76 | - 테이블 또는 인덱스 블록을 읽는 방식 77 | 78 | 1. 시퀀셜 엑세스 : 차례로 블록을 읽는 방식 79 | 2. 랜덤 액세스 : 레코드 하나를 읽기 위해 한 블록씩 접근하는 방식 80 | 81 | - 논리적 I/O vs 물리적 I/O 82 | 83 | - 논리적 I/O 84 | - SQL문을 처리하는 과정에 메모리 버퍼캐시에서 발생한 총 블록 I/O 85 | - 물리적 I/O 86 | - 디스크에서 발생한 총 블록 I/O를 말한다 87 | 88 | - 버퍼캐시 히트율(BCHR) = ( 1- ( 물리적I/O / 논리적I/O ) ) * 100 89 | 90 | - 물리적I/O는 통제불가, 논리적I/O는 통제가능 91 | - 논리적I/O를 줄임으로써 물리적I/O를 줄이는 것이 SQL 튜닝이다 92 | - BCHR이 높다고 해서 효율적인 SQL라는 뜻은 아니다 (같은 블록을 비효율적으로 반복하면 BCHR은 높아진다) 93 | 94 | - Single Block I/O vs Multiblock I/O 95 | 96 | - I/O call할때 97 | - 한번에 한 블록씩 요청해서 메모리에 적재하면 Single Block I/O 98 | - 한번에 여러 블록씩 요청해서 메모리에 적재하면 Multiblock I/O 99 | - 인덱스를 사용할때는 기본적으로 Single Block I/O를 사용한다 (소량을 읽을때 주로 사용하므로) 100 | - 많은 데이터 블록을 읽을땐 Multiblock I/O가 효율적이다 (같은 익스텐트에 속한 블록을 모두 가져온다) 101 | 102 | - Table Full Scan vs Index Range Scan 103 | 104 | - Table Full Scan : 테이블 전체를 스캔해서 읽는 방식 105 | - 시퀀셜 엑세스와 Multiblock I/O 방식으로 디스크 블록을 읽는다 106 | - 한 블록에 속한 모든 레코드를 한번에 읽어들이고 캐시에서 못찾으면 한번의 수면을 통해 많은 블록을 한꺼번에 I/O하는 매커니즘 107 | - Index Range Scan : 인덱스를 이용하여 읽는 방식 108 | - 랜덤 액세스와 Single Block I/O 방식으로 디스크 블록을 읽는다 109 | - 캐시에서 블록을 못찾으면 레코드 하나를 읽기 위해 매번 잠을 자는 I/O매커니즘 110 | - 많은 데이터를 읽을때는 오히려 성능이 떨어질 수 있다 111 | 112 | - 캐시 탐색 매커니즘 113 | 114 | - 다음의 경우 버퍼캐시 탐색 과정을 거친다 115 | - 인덱스 루트 블록을 읽을때 116 | - 인덱스 루트 블록에서 얻은 주소 정보로 브랜치 블록을 읽을때 117 | - 인덱스 브랜치 블록에서 얻은 주소 정보로 리프 블록을 읽을때 118 | - 인덱스 리프 블록에서 얻은 주소 정보로 테이블 블록을 읽을때 119 | - 테이블 블록을 Full Scan할때 120 | - 버퍼 캐시에서는 블록 번호를 해시함수로 관리한다 121 | - 래치 122 | - 버퍼 블록을 여러 프로세스가 동시에 접근하면 안된다. 그래서 직렬화(줄세우기)가 필요하다. 직렬화가 가능하도록 지원하는 매커니즘을 래치라고 한다 123 | - 버퍼캐시에는 캐시버퍼 체인래치, 캐시버퍼 LRU 체인래치 등이 작동한다 124 | - 버퍼블록 자체에서 직렬화 매커니즘이 존재한다 (버퍼 락) 125 | -------------------------------------------------------------------------------- /1장_SQL_처리_과정과_IO/1주_김민석.md: -------------------------------------------------------------------------------- 1 | # 1장 SQL 처리 과정과 I/O 2 | 3 | ## 1.1 SQL 파싱과 최적화 4 | 5 | - SQL : Structured Query Language 6 | - 구조적 질의 언어 7 | - 구조적 / 집합적 / 선언적 8 | - 원하는 결과집합은 구조적/집합적으로 선언 9 | - 결과집합을 만드는 과정은 절차적 => 프로시저 필요 10 | - 프로시저를 최적화 하는 것이 옵티마이저 11 | - 또한 이 과정을 SQL 최적화라고 함 12 | 13 | 14 | - SQL 최적화 15 | - SQL 파싱 -> SQL 최적화 16 | 17 | 18 | - SQL 옵티마이저 19 | - 사용자가에게 전달 받은 쿼리를 실행할 후보군 선정 20 | - 후보군 중 가장 최저 비용을 나타내는 실행계획 선택 21 | 22 | 23 | - 옵티마이저 힌트 24 | - 자동으로 옵티마이저를 하지만 사용자가 힌트를 주어 의도적으로 다른 실행계획을 선택 할 수 있게 함 25 | 26 | 27 | ## SQL 공유 및 재사용 28 | 29 | - 소프트 파싱 / 하드 파싱 30 | - 캐시되어 있지 않은 경우 SQL 파싱, 최적화, 로우 소스 생성 등을 거쳐 내부 프로시저를 생성하는데 이를 하드 파싱이라 하고 캐시된 하드 파싱을 바로 찾아 쓰는 것을 소프트 파싱이라 한다. 31 | 32 | 33 | - 바인드 변수의 중요성 34 | - SQL은 전체가 하나의 이름이 된다. 따라서 이름을 따로 만들 필요가 없다 35 | - 따라서, 바인드 변수를 프로그래밍적으로 하드 코딩 하게 되면 그 수만큼 캐싱되어 쌓이게 되는 문제가 발생한다. 36 | 37 | ## 데이터 저장 구조 및 I/O 메커니즘 38 | 39 | - SQL이 느린 이유 40 | - I/O는 매우 느리다. 41 | - 쿼리를 빠르게 하기 위해서는 I/O를 줄이고 프로세서가 일어나서 동작하는 시간을 최대한 늘여야한다. 42 | 43 | 44 | - 데이터베이스 저장 구조 45 | - 테이블스페이스 > 세그먼트 > 익스텐트 > 블록 46 | - 익스텐트 단위로 공간 확장 47 | - 실제 데이터는 블록에 저장 48 | - 한 블록은 같은 테이블 저장 49 | - 한 익스텐트는 같은 테이블 저장 50 | 51 | 52 | - I/O는 블록 단위로 이루어진다 53 | 54 | - 액세스 방식 55 | - 시퀀셜 56 | - 연결된 순서대로 차례대로 읽는다 57 | - 인덱스 리프 블록 읽을 때 58 | - 테이블 블록 읽을 때(Full Table Scan) 59 | - 랜덤 60 | - 논리적, 물리적 순서를 따르지 않고 레코드 하나를 읽기 위해 한 블록씩 접근 61 | 62 | 63 | - 논리적 I/O vs 물리적 I/O 64 | - DB 버퍼 캐시 : 디스크에서 읽은 데이터 블록의 캐시 65 | - 라이브러리 캐시: SQL/ 실행계획/ 프로시저를 저장한 캐시 66 | - 논리적 I/O : 읽어야 하는 총 블록 67 | - 물리적 I/O : 읽어야 하는 총 블록 중 디스크에서 읽어 온 블록 68 | - BCHR(Buffer Cache hit Ratio) 69 | - 논리적 I/O 중 물리적 I/O를 제외하고 DB 버퍼 캐시에서 읽어 온 비율 70 | - 논리적 I/O 를 줄이는 것이 성능을 올리는 방법 71 | - `SQL 튜닝은 결국 논리적 I/O를 줄이는 것` 72 | 73 | - 싱글 블록 I/O vs 멀티 I/O 74 | - 싱글 블록 I/O 75 | - 한번에 하나의 블록만 읽는 것 76 | - 인덱스 읽을 때 77 | - 멀티 블록 I/O 78 | - 데이터 블록 읽을 때 79 | 80 | 81 | - Table Full Scan vs Index Range Scan 82 | - Table Full Scan 83 | - 데이터 전체를 스캔해서 읽는 것 84 | - Index Range Scan 85 | - 인덱스를 이용한 테이블 액세스 86 | - 두 가지 방식을 적절하게 사용하는 것이 중요 87 | 88 | 89 | - 캐시 탐색 메커니즘 90 | - 메모리 버퍼 캐시 탐색 시 해시 알고리즘 사용 91 | - 메모리 버퍼 캐시는 SGA 구성요소 이기 때문에 공유 자원 92 | - 따라서 동시성 관리 필요 93 | - 내부적으로 동시에 접근 불가하고 순차적으로 접근함(직렬화) 94 | - 줄서기 담당이 래치(Latch) 95 | -------------------------------------------------------------------------------- /1장_SQL_처리_과정과_IO/images/1-3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/1장_SQL_처리_과정과_IO/images/1-3-1.png -------------------------------------------------------------------------------- /1장_SQL_처리_과정과_IO/조수진.md: -------------------------------------------------------------------------------- 1 | # 1장 SQL 처리 과정과 I/O 2 | 3 | ## 1.1 SQL 파싱과 최적화 4 | ### 1.1.1 구조적, 집합적, 선언적 질의 언어 5 | - SQL은 기본적으로 구조적이고 집합적이고 선언적인 질의 언어다 6 | - 원하는 결과집합을 구조적, 집합적으로 선언하지만, 그 결과집합을 만드는 과정은 절차적일 수 밖에 없다 = 프로시저가 필요하다! 7 | - DBMS 내부의 엔진인 옵티마이저가 프로시저를 생성한다 = 프로시저가 대신 프로그래밍 해주는 셈 8 | - 프로시저를 작성하고 컴파일 해서 실행 가능한 상태로 만드는 전 과정을 `SQL 최적화` 9 | ### 1.1.2 SQL 최적화 10 | 1. SQL 파싱 11 | - 파싱 트리 생성 12 | - Syntax 체크 13 | - Semantic 체크 14 | 2. SQL 최적화 15 | 3. 로우 소스 생성(실행 가능한 코드 또는 프로시저 형태로 포맷팅하는 단계) 16 | ### 1.1.3 SQL 옵티마이저 17 | - 쿼리를 수행하기 위한 실행계획들을 찾아냄 18 | - 데이터 딕셔너리에 미리 수집해둔 오브젝트에 통계 및 시스템 통계 정보를 이용해 각 실행계획의 예상 비용 산정 19 | - 최저 비용을 나타내는 실행계획 선택 20 | ### 1.1.4 실행계획과 비용 21 | - MySQL에서 실행 계획을 확인하기 위한 키워드 `EXPLAIN` 22 | - 실행하는 실행계획은 옵티마이저가 비용을 근거로 선택한 것 23 | - 비용은 쿼리를 실행하는 동안 발생할 것으로 `예상되는` `I/O 횟수, 예상 소요 시간`을 표현한 것 24 | 25 | **옵티마이저가 실행계획을 세우기 위해 고려하는 수많은 정보들** 26 | 27 | - 테이블, 컬럼, 인덱스 구조에 관한 기본 정보 28 | - 오브젝트 통계: 테이블 통계, 인덱스 통계, (히스토그램을 포함한) 컬럼 통계 29 | - 시스템 통계: CPU 속도, Single Block I/O 속도, Mutliblock I/O 속도 등 30 | - 옵티마이저 관련 파라미터 31 | ### 1.1.5 옵티마이저 힌트 32 | - 상황에 따라 사용하거나 사용하지 않을 수 있음 33 | - MySQL에서 힌트를 사용하기 위해서 주석 기호에 +를 붙여 작성한다 34 | 35 | 예시: `SELECT /*+ ... 8/` 36 | 37 | ## 1.2 SQL 공유 및 재사용 38 | ### 1.2.1 소프트 파싱 vs 하드 파싱성 39 | ![SGA 구조](/img/pic_1-4.png) 40 | - SGA: 서버 프로세스와 백그라운드 프로세스가 공통으로 액세스하는 데이터와 제어 구조를 캐싱하는 메모리 공간 41 | - 라이브러리 캐시(쿼리 캐시): SQL파싱, 최적화, 로우 소스 생성 과정을 거친 내부 프로시저는 라이브러리 캐시에 저장되어 재사용된다 42 | - 소프트 파싱: 파싱한 SQL이 라이브러리 캐시에 존재하면 바로 실행 43 | - 하드 파싱: 파싱한 SQL이 라이브러리 캐시에 존재하지 않아서 최적화, 로우 소스 생성과정을 모두 거치는 것 44 | - 재사용 이유? 최적화 과정에 딕셔너리, 통걔 정보를 읽어 효율성을 판단하는 과정은 CPU를 많이 사용하는 과정 45 | ### 1.2.2 바인드 변수의 중요 46 | - 라이브러리 캐시에 저장되는 SQL에는 이름이 없다 47 | - SQL자체가 검색을 위한 key -> 대, 소문자, 뛰어쓰기 등 아주 조금만 달라도 매번 하드 파싱하고 캐시에 저장한다 48 | - (중요)!바인드 변수를 사용할 것(자바 PreparedStatement)! 49 | 50 | **바인드 변수** 51 | - PL/SQL 또는 SQL 쿼리문에서 WHERE 절에 value 값을 (?) 로 사용하는 변수 52 | - 아래와 같이 파라이터 Driven 방식으로 내부 프로시저를 생성한다 53 | 54 | `create procedure LOGIN (login_id varchar2) { ... }` 55 | 56 | ## 1.3 데이터 저장 구조 및 I/O 메커니즘 57 | ### 1.3.1 SQL이 느린 이유 58 | - SQL이 느린 이유는 디스크 I/O 때문. 디스크 I/O가 SQL 성능을 좌우 59 | - I/O를 진행하는 동안 프로세스는 대기 상태에 빠진다 60 | - 스토리지 성능이 빨라지고 있지만 여전히 시간이 가장 많이 소요되는 작업 61 | ### 1.3.2 데이터베이스 저장 구조 62 | - 데이터파일: 디스크 상의 물리적인 OS 파일 63 | - 테이블 스페이스: 세그먼트를 담는 컨테이너, 여러 개의 데이터파일로 구성됨 64 | - 세그먼트: 테이블, 인덱스, LOB컬럼 등과 같이 데이터 저장공간이 필요한 오브젝트, 파니셔닝 되지 않았다면 한 오브젝트는 하나의 세그먼트에 저장됨 65 | - 테이블 세그먼트 헤더에 익스텐트 맵(각 익스텐트의 첫 번째 블록 DBA 저장)이 있음 66 | - 익스텐트: 공간을 확장하는 단위. 세그먼트 구성요소. 연속된 블록 집합. 세그먼트 공간이 부족하면 테이블스페이스로부터 익스텐트를 추가 할당 받음 67 | - 한 세그먼트의 익스텐트끼리는 연속된 공간이 아닐 수 있음 68 | - 블록(or 페이지): 실제 데이터가 저장되는 공간. 익스텐트의 구성요소. 한 블록에 저장된 레코드는 모두 같은 테이블 레코드 69 | - 익스텐트 내의 블록은 서로 인접한 연속된 공간 70 | - DBA(Data Block Address): 데이터 블록이 디스크 상에서 몇 번 데이터파일의 몇 번째 블록인지 나타내는 고유 주소값 71 | ![그림 1-12](/img/pic_1-12.png) 72 | ### 1.3.3 블록 단위 I/O 73 | 데이터 I/O 단위는 블록이다. 한 레코드만 읽고 싶어도 그 레코드가 포함된 해당 블록을 통째로 읽음(테이블, 인덱스 모두 해당) 74 | ### 1.3.4 시퀀셜 액세스 vs 랜덤 액세스 75 | 테이블 또는 인덱스 블록을 액세스하는 방법 76 | - 시퀀셜 액세스: 논리적 혹은 물리적으로 연결된 순서에 따라 차례로 블록을 읽는 방식 77 | - 인덱스 리프 노드로 범위 검색 78 | - 테이블 세그먼트 헤더 맵으로 풀 테이블 스캔 79 | - 랜덤 엑세스: 레코드 하나를 읽기 위해 한 블록씩 접근하는 방식 80 | ### 1.3.5 논리적 I/O vs 물리적 I/O 81 | - 자주 읽는 블록을 매번 디스크에서 읽는 것은 비효율적! -> 캐싱! 82 | - DB Buffer Cache: 디스크에서 읽은 데이터 블록을 캐시, 공유메모리 영역 83 | - 데이터 블록을 읽을 땐 항상 버퍼 캐시부터 탐색 84 | - 논리적 I/O: SQL을 처리하는 과정에서 메모리 버퍼 캐시에서 발생한 총 블록 I/O 85 | - 논리적 I.O (유사) 메모리 I/O 86 | - 물리적 I/O: 디스크에서 발생한 총 블록 I/O 87 | - 메모리 I/O(전기적 신호) <<<<< 디스크 I/O(액세스 암을 통해 물리적 작용) 대략 10,000배 느림 88 | - 버퍼캐시 히트율(Buffer Cache Hit Ratio, BCHR) = (논리적 I/O - 물리적 I/O) / 논리적 I/O * 100 89 | - OLTP 애플리케이션은 시스템 레벨에서 평균 99% 히트율을 달성해야 한다 90 | - BCHR은 시스템 상황에 따라 달라지므로 물리적 I/O는 결국 통제 불가능한 외생변수 91 | - 논리적 I/O는 내생변수 92 | 93 | => SQL 튜닝을 통해 논리적 I/O를 줄이고, 그로 인해 물리적 I/O도 줄어들어 성능을 향상시키는 것! 94 | ### 1.3.6 Single Block I/O vs Multiblock I/O 95 | - Single block I/O: 한 번에 한 블록씩 요청해서 메모리에 적재하는 방식 96 | - 인덱스를 사용하는 방식 97 | - Multiblock I/O: 한 번에 여러 블록씩 요청해서 메모리에 적재하는 방식 98 | - 테이블 전체 스캔 99 | - 많은 데이터를 가져올 때 multiblcok I/O 단위가 클수록 유리 100 | - multiblock I/O를 이용해도 인접 블록만 가져올 수 있다(같은 익스텐트 내에서면 여러 블록씩 가져올 수 있다는 이야기!) 101 | ### 1.3.7 Table Full Scan vs Index Range Scan 102 | - Table Full Scan이라고 항상 느리고, Index Range Scan이라고 항상 빠른 것이 아니다 103 | - 많은 데이터를 처리하는 집계용 SQL, 배치 프로그램 등은 Table Full Scan을 사용해서 자주 성능이 향상된다 104 | ### 1.3.8 캐시 탐색 메커니즘 105 | - Direct Path I/O를 제외한 모든 블록 I/O는 메모리 버퍼캐시를 경유한다 106 | - 버퍼캐시는 해시 구조로 관리한다(같은 입력값은 동일한 해시 체인=버킷에 연결) 107 | - 해시 체인에 버퍼 헤더를 담고 있다, 거기서 얻은 포인터로 버퍼 블록을 액세스 한다 108 | ![그림 1-27](/img/pic_1-27.png) 109 | - 버퍼캐시, 버퍼캐시 체인은 공유자원이므로 직렬화 메커니즘이 필요하다 110 | - 캐시버퍼 체인 레치, Lock 111 | 112 | => 직렬화 메커니즘으로 경합을 줄이기 위해 SQL 튜닝을 통해 논리적 I/O를 줄여야한다 113 | 114 | ### 기타 115 | - OLTP: online transaction processing 116 | - DW: data warehouse 117 | 118 | ### 참고 링크 119 | https://myinfrabox.tistory.com/92 120 | 121 | https://velog.io/@sezzzini/%EA%B8%88%EC%9C%B5-IT-ETL-%EA%B3%BC-DW#dw 122 | 123 | ### 질문 124 | 1. 1.1장 질문 125 | - 데이터 딕셔너리가 뭔가요? 126 | - SQL plus의 Auto trace 기능? 127 | 2. 1.2장 질문 128 | 3. 1.3 장 질문 129 | - 59p Full Scan 중에 Chian이 발생한 로우를 읽을 때도 Single Block I/O 방식으로 발생. Chian이 뭐지,,? 130 | - 인덱스를 이용한 접근은 Single Block I/O를 사용한다고 했는데, 인덱스의 리프 노드로 시퀀셜 액세스 해도 Single Block I/O로 동작할까? Mutliblock I/O로 동작할까? -------------------------------------------------------------------------------- /2장_인덱스_기본/2.2_인덱스_기본_사용법_김광훈.md: -------------------------------------------------------------------------------- 1 | # 2.2 인덱스 기본 사용법 2 | ## 2.2.1 인덱스를 사용한다는 것 3 | #### (1) Index range scan 4 | Index range scan 은 인덱스 시작점을 찾아서 거기서부터 스캔을 시작하고 중간에 멈추는 것을 의미한다. 5 | 6 | 일단 아래 쿼리를 예제로 살펴보자. 7 | 8 | ```mysql 9 | SELECT * FROM employees WHERE first_name BETWEEN 'Ebbe' AND 'Gad'; 10 | ``` 11 | 12 | 13 | 14 | 위 그림의 화살표를 살펴보면 루트 노드에서부터 비교를 시작해서 브랜치 노드를 거치고 최종적으로 리프 노드까지 찾아 들어가야만 비로서 실재로 원하는 시작 지점을 찾을 수 있다. 15 | 16 | 일단, 시작해야 할 위치를 찾으면 그때부터는 리프 노드의 레코드만 순서대로 읽으면 된다. 17 | 18 | 이처럼 쭉 읽는 것을 스캔이라고 한다. 만약 스캔하다가 리프 노드의 끝까지 읽으면 리프 노드 간의 링크를 이용해 다음 리프 노드를 찾아서 다시 스캔하고 반복한다. 19 | 20 | 위 예제는 인덱스만을 읽는 경우를 보여준 것이다. 21 | 22 |
23 | 24 | 실제로 B-Tree 인덱스의 리프 노드를 스캔하면서 실제 데이터 파일의 레코드를 읽어 와야 하는 경우도 많다. 이제 그 과정을 보자. 25 | 26 | 27 | 28 | 위 그림을 보면, B-Tree 인덱스에서 루트와 브랜치 노드를 이용해 특정 검색(스캔) 시작 가지고 있는 리프 노드를 검색한다. 29 | 30 | 그 이후, 필요헌 방향(오름차순 or 내림차순) 으로 인덱스를 읽어 나가는 과정을 볼 수 있다. 31 | 32 | 중요한 점은 해당 인덱스를 구성하는 컬럼의 정(역)순으로 `정렬 된 상태`로 레코드를 가져온다는 것이다. 33 | 34 | 이는 별도의 정렬 과정이 수반되는 것이 아닌 `인덱스 자체의 정렬 특성` 때문에 자동으로 그렇게 된다. 35 | 36 |
37 | 38 | 또 다른 중요한 점은 리프 노드에 저장된 레코드 주소로 데이터 파일의 레코드를 읽어온다. 39 | 40 | 이때 레코드 한 건 한 건 단위로 `랜덤 I/O` 가 한 번씩 실행된다. 41 | 42 |
43 | 44 | 예를들어 3건의 레코드가 검색 조건에 일치한다고 한다면 데이터 레코드를 읽기 위해 랜덤 I/O 가 최대 3번이 필요하다. 45 | 46 | 따라서 인덱스를 통해 읽어야 할 데이터 레코드가 20~25% 를 넘으면 인덱스를 통한 읽기보다 테이블의 데이터를 직접 읽는 것이 더 효율적이다. 47 | 48 |
49 | 50 | #### (2) Index Full scan 51 | 인덱스를 사용하는 것은 동일하지만 인덱스의 처음부터 끝까지 모두 읽는 방식이다. 52 | 53 | 대표적으로 쿼리의 조건절에 사용된 칼럼이 인덱스의 첫 번째 칼럼이 아닌 경우 인덱스 풀 스캔 방식이 사용된다. 54 | 55 | 예를 들어 인덱스는 (A, B, C) 칼럼의 순서대로 만들어져 있지만 쿼리의 조건절은 B 혹은 C 칼럼으로 검색하는 경우이다. 56 | 57 | 58 | 59 | 위 그림을 보면, 인덱스 리프 노드의 제일 앞 또는 제일 뒤로 이동 후 인덱스의 리프 노드를 연결하는 Linked List 를 따라서 처음부터 끝까지 스캔하는 것을 볼 수 있다. 60 | 61 |
62 | 63 | 참고로, Real MySQL 책에서는 인덱스 풀 스캔 방식을 사용하는 경우 인덱스를 사용하지 못한다 또는 인덱스를 효율적으로 사용하지 못한다고 표현했다. 64 | 65 |
66 | 67 | #### (3) 인덱스 정렬 --> 추가 내용 68 | 일반적인 오라클 같은 상용 DBMS 에서는 아래와 같이 인덱스 생성 시점에 오름차순 혹은 내림차순 정렬을 설정할 수 있다. 69 | 70 | ```mysql 71 | CREATE INDEX ix_teamname_userscore ON employees (team_name ASC,user_score DESC); 72 | ``` 73 | 74 | 하지만 MySQL 5.7 까지는 앞으로 만들어질 버전에 대한 호환성을 위해 문법상으로만 제공이되고, 실질적으로는 조건은 무시되고 무조건 오름차순으로만 정렬이 되었다. 75 | 76 | 그 이후로 MySQL 8.0 에서 부터는 ASC, DESC 모두 지원하게 되었다. 77 | 78 | 79 | 80 | 이로인하여 인덱스를 역으로 스캔하지 않아도 되기 때문에 이전 버전에 비하여 상당히 효율적인 것을 볼 수 있다. 81 | 82 | 추가로 공식문서에서는 Descending index 는 InnoDB storage engine 만 지원된다고 적혀있다. 83 | 84 | 85 | 86 | 해당 기능에 대한 내용은 [해당 블로그](https://tech.kakao.com/2018/06/19/mysql-ascending-index-vs-descending-index/) 에서 자세히 설명하고 있다. 87 | 88 |
89 | 90 | ## 2.2.2 인덱스 Range scan 할 수 없는 이유 91 | 인덱스 스캔을 하기 위해서는 시작지점과 끝지점이 있어야 한다. 92 | 93 | 예를들어 년도에 상관 없이 5월에 태어난 학생을 찾으라는 쿼리를 작성한다고 해보자. 94 | 95 | 2010년, 2020냔, 2021년 등등 스캔해야 하는 지점이 너무 많기 때문에 이러한 케이스는 전교샹 전부를 스캔할 수 밖에 없다. 96 | 97 | 이제 쿼리를 보면서 MySQL 에서 인덱스 Range scan 이 가능한지 보자 98 | 99 |
100 | 101 | #### (1) 조건절 + OR 절 102 | ```mysql 103 | CREATE INDEX idx_test ON customer (name); 104 | 105 | SELECT * 106 | FROM customer as c 107 | WHERE (c.name = '광훈' OR c.phone_number = '01044445555'); 108 | ``` 109 | 110 | 111 | 112 | 위 결과를 보면, 전화번호가 XXX 이고 이거나 이름이 XXX 인 어느 한 시작지점을 찾을 수 없기 때문에 Index range scan 을 할 수 없다. 113 | 114 |
115 | 116 | #### (2) OR Expansion 117 | ```mysql 118 | CREATE INDEX idx_test ON customer (name); 119 | 120 | SELECT * 121 | FROM customer as c 122 | WHERE c.name = '광훈' 123 | UNION ALL 124 | SELECT * 125 | FROM customer as c 126 | WHERE c.phone_number = '01044445555' 127 | AND (c.name != '광훈' or c.name is null); 128 | ``` 129 | 130 | 131 | 132 | Unique Index Scan 으로 동작했기 때문에 시작과 끝만 정의를 한다면 Index Range scan 으로 동작할 것을 알 수 있다. 133 | 134 |
135 | 136 | #### (3) IN 절 137 | 138 | ```mysql 139 | CREATE INDEX idx_test ON customer (name); 140 | 141 | SELECT * 142 | FROM customer as c 143 | WHERE c.name in ('광훈', '세윤'); 144 | ``` 145 | 146 | 147 | 148 | IN 절은 OR 조건을 표현하는 다른 방식이기 때문에 역시 Index range scan 은 불가능하다. 149 | 150 | 마찬가지로 IN 절은 UNION ALL 방식으로 작성해보자. 151 | 152 | ```mysql 153 | SELECT * 154 | FROM customer as c 155 | WHERE c.name = '광훈' 156 | UNION ALL 157 | SELECT * 158 | FROM customer as c 159 | WHERE c.name = '세윤'; 160 | ``` 161 | 162 | 163 | 164 | Unique Index Scan 으로 동작했기 때문에 시작과 끝만 정의를 한다면 Index Range scan 으로 동작할 것을 알 수 있다. 165 | 166 |
167 | 168 | ## 2.2.3 더 중요한 인덱스 사용 조건 169 | 아래와 같이 인덱스를 (소속팀 + 사원명 + 연령) 순으로 구성한 예제를 보자. 170 | 171 | 172 | 173 | ```mysql 174 | SELECT 사원번호, 소속팀, 연령, 입사일자, 전화번호 175 | FROM 사원 176 | WHERE 사원명 = '홍길동'; 177 | ``` 178 | 179 | 만약 위와 같은 쿼리를 실행시켜보자. 180 | 181 | 홍길동이라는 이름을 가진 사원은 A 소속팀, B 소속팀 등등 여러명이 있을 수 있다. 따라서 이름이 같을 지라도 서로 멀리 떨어지게 된다. 182 | 183 | 184 | 185 | 따라서 해당 쿼리로는 인덱스 스캔 시작점을 찾을 수 없고 어디서 멈춰야될지 모르기 때문에 인덱스 리프 블록을 처음부터 끝까지 모두 스캔해야 한다. 186 | 187 | 정리하자면, 인덱스를 Range scan 하기 위한 가장 첫 번째 조건은 인덱스 선두 컬럼이 조건절에 있어야 한다. (가공하지 않은 상태로) 188 | 189 |
190 | 191 | ## 2.2.4 인덱스를 이용한 소트 연산 생략 192 | 인덱스를 사용하면 인덱스가 정렬되어 있기 때문에 소트 연산 생략 효과도 부수적으로 얻을 수 있다. 193 | 194 | 195 | 196 | 197 | ```mysql 198 | -- 인덱스 생성 199 | CREATE INDEX idx_test ON equipment (number, updated_at, updated_order ); 200 | 201 | -- 소트 연산 생략 예제 202 | SELECT * 203 | FROM equipment as e 204 | WHERE e.number = 'C' AND e.updated_at = '20180507' ORDER BY e.updated_order; 205 | ``` 206 | 207 | 208 | 209 | 위와 같이 장비번호와 변경일자를 모두 '=' 조건으로 검색할 때 PK 인덱스를 사용하면 결과집합은 변경순번 순으로 출력된다. 210 | 211 | 실행 결과를 살펴보면 소트 연산은 실행되지 않는 것으로 보인다. 212 | 213 |
214 | 215 | ## 2.2.5 ORDER BY 절에서 컬럼 가공 216 | "인덱스 컬럼을 가공하면 인덱스를 정상적으로 사용할 수 없다." 는 말은 주로 조건절에서 사용한 칼럼을 말한다. 217 | 218 | 그런데 조건절이 아닌 ORDER BY 또는 SELECT-LIST 에서 컬럼을 가공함으로 인해 인덱스를 정상적으로 사용할 수 없는 경우도 종종 있다. 219 | 220 | ```mysql 221 | -- ORDER BY 에 가공 된 컬럼 제공 예제 222 | -- 인덱스 생성 223 | CREATE INDEX idx_test ON equipment (number, updated_at, updated_order ); 224 | 225 | SELECT * 226 | FROM equipment as e 227 | WHERE e.number = 'C' 228 | ORDER BY e.updated_order, e.updated_at; 229 | ``` 230 | 231 | 인덱스에는 가공하지 않은 상태로 값을 저장하였는데, 가공한 값 기준으로 정렬해 달라고 요청했기 떄문에 정렬 연산을 생략할 수 없다. (오라클 기준) 232 | 233 | 234 | 235 | 하지만 MySQL 에서는 실행결과를 확인해보면 Sort 연산이 실행이 된 것을 볼 수 있다. 236 | 237 |
238 | 239 | ## 2.2.6 SELECT-LIST 에서 컬럼 가공 240 | ```mysql 241 | -- MIN, MAX 함수 예제 242 | SELECT MIN(e.updated_order) 243 | FROM equipment as e 244 | WHERE e.number = 'C' AND e.updated_at = '20180507'; 245 | 246 | SELECT MAX(e.updated_order) 247 | FROM equipment as e 248 | WHERE e.number = 'C' AND e.updated_at = '20180507'; 249 | 250 | ``` 251 | 252 | 253 | 위와 같이 최대(최소 값)을 구하는 연산을 할 때도 옵티마이저는 정렬 연산을 실행시키지 않는다. 254 | 255 |
256 | 257 | ## 2.2.7 자동 형변환 258 | MySQL 5.7/8.0 에서는 자동으로 서로 다른 타입을 비교하거나 연산을 할 때, 자동으로 형변환하는 것을 지원해준다. 259 | 260 | 261 | 262 | ```mysql 263 | drop index idx_test on customer; 264 | CREATE INDEX idx_test ON customer ( birth_day ); 265 | 266 | SELECT * 267 | FROM customer as c 268 | WHERE c.birth_day = 19940318; 269 | ``` 270 | 271 | 272 | 273 | 실행계획을 보면 인덱스 Range scan 으로 동작하지 않은 것 을 볼 수 있다. 274 | 275 | 고객 테이블 생년월일 컬럼이 문자형인데 조건절 비교 값을 숫자형으로 표현했기 때문에 옵티마이저는 인덱스 컬럼을 가공했기 때문에 Full Scan 으로 동작을 한 것이다. 276 | 277 | -------------------------------------------------------------------------------- /2장_인덱스_기본/2.3_인덱스_확장기능_사용법_황준호.md: -------------------------------------------------------------------------------- 1 | ### 2.3 인덱스 확장기능 사용법 2 | 3 | #### Index Range Scan 4 | 5 | - B-Tree 인덱스의 가장 일반적이고 정상적인 형태의 액세스 방식 6 | - 인덱스 루트에서 리프 블록까지 수직적으로 탐색한 후 필요한 범위만 스캔한다 7 | - 인덱스를 Range Scan 하려면 선두 컬럼을 가공하지 않은 상태로 조건절에서 사용해야 한다 8 | - 반대로 말하면 선두 컬럼을 가공만 하지 않으면 Range Scan은 무조건 가능하므로 인덱스를 탄다고 성능도 무조건 좋다고 생각하면 안된다 9 | - 성능은 인덱스 스캔 범위, 테이블 엑세스 횟수를 얼마나 줄일 수 있느냐로 결정된다 10 | 11 | #### Index Full Scan 12 | 13 | - 수직적 탐색 없이 인덱스 리프 블록을 처음부터 끝까지 수평적으로 탐색하는 방식 14 | - 대개 데이터 검색을 위한 최적의 인덱스가 없을때 차선으로 선택됨 15 | 16 | #### Index Unique Scan 17 | 18 | - 수직적 탐색으로만 데이터를 찾는 스캔 방식 19 | - unique 인덱스를 '=' 조건으로 탐색하는 경우 Index Unique Scan 20 | - 단, unique 인덱스라 하더라도 범위검색 조건(between, 부등호, like)으로 검색할떄는 Index Range Scan으로 처리된다 21 | - 또, unique 결합 인덱스에 대해 일부 컬럼만으로 검색할 때도 Index Range Scan으로 처리된다 22 | - 예 : 주문상품 PK 인덱스를 "주문일자 + 고객id + 상품id"로 구성했는데 "주문일자 + 고객id"로만 검색하는 경우 23 | 24 | #### Index Skip Scan 25 | 26 | - 루트 또는 브랜치 블록에서 읽은 컬럼 값 정보를 이용해 조건절에 부합하는 레코드를 포함할 가능성이 있는 리프 블록만 골라서 엑세스하는 스캔 방식 27 | - 인덱스 선두 컬럼의 카디널리티가 낮고, 후행 컬럼의 카디널리티가 높을때 유용하다 28 | 29 | #### Index Fast Full Scan 30 | 31 | - Index Full Scan보다 빠른 스캔 방법 32 | - 논리적인 인덱스 트리 구조를 무시하고 인덱스 세그먼트 전체를 Multiblock I/O방식으로 스캔하기 때문 33 | - 특징 34 | - Multiblock I/O방식을 사용하므로 디스크로부터 대량의 인덱스 블록을 읽어야 할 때 큰 효과를 발휘한다 35 | - 결과 집합이 인덱스 키 순서대로 정렬되지 않는다 36 | - 쿼리에 사용한 컬럼이 모두 인덱스에 포함돼 있을때만 사용 가능하다 37 | - 인덱스카 파티션 돼 있지 않더라도 병렬 쿼리가 가능하다 38 | 39 | #### Index Range Scan Descending 40 | 41 | - Index Range Scan과 기본적으로 동일하지만 인덱스를 뒤에서부터 스캔하기 때문에 내림차순으로 정렬된 결과집합을 얻는다 42 | 43 | #### 참고. MySQL의 실행계획 44 | 45 | - id : 각 SELECT문에 부여됨 46 | - select_type 47 | - SIMPLE : 단순 SELECT문 48 | - table : 접근하는 테이블 이름 49 | - partitions 50 | - type 51 | - ALL : 테이블 풀 스캔 52 | - index : 인덱스 풀 스캔 53 | - range : 인덱스 레인지 스캔 54 | - const : PK 혹은 UK로 조회하는 경우. 많아야 한 건 55 | - possible_keys : 사용 가능한 인덱스들 56 | - key : possible_keys중 실제로 사용할 인덱스 57 | - key_len : 인덱스에 얼마나 많은 바이트를 사용하고 있는지 58 | - ref 59 | - rows : 원하는 행을 찾기 위해 얼마나 많은 행을 읽어야 할 지에 대한 예측값 60 | - filtered : 행 데이터를 가져와 거기에서 WHERE 구의 검색 조건이 적용되면 몇행이 남는지 예측값 61 | - Extra 62 | - Using where : where 조건으로 데이터를 추출. type이 ALL 혹은 INDEX 타입과 함께 표현되면 성능이 좋지 않다는 의미 63 | - Using filesort : 데이터 정렬이 필요한 경우로 메모리 혹은 디스크상에서의 정렬을 모두 포함. 결과 데이터가 많은 경우 성능에 직접적인 영향을 줌 64 | - Using index condition 65 | - Using temporary 66 | - Select tables optimized away 67 | -------------------------------------------------------------------------------- /2장_인덱스_기본/2주_김민석.md: -------------------------------------------------------------------------------- 1 | # 2장 인덱스 기본 2 | 3 | 4 | ## 인덱스 구조 및 탐색 5 | 6 | - 데이터를 찾는 두 가지 방법 7 | - 테이블 전체 스캔 8 | - 인덱스 이용 9 | 10 | 11 | - 온라인 트랜잭션 처리에서는 소량의 데이터를 찾기 때문에 인덱스를 사용하고 인덱스 튜닝이 중요하다 12 | 13 | 14 | - SQL 튜닝은 랜덤 I/O와의 전쟁 15 | 16 | 17 | - 인덱스는 대부분 B-Tree 18 | - 수직적 탐색 : 인덱스 스캔 지점을 찾는 과정 19 | - 수평적 탐색 : 데이터를 찾는 과정 20 | 21 | 22 | - 인덱스는 필터링이 아니다. 결합인덱스를 사용할 때 주의 사항. 23 | 24 | 25 | ## 인덱스 기본 사용법 26 | 27 | - 색인이 정렬되어 있더라도(인덱스라도) 가공한 값이나 중간값으로는 스캔 시작점을 찾을 수 없다. 28 | - Index Range Scan 29 | - 인덱스 컬럼이 가공되면 전체를 스캔할 수 밖에 없다. 30 | - Index Full Scan 31 | 32 | 33 | - 인덱스를 range scan 하기 위한 가장 첫번째 조건은 인덱스 선두 컬럼이 조건절에 있어야 한다. 34 | 35 | 36 | - 인덱스는 기본적으로 정렬되어 있기 때문에 소트 연산의 생략이 가능하다 37 | 38 | 39 | - ORDER BY 나 SELECT-LIST 컬럼을 가공하면 인덱스 레인지 스캔이 불가능하다. 40 | 41 | 42 | - 인덱스의 타입을 맞추지 않으면 인덱스 사용이 불가능하다. 43 | 44 | 45 | ## 인덱스 확장기능 사용법 46 | 47 | - Index Range Scan 48 | - 루트에서 리프 블록까지 수직적 탐색 후에 필요한 범위 수평적 스캔 49 | - Index Full Scan 50 | - 수직적 탐색 없이 인덱스 리프 블록을 처음부터 끝까지 수평적 탐색 51 | - Index Unique Scan 52 | - 인덱스로 '=' 조건 검색 시. 53 | - Index Skip Scan 54 | - Index Full Scan을 사용해야 할 경우에 필요없는 부분은 skip하는 방식 55 | - 인덱스 선두 컬럼의 Distinct Value 개수가 적고 후행 컬럼의 Distinct Value 개수가 많을 때 유용 56 | - Index Fast Full Scan 57 | - Index Full Scan을 순서대로 하지 않고 인덱스 세그먼트 전체를 멀티블럭 I/O 하는 방식. 58 | - 속도도 빠르고 병렬로 읽어 올 수 있지만 결과집합이 키 순서대로 정렬되지 않는다. 59 | - Index Range Scan Descending 60 | - Index Range Scan을 필요에 따라 거꾸로 읽는 방식 61 | 62 | -------------------------------------------------------------------------------- /2장_인덱스_기본/조수진.md: -------------------------------------------------------------------------------- 1 | # 2장. 인덱스 기본 2 | 3 | ## 2.1 인덱스 구조 및 탐색 4 | ### 2.1.1 미리 보는 인덱스 튜닝 5 | **데이터베이스 테이블에서 자료를 찾는 방법** 6 | 1. 테이블 풀 스캔 7 | 2. 인덱스 이용 8 | 9 | **인덱스 튜닝의 핵심 요소** 10 | - 인덱스 스캔 효율화 튜닝 11 | - 랜덤 액세스 최소화 튜닝 12 | 13 | **Tip!** DBMS가 제공하는 많은 기능이 느린 랜덤 I/O를 극복하기 위해 개발됨 14 | ### 2.1.2 인덱스 구조 15 | 인덱스는 B*Tree 구조 = 정렬됨 = 범위 스캔이 가능하다 16 | 17 | **BTree 구성** 18 | - 루트 노드 19 | - 브랜치 노드 20 | - 리프 노드 21 | 22 | **루트와 브랜치 블록의 구성** 23 | - 각 레코드는 하위 블록에 대한 주소값을 가짐 24 | - key 값을 가지고, 키 값은 하위 블록에 저장된 키 값의 범위를 나타냄 25 | 26 | ex) 서 → 연결된 블록은 서보다 크거나 같은 레코드가 저장됌 27 | 28 | - LMC: leftmost child 가장 왼쪽에 위치한 블럭을 가리킴 29 | 30 | → 첫 번째 키값을 가진 레코드보다 작거나 같은 레코드가 저장됌 31 | 32 | **리프 블록의 구성** 33 | - 정렬된 key 값 34 | - 테이블 레코드를 가리키는 주소값(=ROWID) 35 | 36 | ROWID = DBA(Data block address) + 로우 번호 37 | 38 | = 데이터 파일 번호 + 블록 번호 + 로우 번호 39 | 40 | **수직적, 수평적 탐색** 41 | - 수직적 탐색: 인덱스 스캔 시작 시점을 찾는 과정 42 | - 수평적 탐색: 데이터를 찾는 과정 43 | ### 2.1.3 인덱스 수직적 탐색 44 | 조건을 만족하는 첫 번째 레코드를 찾는 과정 45 | 46 | → 찾고자 하는 값 보다 크거나 같은 레코드(key)를 찾으면 그 직전 레코드가 가리키는 하위 블록으로 이동 47 | 48 | → 찾고자 하는 값과 같은 레코드를 찾아도 그 직전 레코드가 가리키는 하위 블록으로 가야 조건을 만족하는 '첫번째' 블록을 찾을 수 있음 49 | ### 2.1.4 인덱스 수평적 탐색 50 | 실제 데이터를 찾는 과정 51 | 52 | = 리프 블록에서 수평적으로 탐색하면서 찾고자 하는 데이터가 더 안나타날 때까지 스캔 53 | 54 | → 인덱스 리프는 더블 링크드 리스트 구조 55 | ### 2.1.5 결합 인덱스 구조와 탐색 56 | 결합 인덱스: 두 개 이상의 컬럼을 결합해서 인덱스를 만드는 것 57 | 58 | 인덱스를 (고객명, 성별)로 구성하든, (성별, 고객명)으로 구성하든 읽는 인덱스 블록 개수는 똑같다 59 | 60 | where에서 '=' 조건 검색할 때는 어느 컬럼을 인덱스 앞쪽에 두든 블록 I/O 개수가 같다(= 성능이 똑같다) 61 | 62 | 인덱스 구성에 따라 성능 차이가 나는 것은 맞지만, 엑셀의 필터처럼 생각하면 안된다! 63 | 64 | ## 2.2 인덱스 기본 사용법 65 | 66 | ### 2.2.1 인덱스를 사용한다는 것 67 | 68 | Index Full Scan vs Index Range Scan 69 | 70 | 일반적으로 인덱스를 정상적으로 사용한다는 것은 Index Range Scan을 의미 71 | 72 | = 리프 블록에서 스캔 시작점을 찾아서 중간에 멈추는 지점을 찾아 색인을 멈추는 것 73 | 74 | → 인덱스 컬럼(정확히는 선두 컬럼)을 가공하거나 인덱스의 중간에 포함된 값으로 색인을 하고자 하는 경우 Index Range Scan이 불가능하고 Index Full Scan을 할 수 밖에 없다 75 | 76 | ### 2.2.2 인덱스 Range Scan 할 수 없는 이유 77 | 78 | 아래와 같이 컬럼을 가공하는 경우, 컬럼의 중간에 위치하는 값으로 검색하는 경우, or이나 in 절로 검색하는 경우는 Index Range Scan이 불가능하다 79 | 80 | 하지만 Or, In 절은 옵티마이저의 쿼리 변환 기능을 통해 여러 개의 Index Range Scan으로 처리되기도 한다 81 | 82 | → IN-List Iterator 방식, union all 83 | 84 | where substr(생년월일, 5, 2) = ‘05’ 85 | 86 | where nvl(주문수량, 0) < 100 87 | 88 | where 업체명 like ‘%대한%’ 89 | 90 | where (전화번호 =: tell_no or 고객명 =: cust_nm) 91 | 92 | where 전화번호 in (:tell_no1, :tell_no2) 93 | 94 | ### 2.2.3 더 중요한 인덱스 사용 조건 95 | 96 | 인덱스 Range Scan을 하기 위해서는 반드시 **인덱스 선두 컬럼**이 **가공되지 않은 상태로** 조건절에 있어야 한다 97 | 98 | 그렇다면 무조건 Range Scan은 가능하다 99 | 100 | 하지만 인덱스를 잘 탄다고 해서 튜닝이 끝난 것은 아니다 101 | 102 | 예를 들어, 인덱스가 `주문일자 + 상품번호` 순으로 구성되어 있고, 하루 평균 100만 건의 데이터가 쌓인다면 103 | 104 | `Select * from 주문상품 where 주문일자 =: ord_dt and 상품번호 like ‘%ping%’;` 105 | 106 | 위의 쿼리는 매 번 100만 건의 데이터를 스캔해야함! 107 | 108 | ### 2.2.4 인덱스를 이용한 소트 연산 생략 109 | 110 | 인덱스는 정렬된 상태로 저장되어 있다 → select 의 order by 연산을 생략할 수 있다 111 | 112 | 오름 차순의 경우, 조건을 만족하는 가장 작은 값부터 왼쪽에서 오른쪽으로 스캔을 하고, 113 | 114 | 내림차순의 경우 조건을 만족하는 가장 큰 값인 오른쪽에서부터 왼쪽으로 스캔함 115 | 116 | ### 2.2.5 ORDER BY 절에서 컬럼 가공 117 | 118 | `select * from (select to_char(a.주문번호, ‘FM000000’) as 주문번호, a.업체번호, a.주문금액 from 주문 a where a.주문일자 =: dt and a.주문번호 > NVL(:next_ord_no, 0) order by 주문번호) 119 | where rownum < = 30` 120 | 121 | → order by 절에 사용된 주문번호는 to_char로 가공된 주문번호 → sort 가 일어어난다 122 | 123 | → a.주문번호로 바꿔주면 sort 생략가능 124 | 125 | ### 2.2.6 SELECT-LIST에서 컬럼 가공 126 | 127 | `SELECT MIN(변경순번) FROM 상태변경이력 WHERE 장비번호 = 'C' AND 변경일자 = '20180316'` 128 | 129 | `SELECT MAX(변경순번) FROM 상태변경이력 WHERE 장비번호 = 'C' AND 변경일자 = '20180316'` 130 | 131 | → 인덱스를 이용해 정렬 연산 없이 최소, 최대값을 빠르게 찾을 수 있다 132 | 133 | 실행계획에는 INDEX RANGE SCAN (MIN/MAX) 상태변경이력_PK, FIRST ROW라고 뜸 134 | 135 | `SELECT NVL(MAX(TO_NUMBER(변경순번)), 0) FROM 상태변경이력 WHERE 장비번호 = 'C' AND 변경일자 = '20180316'` 136 | 137 | → 이런식으로 짜면 인덱스 컬럼에 수정이 생기므로 정렬 연산을 줄일 수 없다 138 | 139 | ⇒ `SELECT NVL(TO_NUMBER(MAX(변경순번)), 0) FROM 상태변경이력 WHERE 장비번호 = 'C' AND 변경일자 = '20180316'`로 수정 가능 140 | 141 | - 서브 쿼리에서 사용할 경우도 마찬가지 142 | 143 | ```sql 144 | SELECT 장비번호, 장비명, 상태코드 145 | , (SELECT MAX(변경일자)) 146 | FROM 상태변경이력 147 | WHERE 장비번호 = P.장비번호) 최종변경일자 148 | , (SELECT MAX(변경순번) 149 | FROM 상태변경이력 150 | WHERE 장비번호 = P.장비번호 151 | AND 변경일자 = (SELECT MAX(변경일자) 152 | FROM 상태변경이력 153 | WHERE 장비번호 = P.장비번호)) 최종변경순번 154 | FROM 장비 P 155 | WHERE 장비구분코드 = 'A001' 156 | ``` 157 | 158 | ### 2.2.7 자동 형변환 159 | 160 | 조건절에서 입력되는 데이터 타입과 실제 테이블 타입이 다르면 컴파일 시점에 에러를 내는 DBMS도 있고 Oracle처럼 형변환해주는 DBMS도 있다. 인덱스 컬럼에 자동 형변환이 걸리면 인덱스 사용이 불가능하거나 효율이 떨어지므로 주의해야함! 161 | 162 | - 우선 순위: 숫자형 > 날짜형 > 문자형 163 | 164 | ex. `SELECT * FROM 고객 WEHRE 생년월일 = 19821225` 165 | 166 | 167 | → 문자열 타입인 생년월일이 숫자 타입으로 형변환되면서 인덱스를 사용할 수 없게 됨 ⇒ 테이블 풀스캔 168 | 169 | - Like는 문자열 비교 연산자이므로 문자형 기준으로 숫자형 컬럼이 변환된다 170 | 171 | ex. `SELECT * FROM 고객 WEHRE 고객번호 LIKE '9410%'` ⇒ 테이블 풀스캔 172 | 173 | - 형변환에 의해 실행 에러가 발생할 수 있다 174 | 175 | ex. 문자형 컬럼을 숫자형으로 변경할 때, 숫자로 변경할 수 없는 문자열이 있는 경우 176 | 177 | - 형변환에 의해 결과 오류가 발생할 수 있다 178 | 179 | ex. `select round(avg(sal)) avg_sal, min(sal) min_sal, max(sal) max_sal, max(decode(job, 'PRESDENT', NULL, sal)) max_sal2 from emp` 180 | 181 | → president를 제외한 급여 중 가장 큰 급여 값이 950으로 나온다 182 | 183 | → decode(a, b, c, d)를 처리할 때, a=b이면 c를 반환하고 아니면 d를 반환. 그 때, 데이터 타입은 세번째 인자 c에 의해 결정되는데, Null은 varchar2로 취급된다 184 | 185 | ⇒ sal이 문자열로 리턴되고 문자열 기준 max가 동작됨 186 | 187 | 188 | ⇒ 결론! 자동 형변환에 의존하지말고 명시적으로 형변환 해줄 것(TO_CAHR, TO_DATE, TO_NUMBER) 189 | 190 | ## 2.3 인덱스 확장기능 사용법 191 | 192 | ### 2.3.1 Index Range Scan 193 | 194 | - BTree인덱스의 가장 일반적이고 정상적인 액세스 방식 195 | - 인덱스 루트에서 리프 블록까지 수직적 탐색 → 필요한 범위 스캔 196 | - 인덱스의 선두 컬럼을 가공하지 않은 상태로 조건절에서 사용해야 한다 197 | - 성능을 위해서는 인덱스를 스캔하는 범위, 테이블 액세스 횟수를 얼마나 줄일 수 있느냐로 결정된다 198 | 199 | ### 2.3.2 Index Full Scan 200 | 201 | - 수직적 탐색 없이 인덱스 리프 블록을 처음부터 끝까지 수평적으로 탐색하는 방식 202 | - 데이터 검색을 위한 최적의 인덱스가 없을 때, I/O를 줄이기 위해 Table Full Scan 대신 차선으로 사용 203 | 204 | (ex. 대용량 테이블이어서 Table Full Scan 비용이 크다면 = 대용량 데이터 중 아주 일부 테이블을 액세스 하는 상황) 205 | 206 | 207 | ```sql 208 | create index emp_ename_sal_idx on emp (ename, sal); 209 | select * from emp where sal > 2000 order by ename; 210 | ``` 211 | 212 | - 정렬된 결과를 쉽게 얻기 위해(소트 연산을 생략) 옵티마이저가 전략적으로 선택할 수 있다 213 | 214 | → first rows 힌트로 옵티마이저 모드를 변경해서 최초 N건 조회. But fetch를 멈추지않고 데이터를 끝까지 읽는다면 Table Full Scan보다 느리다 215 | 216 | 217 | ```sql 218 | select /*+ first_rows */from emp where sal > 1000 order by ename; 219 | ``` 220 | 221 | ### 2.3.3 Index Unique Scan 222 | 223 | - 수직적 탐색만으로 데이터를 찾는 스캔 방식 224 | - Unique 인덱스를 '='조건으로 탐색하는 경우 225 | - Unique 인덱스라도 범위 검색(between, 부등호, like)을 할 때는 Index Range Scan 226 | - Unique 결합 인덱스에 대해 일부 컬럼으로만 검색할 때도 Index Range Scan 227 | 228 | ### 2.3.4 Index Skip Scan 229 | 230 | - 오라클 9i에서 새로 선보인 스캔 방식(MySQL에서는 Loose Index Scan) 231 | - 조건절에 부합하는 레코드를 포함할 '가능성이 있는' 리프 블록만 골라서 액세스하는 스캔 방식 232 | - index_ss, no_index_ss 힌트를 사용해서 스캔 방식 유도 233 | - 인덱스 선두 컬럼의 Distinct Value 개수가 적고 후행 컬럼의 Distinct Value 개수가 많을 때 유용 234 | - 인덱스의 빠진 컬럼의 경계 조건에 해당하는 블럭은 꼭 포함해야함! 235 | - 적용 예시: 복합 인덱스의 선두 혹은 중간 컬럼이 없는 경우, 선두 컬럼이 범위 조건인 경우 236 | - Index Range Scan이 불가능하거나 효율적이지 못한 상황에서 Index Skip Scan을 차선으로 택할 수 있다 237 | 238 | ### 2.3.5 Index Fast Full Scan 239 | 240 | - Index Full Scan 보다 빠르다 241 | - 이유: 인덱스 트리 구조를 무시하고 인덱스 세그먼트 전체를 Multiblock I/O 스캔 242 | - 루트와 브랜치도 읽지만 필요없는 정보라 버림 243 | - 속도는 빠르지만 결과 지합이 인덱스 키 순서대로 정렬되지 않음 244 | - 쿼리에서 사용한 컬럼이 모두 인덱스에 포함되어 있어야 사용 가능 245 | - index_ffs, no_index_ffs 힌트 사용 246 | - Index Range Scan, Index Full Scan과 달리 인덱스가 파티션 돼 있지 않더라도 병렬 쿼리(스캔) 가능 247 | - 병렬 쿼리 시에는 Direct Path I/O 방식을 사용해서 I/O 속도가 더 빨라진다 248 | 249 | ### 2.3.6 Index Range Scan Descending 250 | 251 | - Index Range Scan과 기본적으로 동일 252 | - 뒤에서부터 앞으로 스캔하기 때문에 내림차순으로 정렬된 결과 집합을 얻는다 253 | 254 | ### 질문 255 | - 83p: 루트로부터 모든 리프 블록까지 높이가 항상 같다 → 대량으로 insert되거나 delete되는 경우는 바로 밸런싱되나? 256 | - 그림 2-1 리프 노드가 논리적으로는 시퀀셜인데 실제 물리적으로 연결된 곳에 위치하는 건 아니지 않을까,,,? 257 | - union all?? union은 합집합 기능 258 | - 99p 참고로 애초에 발견한 SQL의 ORDER BY 절에는 ‘주문번호’가 아니라 ‘1’이라고 적혀있었다. ‘1’은 SELECT-LIST에 나열된 첫 번째 컬럼을 의미한다??? 무슨말이지! 259 | - select-list → SELECT와 FROM 사이 - 원하는 COLUMN(속성)만 조회 260 | - 102p 마지막 쿼리 이해가 잘 안됨,, SUBSTR? || 가 인덱스 컬럼을 가공? 261 | - cr, pr? 읽은 블록? 262 | 263 | ### 질문 264 | - 83p: 루트로부터 모든 리프 블록까지 높이가 항상 같다 → 대량으로 insert되거나 delete되는 경우는 바로 밸런싱되나? 265 | - 그림 2-1 리프 노드가 논리적으로는 시퀀셜인데 실제 물리적으로 연결된 곳에 위치하는 건 아니지 않을까,,,? 266 | -------------------------------------------------------------------------------- /3장_인덱스_튜닝/3.1_테이블_엑세스_최소화_김광훈.md: -------------------------------------------------------------------------------- 1 | 2 | # 3.1 테이블 엑세스 최소화 3 | ## 서론 4 | 1. 아무리 데이터가 많아도 인덱스를 사용하면 데이터가 금방 조회가 된다. 5 | 6 | 2. 대량 데이터를 조회할 때, 인덱스를 사용하면 테이블 전체 스캔보다 느리다. 7 | 8 | ## ROWID 9 | #### 인덱스 스캔을 하는 이유 10 | 검색 조건을 만족하는 소량의 데이터를 인덱스에서 빨리 찾고 거기서 테이블 레코드를 찾아가기 위한 주소 값인 ROWID 를 얻으려는데 있다. 11 | 12 | ROWID 는 물리적주소보다는 논리적주소에 가깝다. 디스크 상에서 테이블 레코드를 찾아가기 위한 위치 정보를 담는다. 13 | 14 | ## 메인 메모리 DB 와 비교 15 | #### 메인 메모리 디비(MMDB) 16 | 데이터를 모두 메모리에 로드해 놓고 메모리를 통해서만 I/O 를 수행 17 | 18 | #### 속도 차이 ?? 19 | 오라클과 같은 RDB 는 인덱스에 디스크 상의 주소를 가짐 20 | 21 | 메인 메모리 DB 는 메모리상의 주소정보, 즉 포인터를 갖고 있기 떄문에 인덱스를 경유해 테이블 엑세스하는 비용이 낮다. 22 | 23 | 인덱스 ROWID 는 다시 정리하면 디스크 상에서 테이블 레코드를 찾아가기 위한 논리적인 주소 정보이다. 24 | 25 | ROWID 가 가리키는 테이블 블록을 버퍼캐시에서 먼저 찾아보고 못 찾을 때만 디스크에서 블록을 읽는다. 물론 버퍼캐시에 적재한 후에 읽는다. 26 | 27 | 설령 모든 데이터가 캐싱되어 있더라고 테이블 레코드를 찾기 위해 매번 DBA 해싱과 레치 획득 과정을 반복해야한다. 28 | 29 | 동시 액세스가 심할 때는 캐시버퍼 체인 래치와 Lock 에 대한 경합도 발생한다. 30 | 31 | 32 | ## 인덱스 클러스터링 팩터 33 | 특정 컬럼을 기준으로 같은 값을 갖는 데이터가 서로 모여있는 정도를 의미한다. 34 | 35 | CF 가 좋은 컬럼에 생성한 인덱스는 검색 효율이 매우 좋다. 36 | 37 | 38 | ## 온라인 프로그램 튜닝 vs 배치 프로그램 튜닝 39 | #### 1. 온라인 프로그램 튜닝 40 | 보통 소량의 데이터를 읽고 갱싱하므로 인덱스를 효과적으로 활용하는 것이 중요하다. 41 | 42 | #### 2. 배치 프로그램 튜닝 43 | 대량 데이터를 읽고 갱깃ㄴ하는 배치 프로그램은 항상 전체범위 처리 기준으로 튜닝해야한다. 44 | 45 | 전체를 빠르게 처리하는 것을 목표로 삼아야한다. 46 | 47 | ## 인덱스 컬럼 추가 48 | 테이블 엑세스 최소화를 위해 가장 일반적으로 사용하는 튜닝 기법은 인덱스에 컬럼을 추가하는 것이다. 49 | 50 | ## 클러스터 테이블 51 | #### 1. 인덱스 클러스터 테이블 52 | 클러스터 키 값이 같은 레코드를 한 블록에 모아서 저장하는 구조 53 | 54 | #### 2. 해시 클러스터 테이블 55 | 인덱스를 사용하지 않고 해시 알고리즘을 상요해 클러스터를 찾아간다. -------------------------------------------------------------------------------- /3장_인덱스_튜닝/3.3.1~3.3.4_인덱스_스캔_효율화_유효정.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/3장_인덱스_튜닝/3.3.1~3.3.4_인덱스_스캔_효율화_유효정.pdf -------------------------------------------------------------------------------- /3장_인덱스_튜닝/3.3.5~3.4.내용_요약_김광훈.md: -------------------------------------------------------------------------------- 1 | # 3.3.5 인덱스 선행 컬럼이 등치 조건이 아닐 때 생기는 비효율 2 | 인덱스 스캔 효율성은 인덱스 컬럼을 조건절에 모두 등치 조건으로 사용할 때 가장 좋다. 3 | 4 | 리프 블록을 스캔하면서 읽은 레코드는 하나도 걸러지지 않고 모두 테이블 엑세스로 이어지므로 인덱스 스캔 단게에서의 비효율은 전혀 없다. 5 | 6 | 7 | # 3.3.6 BETWEEN 을 IN-LIST 로 전환 8 | BETWEEN 조건을 IN-LIST 로 바꿀 수 있다. 9 | 10 | IN-List 개수만큼 UNION ALL 브랜치가 생성되고 각 브랜치마다 모든 칼럼을 '=' 조건으로 검색하므로 앞서 선두 컬럼에 BETWEEN 을 사용할 때와 같은 비효율은 사라진다. 11 | 12 | # 3.3.8 IN 조건은 '=' 인가 13 | 정리하자면 IN 조건은 = 이 아니다. IN 조건이 '=' 이 되려면 IN-List Iterator 방식으로 풀려야한다. 14 | 15 | 그렇지 않다면 IN 조건은 필터 조건이다. 16 | 17 | 즉, IN 은 엑세스 조건 또는 필터 조건으로 사용할 수 있다. 18 | 19 | # 3.3.9 BETWEEN 과 LIKE 스캔 범위 비교 20 | 결론부터 말하자면 LIKE 보다 BETWEEN 을 사용하는 것이 낫다. 21 | 22 | BETWEEN 은 다소 불편하지만 LIKE 보다 효율적이다. 예를들어 `WHERE LIKE '2019%' AND 판매구분 = 'B'` 로 쿼리를 날린다면 201901, 201900 등등 계속 찾아야한다. 23 | 24 | # 3.3.11 다양한 옵션 조건 처리 방식의 장단점 비교 25 | 인덱스 선두 컬럼에는 OR 조건을 사용해서는 안된다. --> 인덱스 스캔을 못함 26 | 27 | OR 조건 정리 28 | ``` 29 | 1. 인덱스 액세스 조건으로 사용 불가 30 | 2. 인덱스 필터 조건으로도 사용 불가 31 | 3. 테이블 필터 조건으로만 사용 가능 32 | ``` 33 | 34 | # 3.4.1 인덱스 설계가 어려운 이유 35 | 인덱스가 많으면 생기는 문제 36 | ``` 37 | 1. DML 성능 저하 38 | 2. 데이터베이스 사이즈 증가 39 | 3. 데이터베이스 관리 및 운영 비용 상승 40 | ``` 41 | 42 | # 3.4.2 가장 중요한 두 가지 선택 기준 43 | Index Range Scan 을 사용하기 위해서는 인덱스 선두 컬럼을 조건절에 반드시 사용해야 한다. 44 | 45 | 따라서 결합 인덱스를 구성할 때 첫 번째 기준은, 조건절에 항상 사용하거나, 자주 사용하는 컬럼을 선정하는 것이다. 46 | 47 | 두 번째 기준은, 그렇게 선정한 컬럼 중 '=' 조건으로 자주 조회하는 컬럼을 앞쪽에 두어야 한다. 48 | 49 | # 3.4.5 소트 연산을 생략하기 위한 컬럼 추가 50 | 인덱스는 항상 정렬 상태를 유지하므로 ORDER BY, GROUP BY 를 위한 소트 연산을 생략할 수 있게 해 준다. 51 | 52 | 따라서 조건절에 사용하지 않는 컬럼이더라도 소트 연산을 생략할 목적으로 인덱스 구성에 포함시킴으로써 성능 개선에 도모할 수 있다. 53 | 54 | # 3.4.6 결합 인덱스 선택도 55 | 인덱스 생성 여부를 결정할 때는 선택도가 충분히 낮은지가 중요한 판단 기준임 56 | 57 | 선택도: 전체 레코드 중에서 조건절에 의해 선택되는 레코드 비율 -> 선택도 x 총 레코스 = 카디널리티 58 | 59 | 인덱스 선택도: 인덱스 칼럼을 모두 '=' 로 조회할 때 평균적으로 선택되는 비율 -> 비율이 높으면 인덱스로서의 효용가치가 없다. -------------------------------------------------------------------------------- /3장_인덱스_튜닝/3.3.5~3.4_황준호.md: -------------------------------------------------------------------------------- 1 | ### 3.3 인덱스 스캔 효율화 2 | 3 | - 인덱스 선두 컬럼이 = 조건일때 효율적이다 4 | 5 | - 인덱스 선행 컬럼이 범위 검색일 경우 비효율적이다 6 | - 범위 검색을 하는 컬럼을 인덱스 선행 컬럼이 아니도록 수정하는 게 어렵다면 between을 in으로 바꾸면 효과를 얻는 경우도 있다 7 | - 하지만 in-list 개수가 많다면 더 비효율적으로 될수도 있다 8 | - 바꾸기 전에 데이터 분포와 수직적 탐색 비용을 고려해야 한다 9 | 10 | - between보단 like를 쓰는게 더 효율적이다 11 | 12 | - between, like 패턴을 사용하고자 할 땐 아래의 경우에 속하는지 고려해야 한다 13 | 14 | 1. 인덱스 선두 컬럼에 대한 옵션 조건을 between, like 연산자로 처리하면 안된다 15 | 2. null 허용 컬럼에 대한 옵션 조건을 between, like 연산자로 처리하면 안된다 16 | 3. 숫자형 컬럼에 대한 옵션 조건을 like연산자로 처리하면 안된다 17 | 4. like를 옵션조건에 사용할 때는 컬럼 값 길이가 고정적이어야 한다 18 | 19 | - 옵션 조건을 처리하는 가장 효율적인 방법은 union all을 이용하는 것이다 20 | 21 | ```sql 22 | -- 인덱스 : a + b 23 | -- a는 옵션 조건 24 | select * 25 | from table_name 26 | where :a_input is null and b between :b_input1 and :b_input2 27 | union all 28 | select * 29 | from table_name 30 | where :a_input is not null and a = :a_input and b between :b_input1 and :b_input2 31 | ``` 32 | 33 | ### 3.4 인덱스 설계 34 | 35 | - 인덱스 설계가 어려운 이유? SQL 각각에 최적화된 인덱스를 마음껏 생성할 수 없기 때문에 36 | 37 | - 인덱스가 늘어나면 늘어날수록 DML 성능저하, 데이터베이스 사이즈 증가, 관리비용 상승 등의 문제가 커진다 38 | 39 | - 결합 인덱스를 구성할때의 가장 중요한 두가지 (판단 기준이 인덱스 스캔 효율성일때) 40 | 41 | 1. 조건절에 항상 사용하거나, 자주 사용하는 컬럼을 선정해야 한다 42 | 2. 그렇게 선정한 컬럼 중 = 조건으로 자주 조회하는 컬럼을 앞쪽에 두어야 한다 43 | 44 | - 인덱스 스캔 효율성 외에도 다른 판단 기준들도 많다 45 | 46 | - 수행 빈도 (효율성 다음으로 가장 중요) 47 | - NL방식으로 조인하는 두 테이블이 있을 때, outer쪽 인덱스보다 inner쪽 인덱스가 더 중요하다 48 | - 업무상 중요도 49 | - 클러스터링 팩터 50 | - 데이터량 51 | - 데이터량이 적으면 인덱스가 많던 적던 크게 이슈가 되지 않는다 52 | - 대용량이라면, 인덱스가 중요하다 53 | - DML부하 54 | - 저장 공간 55 | - 인덱스 관리 비용 등 56 | 57 | - 인덱스는 order by, group by를 위한 소트 연산을 생략할 수 있게 해준다 58 | 59 | - I/O를 최소화하면서도 소트 연산을 생략하려면.. 60 | 1. = 연산자로 사용한 조건절 컬럼 선정 61 | 2. order by 절에 기술한 컬럼 추가 62 | 3. = 연산자가 아닌 조건절 컬럼은 데이터 분포를 고려해 추가 여부 결정 63 | 64 | - 항상 사용하는 컬럼을 앞쪽에 두고 그 중 = 조건을 앞쪽에 위치시켜야 효율적이다 65 | 66 | - 예: 조건절로 인덱스 설계하기 67 | 68 | ```sql 69 | -- 조건절1 70 | where a = :v1 71 | and b = :v2 72 | and c >= :v3 73 | -- 조건절2 74 | where a = :v1 75 | and b = :v2 76 | and c >= :v3 77 | and d = :v4 78 | -- 조건절3 79 | where a = :v1 80 | and b = :v2 81 | and c >= :v3 82 | and e = :v5 83 | -- 조건절4 84 | where a = :v1 85 | and b = :v2 86 | and c >= :v3 87 | and d = :v4 88 | and e = :v5 89 | ``` 90 | 91 | - a,b,c는 항상 들어가고, d,e은 선택이므로 a,b,c먼저, d,e는 뒤로 배치한다 92 | 93 | - a,b는 = 조건이고, c는 between조건으므로 a,b가 c보다 앞에 배치된다 94 | 95 | - a,b사이와 d,e사이는 어떤게 앞에 오더라도 상관없다 96 | 97 | -> 효율적인 인덱스 순서 : abcde, abced, bacde, baced 98 | 99 | - 인덱스 중복 제거 100 | 101 | - 실습1 102 | 103 | - 상황 104 | 105 | X01: a + b 106 | 107 | X02: a + b + c 108 | 109 | X03: a + b + c + d 110 | 111 | - 해결 112 | 113 | X03만 남기고 X01, X02는 제거해도 된다 114 | 115 | - 실습2 116 | 117 | - 상황 118 | 119 | X01: a + b 120 | 121 | X02: a + c 122 | 123 | X03: a + d 124 | 125 | X04: a + e 126 | 127 | (+) a의 평균 카디널리티가 매우 낮다 128 | 129 | - 해결 130 | 131 | 카디널리티가 매우 낮다면 사실상 중복이다. 카디널리티가 5라면 a 를 =조건으로 조회하면 평균 5건이 남으니까 인덱스를 4개씩 만들 필요가 없다. 모두 제거하고 다음 인덱스 하나만 남겨도 된다 132 | 133 | X01: a + b + c + d + e 134 | 135 | - 실습3 136 | 137 | - 상황 138 | 139 | PK: a + b + c 140 | 141 | N1: d + a 142 | 143 | N2: f + b 144 | 145 | N3: a + e 146 | 147 | N4: a + d 148 | 149 | 밑줄(a, f) : 항상 범위조건으로만 조회 150 | 151 | 컬럼에 입력된 값 종류 개수 : a(2356), b(127), c(1850), d(5956), e(1715), f(2356) 152 | 153 | - 해결1 154 | 155 | a가 항상 범위조건이면 N3와 N4는 둘다 a가 인덱스 액세스 조건이다. 156 | 157 | 그래서 N4를 지우고 N3 맨뒤에 d를 추가한다 158 | 159 | - 해결2 160 | 161 | d와 a로 조회하거나 d단독으로 조회할때는 N1을 사용하고, a만으로 조회할때는 N3을 쓰면 되니까 N4만 제거해도 된다 162 | 163 | - 해결3 164 | 165 | PK: b + a + c 166 | 167 | N1: d + a 168 | 169 | N2: f + b 170 | 171 | N3: a + e 172 | 173 | ~~N4: a + d~~ 174 | 175 | - 실습4 176 | 177 | - 상황 178 | 179 | PK: a + b + c + d 180 | 181 | N1: e + d 182 | 183 | N2: d 184 | 185 | N3: a + d 186 | 187 | 컬럼에 입력된 값 종류 개수 : a(736000), b(175), c(3000), d(250000), e(3) 188 | 189 | - 해결 190 | 191 | e의 NDV가 매우 낮기 때문에 e로만 조회할 땐 N1인덱스가 사용되지 않는다 192 | 193 | N2를 제거하고 N1의 e와 d의 순서를 바꾼다 -------------------------------------------------------------------------------- /3장_인덱스_튜닝/3.3_최락준.md: -------------------------------------------------------------------------------- 1 | # 3.3.9 BETWEEN과 LIKE 스캔 범위 비교 2 | 3 | ### 결론 4 | 5 | 범위 표현을 위해서는 LIKE 보다는 BETWEEN 연산자를 사용하자. 6 | 7 | ### 원인 8 | 9 | 범위 조건에서 LIKE와 BETWEEN 중 어떤 것을 사용하는가에 따라 스캔 범위가 달라진다. 10 | 11 | ### 예시 12 | between 의 경우 13 | ```sql 14 | WHERE 판매월 BETWEEN '201901' AND '201912' 15 | AND 판매구분 = 'B' 16 | ``` 17 | * 201901 이면서 **B인 레코드에서 스캔을 시작**할 수 있다.(201901 이면서 A인 레코드 생략 가능) 18 | 19 | like 의 경우 20 | ```sql 21 | WHERE 판매월 LIKE '2019%'; 22 | AND 판매구분 = 'B' 23 | ``` 24 | * **201900 이 스캔 시작점**이 되기 때문에 201901이면서 A인 레코드도 전부 스캔해야 함. 25 | 26 | *스캔 종료 지점을 정하는 것 역시 BETWEEN이 더 유리하다.(ex. 판매구분이 A일 경우)* 27 | 28 | ## 3.3.10.1 범위검색 조건을 남용할 때 생기는 비효율 - LIKE 29 | 30 | ### 결론 31 | 옵션 조건을 처리할 때 LIKE 연산자를 사용하지 말자. 32 | 33 | ### 예시 34 | (1) 올바른 예 35 | 지역코드는 옵션조건일 때, 지역코드의 유무에 따라 아래의 두 쿼리 중 하나가 실행된다. 36 | ```sql 37 | 쿼리1 38 | SELECT * FROM 가입상품 39 | WHERE 회사코드 =:COM 40 | AND 지역코드 =:REG -- 지역코드가 조건절에 추가됨 41 | AND 상품명 LIKE :PROD || '%' 42 | 43 | 쿼리2 44 | SELECT * FROM 가입상품 45 | WHERE 회사코드 =:COM 46 | AND 상품명 LIKE :PROD || '%' 47 | ``` 48 | 49 | ![](images/3-43.jpg) 50 | 51 | 왼쪽은 회사코드, 지역코드, 상품명에 'C70', '02', '보급'을 입력한 경우이고(쿼리1) 52 | 오른쪽은 이 중 지역코드를 입력하지 않은 경우이다.(쿼리2) 53 | 54 | * 왼쪽은 **회사코드, 지역코드, 상품명이 모두 액세스 조건**이 되어 스캔 범위가 상당히 줄었다. 55 | * 오른쪽은 회사코드만 액세스 조건이 되어 스캔범위가 넓지만 왼쪽과 오른쪽의 스캔범위 평균은 비교적 적은 것을 알 수 있다. 56 | 57 | (2) 잘못된 예 58 | 59 | 지역코드를 LIKE 연산자로 처리할 때 쿼리는 다음과 같다. 60 | ```sql 61 | SELECT * FROM 가입상품 62 | WHERE 회사코드 =:COM 63 | AND 지역코드 LIKE :REG || '%' -- 옵션 조건을 LIKE로 처리 64 | AND 상품명 LIKE :PROD || '%' 65 | ``` 66 | ![](images/3-44.jpg) 67 | 68 | * 지역코드를 입력했을 때의 스캔 범위가 별로 줄어들지 않는다. **회사코드, 지역코드만 액세스 조건**이기 때문이다. 69 | 70 | ## 3.3.10.1 범위검색 조건을 남용할 때 생기는 비효율 - BETWEEN 71 | 72 | ### 결론 73 | 옵션 조건을 처리할 때 BETWEEN 연산자를 사용하지 말자. 74 | 75 | ### 예시 76 | ```sql 77 | SELECT * FROM 일별종목거래 78 | WHERE 거래일자 BETWEEN :시작일자 AND :종료일자 79 | AND 종목코드 BETWEEN :종목1 AND :종목2 80 | ``` 81 | 위의 쿼리에서 82 | 거래일자가 옵션 조건일 경우 시작일자를 '1900'으로 하고 종료일자를 '2999'로 지정한다면 83 | 전체범위를 모두 스캔하는 것이다. 매우 비효율적이다. 84 | 85 | ## 3.3.11 다양한 옵션 조건 처리 방식의 장단점 비교 86 | 87 | ### (1). OR 조건 활용 88 | 89 | ```sql 90 | SELECT * FROM 거래 91 | WHERE (:CUST_ID IS NULL OR 고객ID = :CUST_ID) 92 | AND 거래일자 BETWEEN :DT1 AND :DT2 93 | ``` 94 | 95 | * 위의 쿼리의 경우 고객ID가 선두 컬럼이기 때문에 OR Expansion이 작동하지 않는다. 따라서 '고객ID + 거래일자'로 구성된 인덱스를 사용할 수 없다. 96 | * 인덱스를 '거래일자 + 고객ID'로 구성하더라도, 고객ID를 테이블 액세스 단계에서 필터링하기 때문에 비효율적이다. 97 | 98 | ### OR 조건을 사용할 수 있는 경우 99 | 100 | * 인덱스 액세스 조건으로 사용 불가 101 | * 인덱스 필터 조건으로도 사용 불가 102 | * 테이블 필터 조건으로만 사용 가능 103 | * 인덱스 구성 컬럼 중 하나 이상이 Not Null 일 경우 인덱스 필터 조건으로 사용 가능(18C 버전이상) 104 | 105 | ### (2). LIKE/BETWEEN 조건 활용 106 | 107 | LIKE/BETWEEN 패턴을 사용할 수 있는 경우 108 | 109 | 1). 인덱스 선두 컬럼으로 사용x 110 | 111 | 인덱스가 '고객ID + 거래일자'일 경우 112 | ```sql 113 | SELECT * FROM 거래 114 | WHERE 고객ID LIKE :cust_id || '%' 115 | AND 거래일자 BETWEEN :dt1 AND :dt2 116 | ``` 117 | * 고객ID를 입력하지 않을 경우 **모든 고객에 대한 레코드**를 거래일자로 필터링함. 118 | 119 | 2). null 허용 컬럼에서 사용x 120 | 121 | * 위의 sql에서 :cust_id 대신 null이 들어갈 경우 실제로 null인 데이터는 결과에서 누락된다. 122 | 123 | ```sql 124 | where null LIKE null || '%' 125 | 선택된 레코드가 없습니다. 126 | ``` 127 | 128 | 3) 인덱스 액세스 조건이 숫자형 컬럼인 경우 사용x 129 | 130 | * 인덱스 액세스 조건이 숫자형일 경우 like를 사용하면 자동으로 형변환이 일어난다.(문자열로) 131 | * 가공한 컬럼에 대해서는 인덱스 스캔이 발생하지 않는다. 132 | 133 | 4) 가변 길이 컬럼의 경우 사용x 134 | 135 | * '김훈'을 입력한 경우 '김훈남'과 같이 김훈이 들어가는 다른 길이의 레코드까지 조회하면서 스캔 범위가 넓어진다. 136 | 137 | ### (3). UNION ALL 활용 138 | 139 | ```sql 140 | SELECT * FROM 거래 141 | WHERE :cust_id IS NULL -- 고객ID 조건이 없을 경우 142 | AND 거래일자 BETWEEN :dt1 AND :dt2 143 | UNION ALL 144 | SELECT * FROM 거래 145 | WHERE :cust_id IS NULL 146 | AND 고객ID = :cust_id -- 고객ID 조건이 있을 경우 147 | AND 거래일자 BETWEEN :dt1 AND :dt2 148 | ``` 149 | * :cust_id 변수 입력 여부와 상관 없이 인덱스를 사용할 수 있다. 150 | * 단점을 쿼리가 길어진다는 것이다. 151 | 152 | ### (4). NVL/DECODE 함수 활용 153 | 154 | ```sql 155 | SELECT * FROM 거래 156 | WHERE 고객ID = nvl(:cust_id, 고객ID) 157 | AND 거래일자 BETWEEN :dt1 AND :dt2 158 | ``` 159 | * 이 방법을 이용하면 :cust_id가 null일 경우 거래일자를 인덱스로 사용하고, :cust_id가 null이 아닐 경우 고객ID + 거래일자를 인덱스로 사용한다. 160 | * 단점은 앞서 나온 LIKE 패턴처럼 NULL 허용 컬럼에서 사용할 수 없다. 161 | 162 | ### (5). 옵션 조건 처리 방식일 때 개인적인 결론 163 | 164 | * OR / LIKE / BETWEEN 을 사용하지 않는 것이 좋다. 사용해야만 하는 상황이면 위의 사항을 고려해야 한다. 165 | * Mybatis와 같은 동적 SQL을 사용하면 위의 문제를 해결할 수 있다. 166 | * JPA와 같은 ORM이라면 어플리케이션 레벨에서 분기처리 해주는 것이 좋을 것 같다. 167 | 168 | ## 3.3.12 함수호출부하 해소를 위한 인덱스 구성 169 | 170 | PL/SQL 함수가 느린 가장 큰 이유는 Recursive call 때문이다. 171 | 이 방식은 결과 로우가 100만 건이면 PL/SQL 함수를 100만 번 실행시킨다. 172 | 173 | 이러한 함수호출부하를 줄이는 것은 결국 **액세스 조건을 고려한 인덱스 구성**을 하는 것이다. 174 | ```sql 175 | 176 | select * from 회원 177 | where 암호화된_전화번호 = encryption(:phone_no) 178 | ``` 179 | * 위 방식은 데이터가 100만 건일 경우 함수가 100만 번 호출 된다. 180 | 181 | ```sql 182 | 183 | select * from 회원 184 | where 생년 = '1987' 185 | and 암호화된_전화번호 = encryption(:phone_no) 186 | ``` 187 | 생년 + 암호화된_전화번호로 구성된 인덱스일 경우 액세스 조건에 따라 필터링 된 횟수만큼만 함수가 호출된다. -------------------------------------------------------------------------------- /3장_인덱스_튜닝/3장_인덱스_튜닝.md: -------------------------------------------------------------------------------- 1 | ## 3장. 인덱스 튜닝 2 | 3 | ### 3.1 테이블 액세스 최소화 4 | 5 | - 인덱스 ROWID 6 | 7 | - 인덱스 ROWID는 논리적 주소다 8 | - 물리적 주소인 포인터와는 다르다 9 | - ROWID에 의한 테이블 액세스는 생각보다 고비용 연산이다 10 | 11 | - 특정 컬럼 기준으로 같은 값을 갖는 데이터가 모여있을수록 (클러스터링 팩터가 좋은 컬럼에 인덱스를 생성하면) 데이터를 찾는 속도가 빠르다 12 | 13 | - 읽어야 할 데이터가 일정량을 넘는 순간(인덱스 손익분기점이 넘는 순간) Table Full Scan보다 Index Range Scan이 오히려 느려진다 14 | 15 | - Table Full Scan은 시퀀셜 엑세스인데, 인덱스 ROWID를 이용한 테이블 액세스는 랜덤 액세스 방식이기 때문에 16 | - Table Full Scan은 Multiblock I/O인데, 인덱스 ROWID를 이용한 테이블 액세스는 Single Block I/O이기 때문에 17 | 18 | - 인덱스 클러스터링 팩터(CF)에 따라 인덱스 손익분기점이 달라진다 19 | 20 | - 인덱스 CF가 좋을수록 인덱스 손익분기점이 높다 21 | - = 물리적으로 근접해있는 컬럼을 기준으로 인덱스를 생성하면 많은 데이터를 Index Range Scan으로 가져와도 Table Full Scan보다 느려질 가능성이 적어진다 22 | 23 | - 기존에 있는 인덱스에 컬럼을 추가함으로써 테이블 액세스를 줄일수 있다 24 | 25 | - 예를 들어 a,b 컬럼으로 이루어진 인덱스가 있는 테이블에서 아래 쿼리를 시도한다고 가정할때 26 | 27 | - ```sql 28 | select * 29 | from table_name 30 | where a = 30 and c >= 2000; 31 | ``` 32 | 33 | - 기존의 인덱스에 c 컬럼을 추가하면 인덱스 스캔양은 그대로지만 테이블 액세스양을 줄일 수 있다. 34 | 35 | - 이전의 경우엔 where로 걸러지는 양이 많을때일 경우의 튜닝 방법이다. 그럼 걸러지는 레코드가 거의 없다면? 36 | 37 | - 예를 들어 아래의 경우 걸러지는 레코드가 없다 (부서번호로 인덱스가 생성되어 있음) 38 | 39 | - ```sql 40 | select 부서번호, sum(수량) 41 | from 판매집계 42 | where 부서번호 like '12%' 43 | group by 부서번호; 44 | ``` 45 | 46 | - 이럴 경우엔 쿼리에 사용된 컬럼(수량)을 모두 인덱스에 추가해서 테이블 액세스가 아예 발생하지 않도록 튜닝할 수 있다 47 | 48 | - 모든 컬럼이 인덱스에 포함되어 있어서 인덱스만 읽어서 처리하는 쿼리를 Covered 쿼리라고 하고, 그 인덱스를 Covered 인덱스라고 함 49 | 50 | - 이 방법은 효과는 좋지만 추가해야 할 컬럼이 많아지면 추가하기 곤란한 경우도 많다 51 | 52 | ### 3.2 부분범위 처리 활용 53 | 54 | - 부분범위 처리를 활용하면 인덱스로 액세스할 대상 레코드가 아무리 많아도 아주 빠른 응답속도를 낼 수 있다 55 | - 처음부터 모두 가져오지 않고 일정량(array size)만 요청한다(fetch call) 56 | - 멈출 수 있어야 의미있는 부분 범위 처리다. 57 | - 전체를 읽어서 정렬한다던가 하는 작업이 있다면 앞쪽 일부만 출력할 수 없다. 58 | - 앞쪽 일부만 읽고 반환해도 되도록 인덱스를 구성해야 한다 59 | 60 | ### 3.3 인덱스 스캔 효율화 -------------------------------------------------------------------------------- /3장_인덱스_튜닝/3주_김민석.md: -------------------------------------------------------------------------------- 1 | # 3장 인덱스 튜닝 2 | 3 | # 3.1 테이블 엑세스 최소화 4 | 5 | 6 | SQL 튜닝은 랜덤I/O를 줄이는 것. 7 | 8 | 랜던I/O를 최소화 하는 방법을 알아보자. 9 | 10 | 11 | ## 3.1.1 테이블 랜덤 액세스 12 | 13 | - 아무리 데이터가 많아도 인덱스를 사용하면 금방 조회된다 14 | - 어쩔때는 테이블 전체를 스캔하는 것보다 인덱스가 더 느릴때도 있다. 15 | 16 | 왜 그럴까? 이 질문에 대한 대답을 찾아보자. 17 | 18 | ![image](https://user-images.githubusercontent.com/6725753/142974807-c2871c4f-b139-4714-94d6-0c5b22be7838.png) 19 | 20 | 인덱스를 사용하는 이유는 ROWID(테이블에 있는 레코드를 찾기 위한 주소값)를 얻는데에 있다. 21 | 22 | 즉 인덱스를 사용하면 데이터가 실제로 있는 물리 주소가 아닌 물리 주소를 찾을 수 있는 23 | 주소를 얻는다. 반대로 테이블 Full Scan 시에는 직접적인 물리 주소를 얻기 때문에 위와 같은 문제점들이 나타난다. 24 | 25 | 26 | - ROWID 27 | - 논리적 주소(즉 메모리 주소가 아니다) 28 | - 디스크 상에서 테이블 레코드를 찾아가기 위한 위치 정보를 담고 있다 29 | - 즉, DBA(데이터파일번호 + 블록번호)를 가지고 있다. 30 | 31 | ![image](https://user-images.githubusercontent.com/6725753/142974975-ea4d4d7f-7561-4122-96e2-5c0237d8726d.png) 32 | 33 | 34 | - I/O 메커니즘 35 | - 인덱스를 통해 ROWID를 찾고 그 안에서 DBA를 읽어 온다 36 | - 캐시 검색 37 | - DBA를 해시 함수에 넣어서 해시 체인을 찾는다 38 | - 해시 체인 안에서 버퍼 헤더를 찾는다 39 | - 버퍼 해더가 있다면 hit 40 | - 캐시에 없다면 DBA를 통해 디스크 램덤 I/O 41 | 42 | `데이터가 캐싱돼있더라도 매번 DBA해싱과 래치 획득 과정을 반복한다. 동시 액세스가 심할때는 캐시버퍼 채인 래치와 버퍼 Lock 경합까지 발생. ROWID를 통한 테이블 액세스는 생각보다 고비용 구조` 43 | 44 | 또한, 이러한 구조는 메인메모리DB와 비교할 때 히트레이트가 아무리 높아도 메모리DB의 속도를 따라갈 수 없는 구조. 45 | 46 | 47 | ## 3.1.2 인덱스 클러스터링 팩터 48 | 49 | ![image](https://user-images.githubusercontent.com/6725753/142975085-278c04e6-308f-4aea-83bf-0c6815f4d929.png) 50 | CF 좋은 케이스 51 | 52 | 53 | ![image](https://user-images.githubusercontent.com/6725753/142975162-67e3ef6f-945a-4323-8060-3d2b655b9068.png) 54 | 55 | CF 안좋은 케이스 56 | 57 | - 클러스터링 팩트 58 | - 특정 컬럼을 기준으로 같은 값을 갖는 데이터가 서로 모여있는 정도. 59 | - 예) "거주지역 = 제주"에 해당하는 데이터가 물리적으로 근접해 있으면 데이터를 찾는 속도가 빠름 60 | - 가능한 이유는 오라클은 버퍼 Pinning 기능을 통해 래치 획득과 해시 체인 스캔 과정을 통해 찾아간 테이블 블록에 대한 포인터를 바로 해제하지 않고 가지고 있기 때문. 61 | - 따라서 인덱스는 레인지 스캔을 하기 때문에 비슷한 데이터가 같은 블록에 위치한다면 바로 물리적인 주소를 획득할 수 있다 62 | - ![image](https://user-images.githubusercontent.com/6725753/142975259-dc0c590f-3078-4d8c-8847-64a64abc3dee.png) 63 | 64 | 65 | ## 3.1.3 인덱스 손익 분기점 66 | 67 | 위에서 나왔듯이 인덱스 ROWID를 이용한 데이터 액세스는 생각보다 고비용 구조다. 따라서 읽어야 할 데이터가 일정량을 넘는 순간, 테이블 전체를 스캔하는 것보다 오히려 느려진다. 이 점을 손익분기점이라고 한다. 68 | 69 | - 손익분기점이 발생하는 두가지 이유 70 | - Table Full Scan은 시퀀셜 액세스 VS. 인덱스 ROWID 방식은 랜덤 액세스 71 | - Table Full Scan은 Multi Block I/O VS. 인덱스 ROWID 방식은 Single Block I/O 72 | 73 | ![image](https://user-images.githubusercontent.com/6725753/142975346-d529766b-06f7-469b-8159-c4e1216a7eec.png) 74 | 75 | 76 | - 인덱스 손익분기점과 버퍼캐시 히트율 77 | - 데이터량이 많아질수록 손익분기점은 더 낮아진다 78 | - 데이터량이 많아질수록 캐시 히트율이 낮아지기 때문이다. 79 | - 데이터량이 많아질수록 CF가 안좋아지기 때문이다. 80 | - 따라서, 손익분기점을 보고 인덱스를 쓸지 테이블 전체 스캔을 할지 결정해야 한다.(항상 인덱스 방식이 좋은건 아니다!!) 81 | 82 | ### 온라인 프로그램 튜닝 vs. 배치 프로그램 튜닝 83 | 84 | - 온라인프로그램 => 소량의 데이터를 읽거나 갱신 => 인덱스와 NL 조인 유리 85 | - 배치 프로그램 => 한번에 전체 데이터를 처리 => Full Scan과 해시 조인 유리 86 | - Full Scan 튜닝을 위하여 파티셔닝과 병렬처리 가능 87 | 88 | ## 3.1.4 인덱스 컬럼 추가 89 | 90 | 인덱스에 컬럼을 추가하는 튜닝 기법. 91 | 92 | ![image](https://user-images.githubusercontent.com/6725753/142975458-29d4bd46-3af4-4b54-a534-52bedc579d43.png) 93 | 94 | ![image](https://user-images.githubusercontent.com/6725753/142975510-cdac996b-1cf5-4450-bc1c-811b6fa4afe6.png) 95 | 96 | 쓸데없이 테이블을 여섯번이나 엑세스. 97 | 98 | 인덱스를 새로 만드는건 매우 비효율적일 수 있기 때문에 컬럼을 하나 추가하는 것으로 튜닝. 99 | 100 | ![image](https://user-images.githubusercontent.com/6725753/142975588-841d2fb4-7450-4fc8-90d2-63bb69547734.png) 101 | 102 | 테이블 액세스 횟수가 1회로 준다. 103 | 104 | 105 | - 실제 사례 학습 106 | 107 | ![image](https://user-images.githubusercontent.com/6725753/142975665-ffe03910-009d-4c46-aee8-d00e928f9b8a.png) 108 | 109 | ![image](https://user-images.githubusercontent.com/6725753/142976525-ab48a2eb-8d83-4699-89f1-053c19b2051e.png) 110 | 111 | '서비스 번호' 단일 컬럼으로 구성된 인덱스를 사용하기 때문에 매우 비효율적인 액세스 발생. 112 | 113 | 이 인덱스에 '사용 여부' 컬럼 추가. 114 | 115 | ![image](https://user-images.githubusercontent.com/6725753/142976775-334d58f6-e339-45bf-b8ba-22b9cb72a7a9.png) 116 | 117 | 118 | ## 3.1.5 인덱스만 읽고 처리 119 | 120 | 필터 조건에 의해 버려지는 레코드가 거의 없는데도 불구하고 속도를 개선해야 하는 상황이라면? 121 | 122 | 이때는 쿼리에 사용된 컬럼을 모두 인덱스에 넣어서 테이블 액세스가 아예 발생하지 않도록 할 수 있다. 123 | 124 | 이를 Covered 쿼리하고 한다. 그리고 이때 사용한 인덱스를 Covered 인덱스라 한다. 125 | 126 | 127 | ### include 인덱스 128 | 129 | Oracle에는 없지만 SQL Server 2005 버전에 추가된 기능. 130 | 131 | 미리 지정한 컬럼을 리프 레벨에만 함께 저장하여 효율을 높이는 방식. 132 | 133 | `create index emp_x01 on emp (deptno) include (sal)` 134 | 135 | 리프 레벨에만 포함되기 때문에 수직적 탐색에는 당연히 사용될 수 없고 테이블 랜덤 엑세스를 줄이는 용도로만 사용된다. 136 | 137 | 138 | ### 3.1.6 인덱스 구조 테이블 139 | 140 | ROWID 방식이 고비용 구조이기 때문에 테이블 자체를 인덱스 형태로 구성한 방식. 141 | 142 | 오라클에서는 IOT(Index-Organized Table)이라 부르고 143 | 144 | SQL Server에서는 클러스터형(Clustered) 인덱스라 부른다. 145 | 146 | ![image](https://user-images.githubusercontent.com/6725753/142976860-1d2eb063-0cd9-4eb0-acd5-50613ad51e91.png) 147 | 148 | IOT에서는 인덱스 리프 블록이 곧 데이터 블록. 149 | 150 | 만드는 방법 151 | 152 | ![image](https://user-images.githubusercontent.com/6725753/142976942-7d7aeee3-108d-418e-90cd-88197459be16.png) 153 | 154 | - 장점 155 | - CF가 매우 좋다. 100% 156 | - 따라서 BETWEEN이나 부등호 조건으로 넓은 범위를 읽을 때 유리 157 | - 데이터 입력과 조회 패턴이 서로 다른 테이블에도 유용 158 | - 책의 예처럼 강제로 CF를 좋게하여 인덱스를 쓰더라도 느린 경우에 속도를 올릴 수 있다. 159 | 160 | 예) 사원 100명. 실적등록은 일자별로 입력. 한블록에 100개씩 하루에 한블록 생성. 조회는 사원번호순으로 진행. 인덱스를 걸더라도 1년치를 조회하면 365 블럭에 모두 액세스. 하지만 사번으로 IOT구성하면 CF가 좋아져서 4번 액세스로 모든 값 가져올 수 있음. 161 | 162 | 163 | ## 3.1.7 클러스터 테이블 164 | 165 | ### 인덱스 클러스터 테이블 166 | 167 | ![image](https://user-images.githubusercontent.com/6725753/142977034-664d7bea-0795-4f03-9b50-d32276c01fbe.png) 168 | 169 | 키값이 같은 레코드를 같은 블럭에 저장. 블록을 넘어서면 체인으로 묶는다. 170 | 171 | 여러 테이블을 같은 블록에 저장할 수 있는데 이를 다중 테이블 클러스터라 한다. 172 | 173 | 클러스터를 생성하고 인덱스도 필수적으로 생성한다. 데이터 저장위치를 판별하기 위해서다. 174 | 175 | 176 | `create cluster c_dept# (deptno number(2)) index;` 177 | 178 | `create index c_dept#_idx on cluster c_dept#;` 179 | 180 | ![image](https://user-images.githubusercontent.com/6725753/142977149-68e3fc99-f148-4df5-9dae-6a64f6fac3b8.png) 181 | 182 | 클러스터 인덱스도 B-Tree로 구성되지만 리프노드는 해당 키 값을 저장하는 첫번째 테이터 블록을 가리킨다. 따라서 인덱스와 레코드의 관계가 1:M 이다. 그리고 키값이 항상 유니크하다. 183 | 184 | ![image](https://user-images.githubusercontent.com/6725753/142977193-92c8ec1d-a82f-47f3-adc3-64d056d25d1d.png) 185 | 186 | 또한, 랜덤 액세스가 값 하나당 한번씩만 발생하고(체인 스캔 제외) 클러스터에 도달해서는 시퀀셜로 스캔하기 때문에 넓은 범위를 읽더라도 효율적이다. 187 | 188 | 189 | 190 | ### 해시 클러스터 테이블 191 | 192 | 인덱스 대신에 해시를 사용한다. 193 | 194 | ![image](https://user-images.githubusercontent.com/6725753/142977238-3ad6e7f5-3bb2-4866-a55b-adb44056ed6c.png) 195 | 196 | -------------------------------------------------------------------------------- /3장_인덱스_튜닝/4주_김민석.md: -------------------------------------------------------------------------------- 1 | # 3장 인덱스 튜닝 2 | 3 | ## 3.3 인덱스 스캔 효율화 4 | 5 | ### 3.3.5 인덱스 선행 컬림이 등치(=) 조건이 아닐 때 생기는 비효율 6 | 7 | - 인덱스 스캔 효율성은 인덱스 컬럼을 조건절에 모두 등치(=) 조건을 사용할 때 가장 좋다. 8 | 9 | ### 3.3.6 BETWEEN을 IN-List로 전환 10 | 11 | - 선행 조건이 BETWEEN으로 비효율이 발생할 때 In-List로 바꿔주면 큰 효과를 얻을 수도 있다. 12 | - 이때 주의 할 점은 IN-List 개수가 많지 않아야 한다.(수직적 탐색이 많아 질 수 있음) 13 | 14 | ### 3.3.7 Index Skip Scan 활용 15 | 16 | - BETWEEN을 IN-List로 변환하는 것 대신 Index Skip Scan을 활용하는 방법도 있다. 17 | 18 | ### 3.3.8 IN 조건은 '=' 인가 19 | 20 | - IN 조건은 '='이 아니다. 21 | 22 | ### 3.3.9 BETWEEN과 LIKE 스캔 범위 비교 23 | 24 | - LIKE보다 BETWEEN이 낫다. 25 | 26 | ### 3.3.10 범위검색 조건을 남용할 때 생기는 비효율 27 | 28 | - 코딩을 쉽게 하려고 인덱스 컬럼에 범위검색 조건을 남용하지 말라. 29 | 30 | ### 3.3.11 다양한 옵션 조건 처리 방식의 장단점 비교 31 | 32 | - 인덱스 선두 컬럼에 대한 옵션 조건에 OR 조건을 사용하지 말라 33 | - 변별력이 좋은 필수 조건이 있는 상황에서는 LIKE/BETWEEN 사용 가능 34 | - UNION ALL 방식은 옵션 조건 컬럼도 인덱스 액세스 조건으로 사용한다 35 | - NVL/DECODE 사용하면 UNION ALL 보다 단순하면서도 비슷한 성능을 낼 수 있다. 36 | 37 | ### 3.3.12 함수호출부하 해소를 위한 인덱스 구성 38 | 39 | - PL/SQL은 매우 느리다. 40 | - 특히 안에서 SQL을 실행하면 Recursive call이 일어나기 때문에 매우 비효율적이다. 41 | 42 | 43 | ## 3.4 인덱스 설계 44 | 45 | ### 3.4.1 인덱스 설계가 어려운 이유 46 | 47 | - 인덱스가 많으면 생기는 문제 48 | - DML 성능 저하 49 | - 데이터베이스 사이즈 증가 50 | - 테이터베이스 관리 및 운영 비용 상승 51 | 52 | ### 3.4.2 가장 중요한 두 가지 선택 기준 53 | 54 | - 인덱스 선택 기준 55 | - 조건절에 항상 사용하거나, 자주 사용하는 컬럼을 선정하다. 56 | - '=' 조건으로 자주 조회하는 컬럼을 앞에 둔다. 57 | 58 | ### 3.4.3 스캔 효율성 이외의 판단 기준 59 | 60 | - 수행 빈도 61 | - 업무상 중요도 62 | - 클러스터링 팩터 63 | - 데이터량 64 | - DML 부하 65 | - 저장 공간 66 | - 인덱스 관리 비용 67 | 68 | ### 3.4.4 공식을 초월한 전략적 설계 69 | 70 | 모든 조건절을 고려하여 인덱스를 다 만들수는 없다. 도메인에 맞게 전략을 구성하여 최적 인덱스를 구성한다. 71 | 72 | ### 3.4.5 소트 연산을 생략하기 위한 컬럼 추가 73 | 74 | 조건절에 사용하지 않는 컬럼이더라도 소트 연산을 생략할 목적으로 인덱스 구성에 포함시켜서 성능 개선 도모 75 | 76 | ### 3.4.6 결합 인덱스 선택도 77 | 78 | 인덱스 생성 여부를 결정할 때는 선택도가 충분히 낮은지가 중요한 판단기준이다. 79 | 80 | ### 3.4.7 중복 인덱스 제거 81 | 82 | 중복되는 인덱스는 제거한다. 중복으로 보이지 않는다 하더라도 평균 카디널리티가 낮다면 사실상 중복으로 본다. 83 | 84 | ### 3.4.8 인덱스 설계도 작성 85 | 86 | 인덱스 설계에도 전체를 조망할 수 있는 설계도면이 필요한다. 87 | -------------------------------------------------------------------------------- /3장_인덱스_튜닝/images/3-43.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/3장_인덱스_튜닝/images/3-43.jpg -------------------------------------------------------------------------------- /3장_인덱스_튜닝/images/3-44.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/3장_인덱스_튜닝/images/3-44.jpg -------------------------------------------------------------------------------- /3장_인덱스_튜닝/조수진.md: -------------------------------------------------------------------------------- 1 | # 3.1 테이블 액세스 최소화 2 | 3 | ### 3.1.1 테이블 랜덤 액세스 4 | 5 | - 인덱스로 검색해도 빠른데 왜 파티셔닝을 할까? 6 | - 인덱스로 검색하는데 왜 느릴까? 7 | 8 | ### ROWID 9 | 10 | - 커버링 인덱스가 아닌 경우 원하는 자료를 찾기 위해 ROWID로 테이블에 액세스 해야한다 11 | - 인덱스의 ROWID는 포인터(물리 주소)가 아니다. 논리 주소이다!(메모리 DB 인덱스는 포인터) 12 | 13 | ### 인덱스를 이용한 I/O 14 | 15 | - ROWID를 분해해서 DBA 정보 확인 16 | - 해싱 결과로 해시 체인 찾기(래치 획득) 17 | - 해시 체인 내부에서 버퍼 헤더 찾기(버퍼 캐시의 메모리 주소) 18 | - 버퍼 헤더가 있으면 포인터로 버퍼 블록 찾아가기(&pinning 포인터 유지) 19 | - 버퍼 헤더가 없으면 디스크에서 데이터 파일 → 블록 → 레코드 찾아가서 캐시에 로딩 & 결과 리턴 20 | 21 | → 버퍼 캐시에서 테이블 블록이 수시로 밀려났다 다시 캐싱됨 22 | 23 | ⇒ 인덱스에서 포인터 사용할 수 X 24 | 25 | ⇒ 인덱스 ROWID를 이용한 테이블 액세스는 생각보다 고비용이다! 26 | 27 | ![Untitled](/img/pic3-3.png) 28 | 29 | ### Table Full Scan I/O 30 | 31 | - 테이블 세그먼트 헤더에 익스텐트 맵 확인 32 | - 익스텐트 맵을 통해 각 익스텐트의 첫 번째 블록 DBA를 알 수 있음 33 | - 익스텐트 내부의 블록은 연결되어 있으므로 시퀀셜하게 읽으면됨 34 | 35 | ### 3.1.2 인덱스 클러스터링 팩터 36 | 37 | 특정 컬럼을 기준으로 같은 값을 갖는 데이터가 서로 모여있는 정도를 의미 = 군집성 계수 38 | 39 | 익덱스 레코드 정렬 순서와 실제 테이블 정렬 순서가 비슷하면 테이블 액세스량에 비해 블록 I/O가 적게 발생함 40 | 41 | → 버퍼 Pinning(인덱스 ROWID로 테이블 액세스 할 때, 래치 획득과 체인 스캔 후 블록에서 데이터를 읽고 포인터 그대로 유지) 42 | 43 | ### 3.1.3 인덱스 손익분기점 44 | 45 | ROWID로 테이블 액세스는 고비용 구조 46 | 47 | → 읽어야 할 데이터가 일정량 이상 넘어가면 테이블 풀스캔이 효과적일수도 48 | 49 | ⇒ Index Range Scan에 의한 테이블 액세스가 Table Full Scan보다 느려지는 지점을 인덱스 손익분기점 50 | 51 | - 보통 10~100만건 이내 테이블에서 손익 분기점은 5~20% 수준, CF에 따라 크게 달라진다 52 | - 테이블 풀스캔은 1000건을 조회하든, 1000만건을 조회하든 성능이 거의 일정하다 53 | 54 | → 시퀀셜 액세스, Multiblock I/O라서 55 | 56 | 57 | ### OLTP 튜닝 58 | 59 | - 소량 데이터를 읽고 갱신하므로 인덱스를 효과적으로 활용하는 것이 중요 60 | - NL 조인 사용 61 | - 인덱스로 소트 연산 생략 62 | 63 | ### 배치 프로그램 튜닝 64 | 65 | - 전체범위 처리 기준으로 튜닝해야 한다 66 | - 처리 대싱 집합 중 일부를 빠르게 처리하는 것이 아니라 전체를 빠르게 처리하는 것을 목표로 67 | - 대량 데이터를 빠르게 처리하려면 Full Scan과 해시 조인이 유리 68 | - 초대용량 테이블을 Full Scan하면 오래 걸리기 때문에 배치 프로그램은 파티션 활용 전략이 매우 중요한 튜닝 요소 69 | - 파티셔닝은 Full Scan을 빠르게 처리하기 위함 70 | 71 | ### 3.1.4 인덱스 컬럼 추가 72 | 73 | 테이블 액세스 최소화를 위해 가장 일반적으로 사용하는 튜닝 기법은 인덱스에 컬럼 추가 하는 것 74 | 75 | - 인덱스 스캔 후, 테이블 액세스 과정에서 필터되는 데이터가 많은 경우 사용 76 | 77 | cr - Consistent Read(Consistent Read Mode로 버퍼 캐시에서 특정 블록을 읽는 것) 78 | 79 | pr - Physical Read(특정 블록을 읽어서 버퍼 캐시에 로딩) 80 | 81 | ### 3.1.5 인덱스만 읽고 처리 82 | 83 | ### Covered 쿼리, Covered 인덱스 84 | 85 | 쿼리에 사용되는 컬럼을 모두 인덱스에 추가해서 테이블 액세스가 아예 발생하지 않도록 하는 방법 86 | 87 | ### Include 인덱스 88 | 89 | 인덱스 키 외에 미리 지정한 컬럼을 리프 레벨에 함께 저장하는 기능(최대 1023 컬럼까지 지정 가능) 90 | 91 | - 커버링 인덱스랑 차이점: 커버링 인덱스와 달리 수직 탐색에 사용할 수 없다, 정렬도 X 92 | - 인덱스 생성 시, `include (sal)` 93 | 94 | ### 3.1.6 인덱스 구조 테이블 95 | 96 | - 클러스터형 인덱스, IOT 97 | - 인덱스 리프 블록에서 테이블 블록에 있어야 할 데이터를 모두 저장 98 | - 인위적으로 클러스터링 팩터를 좋게 만드는 방법 → 시퀀셜 데이터 액세스 99 | - 테이블 생성할 때, `organization index` 작성(기본 테이블은 힙 구조 테이블) 100 | 101 | ### 3.1.7 클러스터 테이블 102 | 103 | - 인덱스를 이용한 클러스터, 해시 클러스터 존재 104 | 105 | ### 질문 106 | 107 | - 129p 파티션 pruning: 하드 파싱이나 실행 시점에서 SQL 조건절을 분석해서 읽지 않아도 되는 파티션 세그먼트를 액세스 대상에서 제외시키는 기능 108 | - 콤마는 암시적 inner join 109 | - 랜덤 I/O를 줄여야한다고 할 때, 디스크 블록 I/O(테이블 액세스)만 말하는 게 아니라 버퍼 캐시 읽는 것도 포함인듯? 110 | - 142p 실행 계획 읽는 순서? 111 | - 윈도우 함수? 112 | - include의 커버링 인덱스 대비 우위점: 갱신 시 인덱스 트리 재정렬 오버헤드 X 113 | - 152p 클러스터링 인덱스가 수정되면 데이터 전체 재정렬? 114 | - 클러스터 vs 파티셔닝 → 파티셔닝 테이블을 물리적으로 분리해서 I/O 경합으르 줄이기 위함, 클러스터는 CF를 높이기 위한 목적 115 | - 클러스터 없이 쓰면 배치로 여러 데이터 입력되도 다른 블록에 저장될까? CF 나빠질까? 116 | 117 | [SQL 실행계획(Explain Plan) 해석/읽는 법](https://m.blog.naver.com/jump_penguin/20193916878) 118 | 119 | # 3.2 부분범위 처리 활용 120 | 121 | ### 3.2.1 부분범위 처리 122 | 123 | - DBMS는 클라이언트에게 데이터를 전송할 때 일정량씩 나누어 전송 124 | - 일정량(Array Size) 전송하고 서버 프로세스는 CPU를 OS에 반환 125 | - 클라이언트로부터 Fetch Call을 받기 전까지 서버 프로세스는 대기 큐에서 그대로 멈춰 기다림 126 | - 정렬 조건이 있으면 정렬을 완료한 후 전송할 데이터를 나눌 수 있기 때문에 성능 개선 불가능 127 | - 정렬 조건이 인덱스의 선두에 있으면 부분 범위 처리 가능 128 | - 대량 데이터를 파일로 내려받는다면 어차피 데이터를 모두 전송해야 하므로 Array Size를 크게 하고 Fetch Call을 줄임 129 | - 일부 데이터만 Fetch하다가 멈추는 프로그램은 Array Size를 작게 설정하는 것이 유리 130 | 131 | ### 3.2.2 부분범위 처리 구현 132 | 133 | Array Size에 도달하면 멈추었다가 사용자 요청이 있을 때 다시 데이터를 Fetch하는 부분이 필요하다 134 | 135 | ### 3.2.3 OLTP 환경에서 부분범위 처리에 의한 성능개선 원리 136 | 137 | 클라이언트 - DB 서버 2-Tier 환경은 일부만 출력하고 멈출 수 있다 138 | 139 | 클라이언트와 DB 사이에 WAS, AP 서버 등이 존재하는 n-Tire는 클라이언트가 특정 DB 커텍션을 독점할 수 없 → 5.3장에서 방법 설명 140 | 141 | # 3.3 인덱스 스캔 효율화 142 | 143 | ### 3.3.3 액세스 조건과 필터 조건 144 | 145 | - 인덱스 액세스 조건: 인덱스 스캔 범위를 결정하는 조건절 146 | - 인덱스 필터 조건: 테이블로 엑세스 할지 결정하는 조건절 147 | - 테이블 필터 조건: 쿼리 수행 다음 단계로 전달하거나 최종 결과집합에 포함할지 결정 148 | 149 | ### 3.3.4 비교 연산자 종류와 컬럼 순서에 따른 군집성 150 | 151 | - 인덱스 엑세스 조건: 선행 컬럼이 '='인 조건 상태에서 첫 번째로 나타나는 범위 검색 조건까지 152 | 153 | → 조건에 만족하는 인덱스 레코드는 연속해서 모여있음 154 | 155 | - 인덱스 필터 조건: 그 이후는 비교 연산자 종류에 상관없이 결과가 흩어짐 156 | 157 | ### 3.3.5 인덱스 선행 컬럼이 등치(=) 조건이 아닐 때 생기는 비효율 158 | 159 | - 인덱스 스캔 효율은 인덱스 커럼을 모두 등치(=) 조건으로 사용할 때 가장 좋다 160 | - 인덱스 컬럼 중 일부가 조건절에 없거나 등치 조건이 아니더라도 뒤쪽 컬럼일 때는 비효율이 없다 161 | - ex. 인덱스 on (아파트시세코드, 평형, 평형타입, 인터넷 매물) → `where 아파트시세코드 =: a` 162 | - 인덱스 선행 컬럼이 조건절에 없거나, 부등호, BETWEEN, LIKE 같은 범위 검색 조건이면 인덱스를 스캔하는 단계에서 비효율이 생긴다 163 | 164 | ### 3.3.6 BETWEEN을 IN-List로 전환 165 | 166 | - Between 절을 IN-List로 변경해서 개선할 수 있다 167 | 168 | → INLIST ITERATOR 오퍼레이션 발생(IN절의 각 값에 대해 수직 탐색 반복 진행) 169 | 170 | → IN-LIST에 있는 값을 등치 조건으로 실행 후 union all 실행 171 | 172 | → IN-List 항목이 늘어날 수 있으면 불가능(between 값 사이에 현재 알고 있는 값 이외에 추가 발생) 173 | 174 | → IN-List 개수가 많으면 리프 블록 탐색 스캔 비용보다 루트에서 브랜치 블록까지를 반복 스캔하는 I/O 비용이 클 수 있음 175 | 176 | → 인덱스 스캔 과정에서 선택되는 레코드들이 멀리 떨어져있을 때 유용 177 | 178 | 179 | ### 3.3.7 Index Skip Scan 활용 180 | 181 | 선두 컬럼이 Between이어서 나머지 검색 조건을 만족하는 데이터들이 멀리 떨어져있을 때, IN-List대신 Index Skip Scan으로 개선 가능 182 | 183 | - 인덱스 on (판매월, 판매 구분) 184 | - select (*) from 월별고객별판매집계 t where 판매구분 ='A' and 판매월 between '201801' and '201812' 185 | - 블록 I/O 비교: 인덱스 변경 > Skip Scan > IN-LIST >> Between 186 | 187 | ### 3.3.8 IN 조건은 '='인가 188 | 189 | - IN 조건은 '='이 아니다 190 | - '='이 되려면 IN-LIST Iterator로 풀어서 액세스 조건으로 만들어야 한다 191 | - 그렇지 않으면 IN 조건은 필터 조건이다 192 | - IN 조건이 액세스 조건이 된다고 항상 효과적인 것이 아니다 193 | - `NUM_INDEX_KEYS` 힌트로 IN-List 액세스 조건 또는 필터 조건으로 유도할 수 있다 194 | - 세 번째 인자 값(ex. 1)으로 주어진 칼럼까지만 액세스 조건으로 사용 195 | - 힌트 없이 필터 조건으로 사용하고 싶은 컬럼을 가공(ex. RTRIM, || '' 등 )할 수 도 있음 → 인덱스 사용 X 196 | 197 | ### 3.3.9 BETWEEN과 LIKE 스캔 범위 비교 198 | 199 | - LIKE보다 BETWEEN을 사용하는 게 낫다 200 | - BETWEEN은 양 끝단의 레코드 시작점, 멈추는 지점이 설정되여 효율적일 수 있음 201 | - 그러나 LIKE는 양 끝단의 조건 레코드를 모두 훑어야함 202 | 203 | ### 3.3.10 범위 조건 검색을 남용할 때 생기는 비효율 204 | 205 | ```sql 206 | <쿼리1> 회사코드, 지역코드, 상품명을 모두 입력할 때 207 | SELECT 고객ID, 상품명, 지역코드, ... 208 | FROM 가입상품 209 | WHERE 회사코드 =: com 210 | AND 지역코드 =: reg 211 | AND 상품명 LIKE :prod || '%' 212 | 213 | <쿼리1> 회사코드, 상품명을 모두 입력할 때 214 | SELECT 고객ID, 상품명, 지역코드, ... 215 | FROM 가입상품 216 | WHERE 회사코드 =: com 217 | AND 상품명 LIKE :prod || '%' 218 | ``` 219 | 220 | ```sql 221 | <쿼리3> 회사코드, 상품명, 지역코드,,, 222 | SELECT 고객ID, 상품명, 지역코드, ... 223 | FROM 가입상품 224 | WHERE 회사코드 =: com 225 | AND 지역코드 LIKE :reg || '%' 226 | AND 상품명 LIKE :prod || '%' 227 | ``` 228 | 229 | → 2가지 상황에 따라 처리해야하는 쿼리를 효율적으로 개발하기 위해 쿼리 3으로 변경하면 지역코드가 입력되어도 지역 코드가 액세스 조건으로 사용되지 못하고 필터 조건으로 사용됨 230 | 231 | ⇒ 개발 생산성은 좋아질지 몰라도 인덱스 스캔의 비효율이 생긴다. 특히 대량 테이블을 넓은 범위로 검색할 때 영향이 매우 클 수도 있다 232 | 233 | ### 3.3.11 다양한 옵션 조건 처리 방식의 장단점 비교 234 | 235 | 옵션처리를 위해 아래와 같이 다양한 방법이 있다. 성능 개선을 위해 가장 다루기 어려운 주제이다. 여러 방식의 장단점을 이해해서 상황에 따라 선택해야한다. 236 | 237 | 1. OR 조건 활용 238 | 239 | ```sql 240 | select * from 거래 241 | where (:cust_id is null or 고객ID = :cust_id) 242 | and 거래일자 between :dt1 and dt2 243 | ``` 244 | 245 | - 옵티마이저의 OR Expansion이 자동으로 적용되지 않는다 246 | 247 | → 선행조건에 Or 사용시 인덱스 사용X 248 | 249 | → 후행조건에서 사용시, 필터로 사용되지만 oracle는 null을 인덱스에 저장하지 않기 때문에 테이블 액세스 단계에서 필터링 250 | 251 | - 인덱스에 포함되지 않은 컬럼에 대해 옵션 조건은 어차피 테이블 스캔에서 필터링해야하므로 써도 상관없음 252 | 253 | ⇒ 인덱스 액세스 조건으로 사용 불가, 인덱스 필터 조건으로도 사용 불가, 테이블 필터 조건으로만 사용 가능(NotNull 조건 컬럼은 18c부터 인덱스 필터 조건 사용 가능) 254 | 255 | 아래의 경우에는 OR Expansion을 통해 인덱스 사용 가능 256 | 257 | ```sql 258 | select * from 거래 where 고객ID = :custd_id 259 | and ((:dt_type = 'A' AND 거래일자 between :dt1 and :dt2)) 260 | or 261 | ((:dt_type = 'B' AND 거래일자 between :dt1 and :dt2)) 262 | ``` 263 | 264 | 2. LIKE/BETWEEN 조건 활용 265 | 266 | 필수 조건이 변별력이 좋은 경우 나쁘지 않다. 267 | 268 | ```sql 269 | -- 인덱스 : 상품대분류코드 + 상품코드 270 | select * from 상품 271 | where 상품대분류코드 = :prd_lcls_cd // 필수 조건 272 | and 상품코드 like :prd_cd || '%' // 옵션 조건 273 | ``` 274 | 275 | LIKE/BETWEEN 을 사용할 때 아래 조건에 속하는지 반드시 점검해야한다(BETWEEN은 위의 2개) 276 | 277 | - 인덱스 선두 컬럼 → 값이 입력되지 않으면 인덱스에서 모두 검색해서 필터링해야함 278 | - NULL 허용 컬럼 → 결과 집합에 오류가 생김(null은 like '%'에 검색되지 X) 279 | - 숫자형 컬럼 → '%'은 문자 비교 연산이라서 숫자형 컬럼은 형변환 → 인덱스 사용X 280 | - 가변 길이 컬럼 → 입력값이 있어도 여러 값이 의도하지 않게 검색될 수 있음 281 | 3. UNIONALL 활용 282 | 283 | ```sql 284 | select * from 거래 285 | where :cust_id is null 286 | and 거래일자 between :dt1 and :dt2 -- 거래 일자가 선두인 인덱스 사용 287 | union all 288 | select * from 거래 289 | where :cust_id is not null 290 | and 거래일자 between :dt1 and :dt2 -- 고객ID가 선두인 인덱스 사용 291 | ``` 292 | 293 | → :cust_id 변수에 값을 입력했는지 하지 않았는지에 따라 두 SQL 중 하나만 실행되는 방식 294 | 295 | - null 허용 컬럼이어도 문제 없음 296 | - 두 경우 모두 인덱스 조건으로 사용한다 297 | - SQL 코딩량이 길어진다 298 | 4. NVL/DECODE 함수 활용 299 | 300 | ```sql 301 | select * from 거래 302 | where 고객ID = nvl(:cust_id, 고객ID) 303 | and 거래일자 between :dt1 and :dt2 304 | 305 | 혹은 306 | 307 | select * from 고객 308 | where 고객ID = decode(:cuxst_id, null, 고객ID, :cust_id) 309 | and 거래일자 between :dt1 and :dt2 310 | ``` 311 | 312 | → :cust_id 값이 없으면 거래일자가 선두인 인덱스 이용, 있으면 고객ID+거래일자 인덱스 사용 313 | 314 | - NVL, DECODE 둘 다 실행계획 동일 315 | - 고객ID를 가공했는데도 인덱스 사용이 가능한 이유는 OR Expansion 쿼리 변환이 일어났기 때문 316 | - 옵션 조건 컬러도 인덱스 액세스 조건으로 사용할 수 있음 317 | - UNION ALL보다 단순하면서 UNION ALL과 동일한 성능을 냄 318 | - 하지만, NULL 허용 컬럼에서는 사용할 수 없다 319 | - NVL/DECODE 함수를 여러개 사용하면 그 중 가장 변별력이 좋은 컬럼 기준으로 한 번만 OR Expansion이 일어남 320 | 5. Dynamic SQL 321 | - dynamic sql 쓸 수 있으면 사실 옵션 조건에 '=' 사용 가능 322 | - 하지만 금융권 등 dynamic sql을 허용하지 않는 곳도 있고, 허용하더라도 힌트로 액세스 경로를 고정하려고 할 때 동적으로 구성된 조건절과 서로 상충할 수 있어서 고려해야한다 323 | 324 | ### 3.3.12 함수호출부하 해소를 위한 인덱스 구성 325 | 326 | PL/SQL 사용자 정의 함수는 개발자들이 일반적으로 생각하는 것 이상으로 느리다 327 | 328 | <이유> 329 | 330 | - 가상머신(VM) 상에서 실행되는 인터프리터 언어 331 | - 호출 시마다 컨텍스트 스위칭 발생 332 | - 내장 SQL에 대한 Recursive Call 발생 333 | 334 | → 쿼리 내에 풀어서 쓰길 권장 335 | 336 | → 함수 내부 로직이 너무 복잡하면 그대로 쓸 수 밖에 없음. 그럴 땐 효과적인 인덱스 구성을 통해 함수 호출 횟수를 줄이기 337 | 338 | ## 3.4 인덱스 설계 339 | 340 | ### 3.4.1 인덱스 설계가 어려운 이유 341 | 342 | - 인덱스의 양을 적절히 가져가면서 SQL에 최적화된 인덱스를 생성해야하기 때문에 어렵다 343 | - 시스템 전체 시각에서 종합적, 전략적으로 접근해야한다 344 | - 인덱스 추가는 시스템에 부하를 주고, 인덱스 변경은 운영 리스크가 있다. 345 | 346 | ⇒ 개발 단계에서 인덱스를 최적으로 설계하는 것이 중요! 347 | 348 | ### 인덱스가 많아지면 생기는 문제 349 | 350 | - DML 성능 저하(= TPS 저하) - 인덱스 갱신 오버헤드, 인덱스 분할 등 351 | - 데이터베이스 사이즈 증가(= 디스크 공간 낭비) 352 | - 데이터베이스 관리 및 운영 비용 상승(백업,복제, 재구성 등) 353 | 354 | ### 3.4.2 가장 중요한 두 가지 선택 기준 355 | 356 | - 조건절에 항상 사용하거나, 자주 사용되는 컬럼을 선정한다 357 | - '=' 조건으로 자주 조회하는 컬럼을 앞쪽에 둔다 358 | 359 | → 인덱스 스캔 효율 360 | 361 | ### 3.4.3 스캔 효율성 이외의 판단 기준 362 | 363 | - **수행 빈도** 364 | - NL 조인을 하는 경우 Inner 쪽 스캔 365 | - 업무상 중요도 366 | - 클러스터링 팩터 367 | - **데이터량** 368 | - DML 부하(=기존 인덱스 개수, 초당 DML 발생량, 자주 갱신하는 컬럼 포함 여부 등) 369 | - 저장 공간 370 | - 인덱스 관리 비용 등 371 | 372 | ### 3.4.4 공식을 초월한 전략적 설계 373 | 374 | 조건절 패턴이 열 개 있을 때, 패턴마다 인덱스를 하나씩 만들 수는 없다 375 | 376 | → 최적을 달성해야 할 핵심 액세스 경로 한 두 개를 전략적으로 선택해서 최적 인덱스 설계 377 | 378 | → 나머지 액세스 경로는 약간의 비효율이 있더라도 목표 성능을 만족하는 수준으로 인덱스를 구성 379 | 380 | ![](/img/pic3-47.png) 381 | 가계약 테이블. 왼쪽 리스트는 '=' 연산자. 오른쪽은 between 연산자로 조회 382 | 383 | → 공식에 의해 '=' 절을 앞에 두는 인덱스를 만드려면 24개 필요 384 | 385 | ### 최종 설계 386 | 387 | - X01: 청약일자 + 취급부서 + 취급지점 + 취급자 + 입력자 + 대리점설계사 + 대리점지사 388 | - X01: 보험개시일자 + 취급부서 + 취급지점 + 취급자 + 입력자 + 대리점설계사 + 대리점지사 389 | - X01: 보험종료일자 + 취급부서 + 취급지점 + 취급자 + 입력자 + 대리점설계사 + 대리점지사 390 | - X01: 데이터생성일시 + 취급부서 + 취급지점 + 취급자 + 대리점설계사 + 대리점지사 391 | - X05: 입력자 + 데이터생성일시(조회 패턴의 85%) 392 | 393 | → 일자 조회 구간이 길지 않으면 인덱스 스캔 효율이 성능에 미치는 영향이 크지 않다 394 | 395 | → 인덱스 스캔 효율보다 테이블 엑세스가 더 큰 부하 요소 396 | 397 | ⇒ 인덱스 개수를 최소화하면 사용 빈도가 높건 중요한 액세스 경로가 새로 도출됐을 때 최적의 인덱스를 추가할 여유도 생긴다 398 | 399 | ### 3.4.5 소트 연산을 생략하기 위한 컬럼 추가 400 | 401 | - 인덱스는 정렬되어 있어서 소트 연산 생략 가능 402 | - 정렬을 위해 인덱스를 사용하기도 403 | 404 | ### I/O 연산 최소화 & 소트 연산 생략 405 | 406 | - '=' 연산자로 사용한 조건절 컬럼 선정 407 | - ORDER BY 절에 기술한 컬럼 추가 408 | - '=' 연산자가 아닌 조건절 컬럼은 데이터 분포를 고려해 추가 여부 결정 409 | 410 | 아래 쿼리에서 소트 연산을 생략하도록 인덱스를 구성해보자! 411 | 412 | ```sql 413 | SELECT 계약ID, 청약일자, 입력자ID, 계약상태코드, 보험시작일자, 보험종료일자 414 | FROM 계약 415 | WHERE 취급지점ID =: trt_brch_id 416 | AND 청약일자 BETWEEN :sbcp_dt1 AND :sbcp_dt2 417 | AND 입력일자 >= trunc(sysdate - 3) 418 | AND 계약상태코드 IN (:ctr_stat_cd1, :ctr_stat_cd2, :ctr_stat_cd3) 419 | ORDER BY 청약일자, 입력자ID 420 | ``` 421 | 422 | - 정답 423 | 424 | `취급지점ID + 청약일자 + 입력자ID`, 입력일자, 계약상태코드는 상황에 따라 425 | 426 | 이 조건을 만족하는 데이터가 적으면 인덱스에 추가하는 것이 좋다!(테이블 필터링 감소) 427 | 428 | 429 | ### IN 조건은 '='이 아니다 430 | 431 | 위의 쿼리에서 계약상태코드를 인덱스 앞에 두면 소트 연산이 생략될까? 432 | 433 | Nope! IN 조건은 '='이 아니다 434 | 435 | ```sql 436 | SELECT 고객번호, 고객명, 거주지역, 혈액형, 연령 437 | FROM 고객 438 | WHERE 거주지역 = '서울' 439 | AND 혈액형 IN ('A', 'O') 440 | ORDER BY 연령 441 | ``` 442 | 443 | → 소트 연산을 생략하려면 IN-List Iterator 방식으로 풀려선 안됨 444 | 445 | ⇒ IN 조건절을 인덱스 액세스 조건X, 필터 조건 O 446 | 447 | ### 3.4.6 결합 인덱스 선택도 448 | 449 | - 선택도: 전체 레코드 중에서 조건절에 의해 선택되는 레코드 비율(선택도 * 총 레코드수 = 카디널리티) 450 | - 인덱스 생성 여부를 결정할 때 선택도가 충분히 낮은지가 중요한 판단 기준 451 | - 결합 인덱스 컬럼간 순서에 선택도가 중요할까? No 452 | - ex) WHERE 성별 =: gender AND 고객번호 =: cust_id 453 | 454 | → 인덱스 액세스 조건이니까 무관! 선택도 높은 걸 앞에 두는 노력은 의미없거나 손해일수도! 455 | 456 | - '항상 사용하는' 컬럼을 앞쪽에 두고 그중 '=' 조건을 앞쪽에 위치시키는 것이 중요 457 | 458 | ```sql 459 | WHERE 고객등급 =: V1 460 | AND 고객번호 =: V2 461 | AND 거래일자 >=: V3 462 | 463 | WHERE 고객등급 =: V1 464 | AND 고객번호 =: V2 465 | AND 거래일자 >=: V3 466 | AND 거래유형 =: V4 467 | 468 | WHERE 고객등급 =: V1 469 | AND 고객번호 =: V2 470 | AND 거래일자 >=: V3 471 | AND 상품번호 =: V5 472 | 473 | WHERE 고객등급 =: V1 474 | AND 고객번호 =: V2 475 | AND 거래일자 >=: V3 476 | AND 거래유형 =: V4 477 | AND 상품번호 =: V5 478 | ``` 479 | 480 | - 필수) 고객번호(=), 고객 등급(=), 거래일자(between) + 옵션) 거래유형, 상품번호 481 | - 하지만, 고객변호는 필수인데 고객등급이 조건절에서 누락되거나 범위검색 조건일 수 있는 경우라면 고객 등급을 앞에 두는 것이 유리! → Index Skip Scan, In-List 조건 사용 가능 482 | 483 | ⇒ 인덱스 생성 여부에는 선택도가 중요하지만 컬럼 간 순서에는 각 컬럼의 선택도보다 필수 조건 여부, 연산자 형태가 더 중요한 판단 기준! 484 | 485 | ### 3.4.7 중복 인덱스 제거 486 | 487 | ### 완전 중복 인덱스 488 | 489 | X01: 계약ID + 청약일자 490 | 491 | X02: 계약ID + 청약일자 + 보험개시일자 492 | 493 | X03: 계약ID + 청약일자 + 보험개시일자 + 보험종료일자 494 | 495 | ### 불완전 중복 인덱스 496 | 497 | X01: 계약ID + 청약일자 498 | 499 | X02: 계약ID + 보험개시일자 500 | 501 | X03: 계약ID + 보험종료일자 502 | 503 | X04: 계약ID + 데이터생성일시 504 | 505 | → 계약ID 카디널리티가 매우 낮다면 사실상 중복 506 | 507 | ⇒ 계약ID + 청약일자 + 보험개시일자 + 보험종료일자 + 데이터생성일시 508 | 509 | ### 3.4.8 인덱스 설계도 작성 510 | 511 | 시스템 전체 효율을 고려해야하기 때문에 설계도 필요 512 | 513 | - 인덱스 설계 전에 파티션 설계를 먼저 진행하거나 병행해야 제대로 된 인덱스 전략을 수립할 수 있다 514 | 515 | ![Untitled](/img/table3-8.png) 516 | 517 | ### 질문 518 | 519 | - 216p 어떤 경우에는 OR Expantion 동작하는지? 520 | - 228p, 229p 함수 호출 횟수 왜,,? 521 | - 222P `where 고객ID = 고객ID`의 의미? 522 | - 인덱스 압축? 523 | - `where a =: a and b =: b` vs `where b =: b and a =: a` 다른가? 524 | -------------------------------------------------------------------------------- /4장_조인_튜닝/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/4장_조인_튜닝/.empty -------------------------------------------------------------------------------- /4장_조인_튜닝/4.1_NL_조인_김광훈.md: -------------------------------------------------------------------------------- 1 | # 4.1 NL 조인 2 | 조인 메소드으로는 NL 조인, 해쉬 조인, 소트 머지 조인 등이 존재한다. 3 | 4 | MySQL 은 버전에 따라 지원하는 조인 메소드가 다르다. 5 | 6 | 5.7 버전까지는 NL 조인만 지원하였고, 8.0 부터는 해쉬 조인까지 지원하게 되었다. 7 | 8 | https://dev.mysql.com/doc/refman/8.0/en/hash-joins.html 9 | 10 |
11 | 12 | ## 4.1.1 NL 조인 메커니즘 13 | NL 조인 (Nest Loop Join)은 이름 그대로 중첩 루프를 사용하는 조인이다. 14 | 15 | 16 | 17 | 조인 과정은 아래와 같다. 18 | - (1) Table A 에서 row 를 스캔한다. 19 | - 이때 Table A 를 Driving Table 혹은 Outer Table 이라고 한다. 20 | 21 | 22 | - (2) Outer Table 의 row 를 순회할 때 마다 Table B 를 스캔해서 Join 조건에 맞으면 리턴을 한다. 23 | - 이떄 Table B 를 Inner Table 이라고 한다. 24 | 25 | 26 | - (3) 위 1,2 과정을 계속 진행합니다. 27 | 28 |
29 | 30 | 예제로 한 번 더 살펴보자. 31 | 32 | 33 | 34 |
35 | 36 | ## 4.1.2 실행 게획 37 | 38 | MySQL 5.7 에서 두 테이블을 Join 하는 SQL 의 실행 계획을 확인해보자. 39 | 40 | 41 | 42 | 실행 계획을 확인해보면 NL 조인을 사용하는 것을 볼 수 있다. 43 | 44 | 또한 Index 를 사용하는 것도 볼 수 있다. 45 | 46 |
47 | 48 | ## 4.1.3 올바른 조인 메소드 선택 49 | (1) 1차적으로 NL 조인부터 고려하는 것이 좋다. 50 | 51 | (2) 그 이후에 성능이 좋지 않다면 NL 조인 튜닝 포인트에 따라 각 단계를 분석해서 과도한 랜덤 액세스가 발생하는 지점을 우선 파악한다. 52 | 53 | (3) 그 이후에도 나아지지 않는다면 소트 머지 조인이나 해시 조인을 검토한다. 54 | 55 | 56 | #### + 랜덤 엑세스 복습 57 | SSD 에서 랜덤 엑세스가 효율이 좋은 이유 58 | 59 | 60 | 61 | 랜덤 I/O vs 순차 I/O 62 | 63 | 64 | 65 | 66 | ## 4.1.4 NL 조인 특징 요약 67 | (1) 랜덤 엑세스 위주의 조인 방식이다. 따라서 대량의 데이터를 조인할 때에는 효율이 떨어진다. 68 | 69 | (2) 한 코레드씩 순차적으로 진행하다. 따라서 아무리 큰 테이블을 조인하더라도 매우 빠른 응답 속도를 낼 수 있다. -------------------------------------------------------------------------------- /4장_조인_튜닝/6주_김민석.md: -------------------------------------------------------------------------------- 1 | # 4.4장 서브쿼리 조인 2 | 3 | ### 4.4.1 서브쿼리 변환이 필요한 이유 4 | 5 | - 서브쿼리의 종류 6 | 7 | ![image](https://user-images.githubusercontent.com/6725753/145973246-107cd2c3-a25a-46a7-b3e3-b04cec8ef6a1.png) 8 | 9 | 1. 인라인 뷰 : FROM 절에 사용한 서브쿼리 10 | 2. 중첩된 서브쿼리 : 결과집합을 한정하기 위해 WHERE 절에 사용한 서브쿼리. 서브쿼리가 메인쿼리 컬럼을 참조할 때 상관관계 있는(Correlated) 서브쿼리라고 한다. 11 | 3. 스칼라 서브쿼리 : 한 레코드당 정확히 하나의 값을 반환하는 서브쿼리. 주로 SELECT 절에 사용하지만 컬럼이 올 수 있는 대부분 위치에 사용가능. 12 | 13 | 14 | 메인쿼리도 하나의 쿼리 블록이며 옵티마이저는 쿼리 블럭 단위로 최적화 수행. 15 | 16 | ![image](https://user-images.githubusercontent.com/6725753/145973412-05c9228d-0fcd-4124-8fd7-fc6e0312518f.png) 17 | 18 | 하지만, 서브쿼리별로 따로 최적화했다고 해서 전체 쿼리가 최적화 됐다고 할 수 없기 때문에 전체를 바라보는 관점에서 쿼리를 이해하려면 서브쿼리는 풀어내야 한다. 19 | 20 | 21 | ### 4.4.2 서브쿼리와 조인 22 | 23 | 메인쿼리와 서브쿼리간에는 부모와 자식같은 종속적이고 계층적인 관계가 형성된다. 서브가 메인에 종속되기 때문에 단독으로 실행할 수 없고 메인에서 값을 받아서 반복적으로 필터링하는 방식을 사용한다. 24 | 25 | #### 필터 오퍼레이션 26 | 27 | unnest 하지 않고 NL 조인 처럼 서브쿼리 하는 방식 28 | 29 | ![image](https://user-images.githubusercontent.com/6725753/145973577-5fe7cbc2-4b6d-4b3e-ab44-9247da3dc744.png) 30 | 31 | - 필터방식으로 처리하기 위해 no_unnest 힌트 사용 32 | - no_unnest : unnest(풀어내지) 말고 그대로 수행하라는 뜻. 33 | - 필터 오퍼레이션은 NL 조인과 처리 루틴이 같다. 34 | - 다른 점은 35 | - 조인에 성공하는 순간(exist) 진행을 멈추고 다음 로우 진행 36 | - 필터가 각 서브쿼리 수행에 대해서 캐싱 한다. 37 | 38 | #### 서브쿼리 Unnesting 39 | 40 | unnest : 중첩된 상태를 풀어낸다 41 | 42 | 서브쿼리를 unnest 할 때는 unnest 힌트 사용 43 | 44 | 서브쿼리를 풀지 않고 쓰면 필터를 사용해야 하지만 unnest 하게 되면 일반 조인 처럼 다양한 최적화 기법을 사용할 수 있게 된다. 45 | 46 | - NL 세미조인 47 | - 필터처럼 조인에 성공하는 순간 메인 쿼리의 다음 로우로 넘어간다 48 | - ![image](https://user-images.githubusercontent.com/6725753/145973734-b3d2b659-fbfd-437b-bc7f-aac810f30ce1.png) 49 | - leading 50 | - 필터를 쓰면 메인쿼리가 항상 드라이빙 집합이지만 unnest 하게되면 leading 힌트를 통해서 순서를 바꿀 수 있다. 51 | - ![image](https://user-images.githubusercontent.com/6725753/145973912-73cd9e31-6ba2-4730-982d-a24fd2094fa7.png) 52 | - 아래 쿼리로 변환된것과 같은 효과 53 | - ![image](https://user-images.githubusercontent.com/6725753/145974052-d0007747-fe60-4289-9eac-1cb85d61b6f3.png) 54 | - hash 조인 55 | - unnest 하고 해쉬 조인 적용 56 | - ![image](https://user-images.githubusercontent.com/6725753/145974182-010018c9-33bf-4fa9-8239-ad69528ab845.png) 57 | 58 | #### 서브쿼리 Pushing 59 | 60 | 필터방식에서 서브쿼리는 순서가 정해지고 맨 마지막 단계에 처리된다. 하지만, push_subq 힌트를 사용하여 서브쿼리 필터링을 먼저 처리하게 해서 처리량을 줄일 수 있다. 따라서, push는 필터링 상태에서만 적용이 가능하다. 61 | 62 | ![image](https://user-images.githubusercontent.com/6725753/145974433-362e6d0c-98f7-4d4a-af31-417d0d24babe.png) 63 | 64 | ### 뷰와 조인 65 | 66 | 옵티마이저가 뷰 쿼리를 변환하지 않으면 뷰 쿼리 블록을 독립적으로 최적화 하기 때문에 전체 최적화가 안될 수 있다. 67 | 68 | ![image](https://user-images.githubusercontent.com/6725753/145974578-153fc0ff-bdf3-4f70-9c4c-d47bf132a940.png) 69 | 70 | 전월 이후 가입 고객 필터 조건이 뷰 바깥에 있기 때문에 뷰 안에서는 전체 고객에 대한 데이터를 읽어야 한다. 71 | 72 | 이 문제는 merge 힌트를 사용하여 해결 할 수 있다. 73 | 74 | ![image](https://user-images.githubusercontent.com/6725753/145974766-4ec302c6-4734-4d84-bb68-e6ce76de2e81.png) 75 | ![image](https://user-images.githubusercontent.com/6725753/145974838-b45ba914-021b-42ed-833e-1e3da003477b.png) 76 | 77 | 78 | #### 조인 조건 Pushdown 79 | 80 | 메인 쿼리를 실행하면서 조인 조건절 값을 건건이 뷰 안으로 밀어 넣는 기능이다. 81 | 82 | ![image](https://user-images.githubusercontent.com/6725753/145975012-1b1df650-8a37-4d87-a594-9b7f03e96e62.png) 83 | ![image](https://user-images.githubusercontent.com/6725753/145975157-ba91ea7d-ac93-4df8-8e52-ce3a1b68e2d8.png) 84 | 85 | 아래와 같이 최적화 되었다고 생각하면 됨(문법은 틀림) 86 | 87 | ![image](https://user-images.githubusercontent.com/6725753/145975341-42eef524-15bc-4d35-8223-ad0af8dffb96.png) 88 | 89 | 이렇게 되면 부분범위 처리 가능. 90 | 91 | 92 | ### 스칼라 서브쿼리 조인 93 | 94 | #### 스칼라 서브쿼리의 특징 95 | 96 | select 쿼리를 품은 사용자 함수가 있다면 쿼리 건수만큼 재귀적으로 반복 실행된다. 97 | 하지만 스칼라 서브쿼리는 정확히 하나의 값만 반환한다. 함수처럼 재귀적이지 않으면서 컨텍스트 스위칭도 발생하지 않는다. 98 | 99 | ![image](https://user-images.githubusercontent.com/6725753/145975435-a76de1dc-c08b-4528-bd57-3dd451d74ecf.png) 100 | 101 | #### 스칼라 서브쿼리 캐싱 효과 102 | 103 | 스칼라 서브쿼리로 조인할 때 입력값과 출력값은 캐싱된다. 104 | 105 | 입력값 : 서브쿼리에서 참조하는 메인 쿼리의 컬럼 값 106 | 107 | ![image](https://user-images.githubusercontent.com/6725753/145975565-ef3206fc-7767-40a0-b950-094e33de828d.png) 108 | 109 | 필터 서브쿼리 캐싱관 같은 기능. 110 | 쿼리를 시작할 때 PGA 메모리에 공간을 할당하고 쿼리가 끝나면 반환된다. 111 | 112 | 아래와 같이 캐싱을 사용하여 내장함수의 비효율성을 상쇄시킬 수 있다. 113 | 114 | ![image](https://user-images.githubusercontent.com/6725753/145975698-49974a36-8d00-4253-a3ad-a6e10bf30378.png) 115 | 116 | #### 스칼라 서브쿼리 캐싱 부작용 117 | 118 | 스칼라 서브쿼리 캐싱도 캐싱이 가지는 단점을 그대로 가지고 있다. 119 | 120 | - 입력 값의 종류가 적어서 해시 충돌 가능성이 적을 때 효과가 있다. 충돌이 많다면 당연히 성능이 낮아진다. 121 | - 좋은예) 거래구분코드가 20건이라서 입력값(t.거래구분코드)의 종류가 적다 122 | ![image](https://user-images.githubusercontent.com/6725753/145975812-7cbfa609-3dde-4ea9-8f07-08234bcbee65.png) 123 | - 나쁜예) 고객이 100만명이라서 입력값(t.고객번호)의 종류가 많다. 124 | ![image](https://user-images.githubusercontent.com/6725753/145975983-b490a5b0-39ee-4de7-bc6a-90795c6070e6.png) 125 | 126 | - 메인 쿼리 집합이 매우 작은 경우 127 | - 메인쿼리 집합이 클수록 재사용성이 높아서 캐싱 효율이 좋다. 128 | - 나쁜예) 고객당 계좌가 많지 않은 경우 129 | - ![image](https://user-images.githubusercontent.com/6725753/145976073-fe894f11-1ad5-43cb-8c29-a1922591ecb3.png) 130 | 131 | 132 | #### 두개 이상의 값 반환 133 | 134 | ![image](https://user-images.githubusercontent.com/6725753/145978887-864ee995-9bb3-41f9-a7f8-bfb9a067f8a2.png) 135 | ![image](https://user-images.githubusercontent.com/6725753/145978971-98eb6117-f898-4a5b-aeeb-a4620d59f370.png) 136 | 137 | 138 | NL 조인 처럼 처리되기 때문에 부분범위 처리도 가능하고 캐싱의 효과를 쓸 수도 있어서 좋아보이지만 스칼라 서브쿼리는 두 개 이상의 값을 반환할 수가 없는 문제가 있다. 139 | 140 | 이런 경우 아래와 같은 방법을 사용한다. 141 | 142 | ![image](https://user-images.githubusercontent.com/6725753/145977733-fb5be266-e800-48bb-a9f3-6f25a0195306.png) 143 | 144 | 구하는 값들을 문자열로 결합하고 바깥쪽 쿼리에서 다시 분리한다. 145 | 146 | 147 | 다른 방법은 인라인 뷰를 사용한다. 148 | 149 | ![image](https://user-images.githubusercontent.com/6725753/145977861-32e4a1c3-53ac-4f02-ac09-9aabc4619ea1.png) 150 | 151 | 물론 앞에서 설명한 부분범위처리가 안되는 등의 인라인 뷰 머지의 문제가 있기 때문에 '조인 조건 pushdown' 기능을 통해서 문제를 해결할 수 있다. 152 | 153 | #### 스칼라 서브쿼리 Unnesting 154 | 155 | 스칼라 서브쿼리도 캐싱 효과가 미미하면 다른 조인 방식을 사용하기 위해 unnesting 해야 할 때가 있다. 156 | 쿼리가 길고 복잡하면 쉽지 않은데 다행히 오라클 12c부터 옵티마이저가 자동으로 쿼리를 변환해주는 기능이 생겼다. 157 | 158 | `_optimizer_unnest_scalar_sq = true' 159 | 160 | false로 설정하더라도 unnest 힌트로 유도 가능. 161 | 162 | ![image](https://user-images.githubusercontent.com/6725753/145978106-93f0c37e-7456-4dbc-a0db-0714676b296d.png) 163 | 164 | Unnesting 되었기 때문에 NL 조인이 아닌 해시조인 실행됨. 165 | -------------------------------------------------------------------------------- /5장_소트_튜닝/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/5장_소트_튜닝/.empty -------------------------------------------------------------------------------- /5장_소트_튜닝/5.1~5.1_내용_요약_김광훈.md: -------------------------------------------------------------------------------- 1 | # 5.1 소트 연산에 대한 이해 2 | ## 5.1.1 소트 수행 과정 3 | 소트는 기본적으로 PGA 에 할당한 Sort Area 에서 이루어진다. 4 | 5 | 메모리 공간인 Sort Area 가 다 차면, 디스크 Temp 테이블 스페이스를 활용한다. 6 | 7 | - 소트 유형 8 | - 메모리 소트 9 | - 디스크 소트 10 | 11 |
12 | 13 | # 5.2 소트가 발생하지 않도록 SQL 작성 14 | ## 5.2.1 Union vs Union ALl 15 | - Union 16 | - 옵티마이저는 상단과 하단 두 집합 간 중복을 제거하려고 소트 작업을 수행한다. 17 | 18 | 19 | - Union All 20 | - 중복을 확인하지 않고 두 집합을 단순히 결합 21 | - 소트 작업을 수행하지 않는다. 22 | - 두 집합이 상호 배타적일때 사용해도 된다. 23 | 24 | ## 5.2.2 Exist 활용 25 | - Distinct 26 | - 조건에 해당하는 데이터를 모두 읽어서 중복을 제거한다. 27 | - 따라서 많은 I/O 가 발생한다. 28 | 29 | 30 | - Exist 31 | - 데이터의 존재 여부만 확인하면 되기 떄문에 모든 데이터를 읽지 않는다. 32 | - [같이 보면 좋은 글](https://jojoldu.tistory.com/516) 33 | 34 | 35 |
36 | 37 | # 5.3 인덱스를 이용한 소트 연산 생략 38 | ## 5.3.1 Sort Order By 생략 39 | 모든 데이터를 다 읽어 정렬을 마치고 출력을 시작하면 OLTP 환경에서 요구되는 빠른 응답 속도를 내기 어렵다. 40 | 41 | 따라서 인덱스 선두 컬럼에 조건을 구성하여 쿼리에서 소트 연산을 생략하도록 하자. 42 | 43 | 인덱스는 항상 키 컬럼 순으로 정렬된 상태를 유지하기 때문에 적절하게 쿼리를 작성하면 소트 연산을 생략할 수 있다. 44 | 45 |
46 | 47 | ** 3-Tier 환경 48 | 49 | 클라이언트 - WAS - DB 로 구성된 구조 50 | 51 | 해당 아키텍처는 수 많은 클라이언트가 서버 리소스를 공유하는 구조이기 때문에 클라이언트가 특정 DB 커넥션을 독점할 수 없다. 52 | 53 | 독점을 하게 된다면 커넥션 풀이 말라 이슈가 발생할 것이다. 54 | 55 |
56 | 57 | # 5.4 Sort Area 를 적게 사용하도록 SQL 작성 58 | 소트 연산이 불가피하다면 메모리 내에서 처리를 완료할 수 있도록 노력해야 한다. 59 | 60 | 가장 중요한 것은 Sort Area 를 적게 사용할 방법부터 찾는 것이다. 61 | 62 | ## 5.4.1 소트 데이터 줄이기 63 | #### Case 1. 64 | - 일반 쿼리 / 서브 쿼리 65 | - 서브 쿼리를 사용하면 가공하지 않은 상태로 정렬을 완료하고 나서 최종 출력할 때 가공한다. 66 | - 따라서 Sort Area 를 훨씬 더 적게 사용한다. 67 | 68 | #### Case 2. 69 | - Select * / Select XX, TT 70 | - Select XX, TT 이 Sort Area 를 더 적게 사용한다. 71 | - 왜냐하면 XX, TT 두 필드만 저장하기 때문이다. 72 | -------------------------------------------------------------------------------- /5장_소트_튜닝/5.1~5.4_황준호.md: -------------------------------------------------------------------------------- 1 | ## 5장. 소트 튜닝 2 | 3 | ### 5.1 소트 연산에 대한 이해 4 | 5 | - 소트는 기본적으로 Sort Area에서 이루어지지만 Sort Area가 다 차면 디스크 공간까지 사용할 수도 있다 6 | 7 | - Sort Area에서 작업을 완료할수 있는지에 따라 소트를 두가지 유형으로 나눈다 8 | 9 | 1. 메모리 소트 (=Internal Sort) : 전체 소트를 메모리 내에서 완료 10 | 2. 디스크 소트 (=External Sort) : 소트를 하는데에 디스크 공간까지 사용 11 | 12 | - 디스크 소트가 발생하는 순간 쿼리 성능은 나빠지므로 소트가 발생하지 않도록 SQL을 작성해야 하고, 소트가 불가피하다면 메모리 내에서 완료될 수 있도록 해야 한다 13 | 14 | - 소트를 발생시키는 오퍼레이션 15 | 16 | 1. Sort Aggregate 17 | - 전체 로우를 대상으로 집계를 수행할때 (sum, max, min, avg ...) 18 | - 실제로 데이터를 정렬하진 않고, Sort Area를 사용한다 19 | 2. Sort Oeder By 20 | - 데이터를 정렬할때 (order by ...) 21 | 3. Sort Group By 22 | - 소팅 알고리즘을 사용해 그룹별 집계를 수행할때 (group by ...) 23 | - 집계할 대상 레코드가 아무리 많아도 Temp 테이블스페이스를 쓰지 않는다 24 | 4. Sort Unique 25 | - 서브쿼리 Unnesting : 옵티마이저가 서브쿼리를 풀어 일반 조인문으로 변환하는 것 26 | - Unnesting된 서브쿼리가 M쪽 집합일때 메인쿼리와 조인하기 전에 중복 레코드부터 제거할때 27 | 5. Sort Join 28 | - 소트 머지 조인을 수행할때 29 | 6. Window Sort 30 | - 윈도우 함수(분석 함수)를 수행할때 (`avg(sal) over (partition by deptno)` ...) 31 | 32 | ### 5.2 소트가 발생하지 않도록 SQL 작성 33 | 34 | - 불필요한 소트가 발생하지 않도록 주의해야 한다 35 | 36 | - `Union`, `Minus`, `Distinct` 연산자는 중복 레코드를 제거하기 위한 소트 연산을 발생시키므로 필요할때만 사용해야 한다 37 | 38 | - `Union` VS `Union All` 39 | 40 | - 될 수 있으면 소트 작업을 수행하지 않는 `Union All`을 사용해야 한다 41 | 42 | - `Union`을 사용하는 쿼리를 `Union All`으로 변경하려면 `Union All`하는 두 집합이 상호 배타적이어야 (교집합이 없을때) 한다. 그래야 데이터 중복이 없다 43 | 44 | - `Union` -> `Union All` 변경 예시 45 | 46 | ```sql 47 | -- union 사용 48 | select * 49 | from 결제 50 | where 결제일자 = '20180316' 51 | UNION 52 | select * 53 | from 결제 54 | where 주문일자 = '20180316' 55 | 56 | -- union all 사용 57 | select * 58 | from 결제 59 | where 결제일자 = '20180316' 60 | UNION ALL 61 | select * 62 | from 결제 63 | where 주문일자 = '20180316' 64 | and 결제일자 <> '20180316' 65 | ``` 66 | 67 | - Exists 활용 68 | 69 | - 조인 방식 변경 70 | 71 | ### 5.3 인덱스를 이용한 소트 연산 생략 72 | 73 | - 인덱스 선두 컬럼을 where절에 나오는 컬럼으로 구성하면 sort 연산을 생략할 수 있다 74 | 75 | - 부분범위 처리가 가능하도록 SQL를 작성하려면 아래를 만족해야 한다 76 | 77 | - 인덱스 사용 가능하도록 조건절을 구성한다 78 | - 조인은 NL조인 위주로 처리한다 79 | - Order by 절이 있어도 소트 연산을 생략할 수 있도록 인덱스를 구성해준다 80 | 81 | - 인덱스를 잘 구성하면 min, max를 구할때 전체 데이터를 읽지 않을 수 있다 82 | 83 | - 조건절 컬럼과 min, max함수 인자 컬럼이 모두 인덱스에 포함돼 있어야 한다 84 | 85 | - 예 86 | 87 | ```sql 88 | create index IDX_X1 on TABLE_NAME(a, b, c); 89 | 90 | select max(c) 91 | from TABLE_NAME 92 | where a = 30 93 | and b = 70; 94 | -- a, b 두 조건을 만족하는 레코드 중 가장 오른쪽 레코드를 읽는다 95 | ``` 96 | 97 | ```sql 98 | create index IDX_X1 on TABLE_NAME(a, b, c); 99 | 100 | select max(b) 101 | from TABLE_NAME 102 | where a = 30 103 | and c = 70; 104 | -- a 조건을 만족하는 레코드 중 오른쪽부터 스캔하면서 c조건에 맞는 레코드를 찾으면 멈춘다 105 | ``` 106 | 107 | ```sql 108 | create index IDX_X1 on TABLE_NAME(a, b, c); 109 | 110 | select max(a) 111 | from TABLE_NAME 112 | where b = 30 113 | and c = 70; 114 | -- 전체 레코드 중 오른쪽부터 스캔하면서 b, c조건에 맞는 레코드를 찾으면 멈춘다 115 | ``` 116 | 117 | - 인덱스 선두 컬럼을 group by절에 사용한다면 sort group by 연산을 생략할 수 있다 -------------------------------------------------------------------------------- /5장_소트_튜닝/5.3_인덱스를_이용한_소트연산_생략_유효정.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/5장_소트_튜닝/5.3_인덱스를_이용한_소트연산_생략_유효정.pdf -------------------------------------------------------------------------------- /5장_소트_튜닝/5장_김민석.md: -------------------------------------------------------------------------------- 1 | # 5장 소트 튜닝 2 | 3 | ## 5.1 소트 연산에 대한 이해 4 | 5 | ### 소트 수행 과정 6 | 7 | - 소트 연산은 메모리와 CPU를 많이 쓴다. 8 | - 처리할 양이 많을 때는 디스크 I/O를 사용하기도 한다. 9 | - 부분범위 처리가 불가해서 OLTP 환경에서 앱 성능을 저하시킨다. 10 | - 따라서, 되도록 소트를 안하도록 SQL를 작성하고 불가피할경우 메모리내에서 수행이 완료되도록 한다. 11 | 12 | ### 소트 오퍼레이션 13 | 14 | - Sort Aggregate 15 | - 실제 데이터를 정렬하진 않고 Sort Area 사용 16 | - SUM / MAX / MIN / COUNT 17 | - Sort Order By 18 | - 실제 소트 실행 19 | - Sort Group By 20 | - 소팅 알고리즘을 사용해 그룹별 집계 수행 21 | - Sort Unique 22 | - Unnesting된 서브쿼리가 M쪽 집합이면 메인 쿼리와 조인하기 전에 중복 레코드 제거 23 | - Sort Join 24 | - 소트 머지 조인 25 | - Windows Sort 26 | - 분석 함수 수행 27 | 28 | ## 5.2 소트가 발생하지 않고록 SQL 작성 29 | 30 | ### Union vs. Union All 31 | 32 | Union All은 소트 안함. 중복 가능성 없을 경우 Union All 사용하자. 33 | 34 | ### Exists 활용 35 | 36 | 중복 레코드를 제거할 목적으로 Distinct 사용하면 부분범위 처리도 불가할뿐더러 디스크 I/O도 발생한다. 37 | 38 | Exists 서브쿼리는 데이터 존재 여부만 확인하면 되기 때문에 조건절을 만족하는 데이터를 모두 읽지 않는다. 39 | 40 | Distinct, Minus 연산자를 사용한 쿼리는 대부분 Exists 서브쿼리로 변환 가능하다. 41 | 42 | ### 조인 방식 변경 43 | 44 | 조인 방식을 변경하는 것만으로 소트 연산 생략이 가능하다. 45 | 46 | 47 | ## 5.3 인덱스를 이용한 소트 연산 생략 48 | 49 | ### Sort Order By 생략 50 | 51 | 조건절을 고려한 인덱스 생성으로 소트 연산 생략 가능 52 | 53 | ### Top N 쿼리 54 | 55 | 소트가 되어 있는 상황에서 Top N은 필요한 만큼만 연산을 수행한다. 56 | 57 | 대표적으로 페이징이 있다.(물론 뒤쪽 페이지를 select하면 그만큼 많이 읽는다.) 58 | 59 | ### 최소값/최대값 구하기 60 | 61 | 인덱스를 사용하면 바로 읽어올 수 있다. 62 | 이때, 조건절 컬럼과 MIN/MAX 함수 인자 컬럼이 모두 인덱스에 포함돼 있어야 한다. 63 | 64 | ### 이력 조회 65 | 66 | 67 | ### Sort Group By 생략 68 | 69 | 인덱스를 사용하여 Sort Group By 연산 생략이 가능하다. 이때 부분범위 처리도 가능하다. 70 | 71 | 72 | ## 5.4 Sort Area를 적게 사용하도록 SQL 작성 73 | 74 | ### 소트 데이터 줄이기 75 | 76 | 트레이스를 하면서 소트의 데이터량을 직접 비교해 봐야한다. 77 | 78 | ### Top N 쿼리의 소트 부하 경감 원리 79 | 80 | Top N 소트 알고리즘을 사용하여 공간 절약 가능. 81 | 82 | ### 분석함수에서의 Top N 소트 83 | 84 | 윈도우 함수 중 rank / row_number 함수는 max 함수보다 소트 부하가 적다. Top N 소트 알고리즘이 작동하기 때문이다. 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /6장_DML_튜닝/6.1_기본DML튜닝_김광훈.md: -------------------------------------------------------------------------------- 1 | # 6.1 기본 DML 튜닝 2 | ## 6.1.1 DML 성능에 영향을 미치는 요소 3 | #### (1) 인덱스 4 | #### (2) 무결성 제약 5 | #### (3) 조건절 6 | #### (4) 서브쿼리 7 | #### (5) Redo 로깅 8 | #### (6) Undo 로깅 9 | #### (7) Lock 10 | #### (8) 커밋 11 | 12 |
13 | 14 | ### 인덱스와 DML 성능 15 | #### (1) 입력 16 | 테이블에 레코드를 입력하면, 인덱스에도 입력해야 한다. 17 | 18 | 인덱스는 정렬된 자료구조이므로, 수직적 탐색을 통해 입력할 블록을 찾아야 한다. 19 | 20 | 21 | 22 | 23 | #### (2) DELETE 24 | 테이블에서 레코드 하나를 삭제하면, 인덱스 레코드를 모두 찾아서 삭제해야한다. 25 | 26 | #### (3) UPDATE 27 | 변경된 컬럼을 참조하는 인덱스만 찾아서 변경 28 | 29 | 그대신 테이블에서 한 건 변경할 때마다 인덱스에는 두 개의 오퍼레이션 발생 30 | 31 | Example) A --> K 로 변경, 저장 위치가 바뀜 32 | 33 | 34 | 35 | 36 | ### 무결정 제약과 DML 성능 37 | PK, FK, Check, Not Null 같은 제약은 애플리케이션에서 구현할 수 있지만 DBMS 에서 설정한다면 더 완벽하게 데이터 무결성을 보장한다. 38 | 39 | 하지만 성능과 트레이드 오프하는 조건이라는 점은 명심해야한다 40 | 41 | PK, FK 를 검증하기 위해서는 실제 데이터를 조회해 봐야 하기 때문이다. 42 | 43 | 44 |
45 | 46 | ### Redo 로깅과 DML 성능 47 | #### Redo 로그 48 | 오라클은 데이터파일과 컨트롤 파일??에서 발생하는 모든 변경사항을 Redo 로그에 기록 49 | 50 | Redo 로그는 트랜잭션 데이터가 유실됐을 때, 트랜잭션을 재현함으로써 유실 이전 상태로 복구하는 데 사용 51 | 52 | DML 수행을 할때마다 Redo 로그를 생성해야 하므로 성능에 영향을 미친다. 53 | 54 | INSERT 작업을 할 때 Redo 로깅을 생략할 수 있다. 55 | 56 |
57 | 58 | #### Redo 로그 목적 59 | (1) Database Recovery 60 | 61 | 물리적으로 디스크가 깨지는 등의 media fail 발생 시 복구를 위해 사용 62 | 63 | (2) Cache Recovery 64 | 65 | 버퍼캐시는 I/O 성능을 높히기 위해 사용되지만 휘발성이다. 66 | 67 | 인스턴스가 종료되면 버퍼캐시는 휘발되므로 이를 복구하기 위해 사용된다. 68 | 69 | (3) Fast Commit 70 | 71 | 변경사항을 우선 Append 방식으로 빠르게 로그 파일에 기록하고, 72 | 73 | 변경된 메모리 버퍼 블록과 데이터파일 블록 간 동기화는 적절한 수단?? (DBWR, Checkpoint) 을 이용해 추후 배치 방식으로 일괄 수행 74 | 75 | *** 변경된 메모리 버퍼블록을 디스크 상의 데이터 블록에 반영하는 작업 --> 랜덤 엑세스 --> 매우 느림 --> 그래서 Append 방식으로 76 | 77 |
78 | 79 | ### Undo 로깅과 DML 성능 80 | Undo 는 트랜잭션을 롤백함으로써 현재를 과거 상태로 되돌리는 데 사용한다. 81 | 82 | 83 | 84 | 85 | 오라클은 데이터를 입력, 수정, 삭제할 때마다 Undo 세그먼트에 기록을 남긴다. 86 | 87 |
88 | 89 | #### Undo 로그 목적 90 | (1) Transaction RollBack 91 | 92 | 트랜잭션에 의한 변경사항을 최종 커밋하지 않고 롤백하고자 할 때 사용 93 | 94 | (2) Transaction Recovery 95 | 96 | Redo 를 이용해 roll forward 단계가 완료되면 최종 커밋되지 않은 변경사항까지 모두 복구 된다. 97 | 98 | 따라서 셧다운 시점에 아직 커밋되지 않았던 트랜잭션들을 모두 롤백해야 하는데 이때 사용한다. 99 | 100 | 101 | (3) Read Consistency 102 | 103 | 데이터 읽기 일관성을 위해 사용 104 | 105 |
106 | 107 | #### MVCC 모델 (Multi-Version Concurrency Control Model) 108 | (1) Current 모드 109 | 110 | 디스크에서 캐시로 적재된 원본(current) 블록을 현재 상태 그대로 읽는 방식 111 | 112 | 113 | (2) Consistence 모드 114 | 115 | 다른 트랜잭션에 의해 변경된 블록을 만나면 원본 블록으로부터 복사본 블록을 만들고, 거기에 Undo 데이터를 적용 116 | 117 | 118 | 119 | 120 | 121 | ** SCN (System Commit Number) 122 | 123 | 마지막 커밋이 발생한 시점정보를 SCN Global 변수 값을 관리 124 | 125 | ** 블록 SCN 126 | 127 | 각 블록이 마지막으로 변경된 시점을 관리하기 위해 모든 블록 헤더에 SCN 을 기록 128 | 129 | ** 쿼리 SCN 130 | 131 | 모든 쿼리는 Global 변수인 SCN 값을 먼저 확인하고 읽기 작업 시작 이를 쿼리 SCN 이라고 함 132 | 133 | ** 다시 정리 134 | 135 | Consistence 모드는 쿼리 SCN 과 블록 SCN 을 비교함으로서 쿼리 수행 도중에 블록이 변경되었는지 확인하면서 데이터를 읽는 방식 136 | 137 | 데이터를 읽다가 블록 SCN 이 쿼리 SCN 보다 더 큰 블록을 만나면 복사본 블록을 만들고 Undo 적용 138 | 139 | 140 | 141 | 142 | 다시 정리 !! 143 | 144 | 145 | 146 | 147 | MySQL InnoDB 예제 !! 148 | 149 | 기본 세팅 150 | 151 | 152 | 153 | 데이터 UPDATE 154 | 155 | 156 | 157 | 트랜잭션 격리 수준에 따라 읽는 장소가 다르다 !! 158 | 159 | - READ_UNCOMMITTED: InnoDB 버퍼 풀이나 데이터 파일로부터 읽음 --> 데이터 커밋 상관 없이 읽음 160 | - 그 이상: Undo 영역을 읽음 161 | 162 | 163 | ** 예제 요약 164 | 165 | UPDATE 쿼리를 실행하면 InnoDB 버퍼 풀은 즉시 새로운 데이터로 변경되고, 기존 데이터는 Undo 영역으로 복사 166 | 167 | 여기서 COMMIT 명령을 실행하면 InnoDB 는 더 이상 변경 작업 없이 지금의 상태를 영구 데이터로 변환 168 | 169 | 하지만 롤백이 발생한다면 Undo 영역에 있는 백업된 데이터를 InnoDB 버퍼 풀로 다시 복구 그리고 Undo 영역 내용 삭제 170 | 171 | 172 | 173 |
174 | 175 | ### Redo vs Undo 176 | #### 개념 비교 177 | 178 | 179 | #### checkpoint 180 | 181 | 182 | 각 트랜잭션 이해하기 183 | 184 | 185 |
186 | 187 | #### ** 참고 188 | http://blog.skby.net/%EB%8B%A4%EC%A4%91-%EB%B2%84%EC%A0%84-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4-mvcc-multi-version-concurrency-control/ 189 | 190 | https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=dbmsexpert&logNo=220441276804 191 | 192 | https://mangkyu.tistory.com/53 -------------------------------------------------------------------------------- /6장_DML_튜닝/6.3_최락준.md: -------------------------------------------------------------------------------- 1 | # 6.3 파티션을 활용한 DML 튜닝 2 | 3 | 4 | ### 파티션이란 5 | - 하나의 테이블 또는 인덱스를 파티션 키에 따라 별도 세그먼트에 저장하는 것 6 | - 논리적으로는 하나의 테이블이지만 실제로 물리적으로는 여러 개의 테이블의 효과 7 | 8 | ### 파티션이 필요한 이유 9 | - 관리적 측면 : 파티션 단위의 작업 -> 같은 테이블이라도 서로 다른 파티션에 영향을 미치지 않음 -> 가용성 향상 10 | - 성능적 측면 : 경합 또는 부하 분산 11 | 12 | ## 6.3.1 테이블 파티션 13 | 14 | ### (1). 파티션 테이블의 성능 향상 원리 15 | * 파티션 테이블의 성능 향상 원리는 파티션 pruning을 통해 sql파싱이나 실행 시점에 조건절을 분석하여 **다른 파티션 세그먼트를 액세스 대상에서 제외**하는 것에 있다. 16 | * 예를 들어 파티션이 적용되지 않은 테이블에 1000만건의 데이터가 있다면 1000만 건 중 조건에 따른 데이터를 필터링 하지만, 17 | * 파티션이 적용된 경우 1000만건 중 파티션닝이 된 테이블만 액세스 하여 필터링 하기 때문에 성능상 이점이 있다. 18 | 19 | ### (2). range 파티션 20 | * 날짜 컬럼과 같이 범위 값을 기준으로 파티셔닝 21 | * 보관 주기 정책에 따라 관리가 달라지는 경우 효율적 (ex. 1년만 보관하고 백업하는 로그 테이블) 22 | 23 | ### (3) 해시 파티션 24 | * 파티션 개수만 사용자가 결정하고 데이터를 분산하는 알고리즘은 오라클 내부 해시함수가 결정 25 | * 해시 알고리즘 특성상 등치 조건 또는 In-List 조건에서만 파티션 Pruning이 작동 26 | 27 | ### (4) 리스트 파티션 28 | 29 | * 해시 파티션은 오라클 내부의 해시 알고리즘에 따라 임의로 분할하는 반면, 리스트 파티션은 사용자가 정의한 논리적인 그룹에 따라 분할 30 | * 예를 들어 사용자가 지역별로 파티션 키를 설정하면, '서울', '경기', '부산' 등과 같은 논리적 기준에 따라 파티셔닝 된다. 31 | 32 | ### 인덱스 파티션 33 | 34 | ### (1). 로컬 파티션 인덱스 35 | * 각 테이블 파티션과 1:1로 대응 되는 인덱스 파티션. 36 | * 예를 들어 테이블이 월별로 파티셔닝 되어 있다면, 각각의 파티셔닝 테이블에 마찬가지로 인덱스가 만들어 진다. 37 | * 인덱스 파티션은 테이블 파티션의 속성을 그대로 상속 받는다. 따라서 `테이블 파티션 키 == 인덱스 파티션 키`가 된다. 38 | * 파티션 테이블이 변경 되면 로컬 파티션 인덱스도 이에 맞추어 오라클이 자동으로 변경해 준다. 39 | 40 | > quiz 41 | ``` 42 | 파티셔닝 되지 않은 테이블의 경우 로컬 파티션 인덱스가 존재 할까요? 43 | ``` 44 | 45 | ### (2). 글로벌 파티션 인덱스 46 | 47 | * 파티션을 테이블과 다르게 구성한 인덱스. 로컬 인덱스가 아니면 **모두 글로벌 파티션 인덱스**이다. 48 | * 파티션 유형이 다르거나, 파티션 키가 다르거나, 파티션 기준값 정의가 다른 경우 49 | * 테이블 파티션 구성을 변경하는 경우 Unusable 상태로 바뀌므로 재생성해야 한다. 50 | 51 | ### (3). 비파티션 인덱스 52 | 53 | * 파틱셔닝 되지 않은 인덱스 54 | * 비파티션 인덱스와 파티션 테이블과 조합될 때, 테이블에 변경이 있으면 인덱스를 재생성해야 한다. 55 | 56 | ![](images/partition1.jpeg) 57 | 58 | 59 | ## 6.3.3 파티션을 활용한 대량 update 튜닝 60 | 61 | 10억 건의 데이터가 저장된 테이블에서 5%(5000만 건)의 데이터 변경이 있을 경우 62 | 63 | ### 1) 5000만 건을 update 64 | -> 5000 만 건에 대한 select/update, 그리고 index 수정에 대한 부담 발생 65 | 66 | ### 2). index 를 drop 하고 5000만 건을 update 67 | -> 10억 건에 대한 index 재생성 부담 발생 68 | 69 | ### 3). 파티션 exchange를 이용 70 | * 테이블이 파티셔닝이 되어 있고 인덱스도 로컬 파티션으로 구성되어 있을 경우 가능 71 | ```sql 72 | (1). 먼저 임시 테이블을 만들고 73 | (2). 임시 테이블에 update 될 데이터를 insert 74 | (3). 임시 테이블에 인덱스를 생성 75 | (4). 기존 파티션 테이블과 임시 테이블을 exchange 76 | (5). 임시 테이블 drop 77 | ``` 78 | 79 | ![](images/partition2.jpeg) 80 | ## 6.3.4 파티션을 활용한 대량 delete 튜닝 81 | 82 | update 는 변경 대상 컬럼을 포함하는 인덱스만 재생성하면 되지만, delete는 모든 인덱스를 재생성 해야 하기 때문에 83 | 인덱스 재생성 비용이 더 크다. 84 | 85 | ### 1) 파티션 drop을 이용한 대량 데이터 삭제 86 | 87 | * 삭제 조건절 컬럼 기준으로 파티셔닝 되어 있고, 로컬 인덱스일 경우 간단히 삭제 가능 88 | * 예를 들어 월별로 파티셔닝 되어 있고, 1월 파티션을 통째로 삭제하는 경우 89 | ```sql 90 | alter table 거래 drop partition p201412; 91 | ``` 92 | 93 | ### 2) 파타션 truncate를 이용한 대량 데이터 삭제 94 | * 위와 같이 파티셔닝 기준으로 일괄 삭제하지 않고 또 다른 삭제 조건이 있는 경우 95 | * 지워야할 데이터가 대다수라면, 데이터를 지우는 것 보다 전부 삭제 후 남길 데이터를 insert 하는 것이 낫다. 96 | ```sql 97 | (1). 임시 테이블 생성, 남길 데이터만 insert 98 | (2). 삭제 대상 테이블 파티션을 truncate 99 | (3). 임시 테이블에 복제한 데이터를 원본 테이블에 insert (insert select 사용) 100 | (4). 임시 테이블 drop 101 | ``` 102 | 103 | ## 6.3.5 파티션을 활용한 대량 insert 튜닝 104 | 105 | ### 1) 비파티션 테이블일 때 106 | * 비파티션 테이블에서 대량 데이터를 insert 할 때, 인덱스를 unusable 시킨 후 재생성하는 방식이 더 빠를 수 있다. 107 | 108 | ### 2) 파티션 테이블일 때 109 | 테이블이 파티셔닝돼 있고, 로컬 인덱스라면 파티션 단위로 인덱스를 재생성 할 수 있다. 110 | 111 | ![](images/partition3.jpeg) 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /6장_DML_튜닝/6.4_Lock과_트랜잭션_동시성_제어_황준호.md: -------------------------------------------------------------------------------- 1 | ### 6.4 Lock과 트랜잭션 동시성 제어 2 | 3 | - 자신이 사용하는 데이터베이스의 Lock 매커니즘과 트랜잭션 동시성 제어에 대한 학습은 필수다! 4 | 5 | - 오라클 Lock 종류 6 | 7 | 1. DML Lock 8 | - 어플리케이션 개발 측면에서 가장 중요하게 다뤄야 할 Lock 9 | - 다중 트랜잭션이 동시에 액세스하는 사용자 데이터의 무결성을 보호해준다 10 | - DML Lock엔 테이블 Lock과 로우 Lock이 있다 11 | 2. DDL Lock 12 | 3. 래치 13 | - SGA에 공유된 각종 자료구조를 보호하기 위해 사용 14 | 4. 버퍼 Lock 15 | - 버퍼 블록에 대한 엑세스를 직렬화하기 위해 사용 16 | 5. 라이브러리 캐시 Lock/Pin 17 | - 라이브러리 캐시에 공유된 SQL 커서와 PL/SQL 프로그램을 보호하기 위해 사용 18 | 19 | - DML 로우 Lock 20 | 21 | - 두 개의 동시 트랜잭션이 같은 로우를 변경하는 것을 방지함 22 | - 같은 로우를 UPDATE/DELETE/INSERT를 시도할때 23 | - UPDATE, DELETE : 진행중인(아직 커밋하지 않은) 로우를 다른 트랜잭션이 수정할 수 없다 24 | - INSERT : unique인덱스가 있고 같은 값을 입력하려고 할때 블로킹이 발생한다. 25 | 1. 선행 트랜잭션이 커밋할 동안 후행 트랜잭션은 기다린다 26 | 2. 선행 트랜잭션이 커밋하면 후행 트랜잭션은 실패하고, 선행 트랜잭션이 롤백하면 후행 트랜잭션은 성공한다 27 | - DML 로우 Lock에 의한 성능 저하를 방지하려면 Lock을 너무 오래 유지하지 않도록 커밋 시점을 조절해야 한다 28 | 29 | - DML 테이블 Lock (= TM Lock) 30 | 31 | - 현재 트랜잭션이 갱신 중인 테이블 구조를 다른 트랜잭션이 변경하지 못하게 하기 위해 로우 Lock 이전에 테이블 Lock을 설정한다 32 | 33 | - 로우 Lock은 1가지 Lock 모드(배타적 모드)만 사용하는 반면 테이블 Lock에는 여러 Lock 모드(RS, RX, S, SRX, X)가 있다 34 | 35 | - 테이블 Lock을 테이블 전체에 걸리는 Lock이라고 오해하기 쉬운데, 그게 아니라 테이블 Lock을 설정한 트랜잭션이 해당 테이블에서 어떤 작업을 수행중인지 알리는 일종의 푯말이라고 이해하면 좋다 (후행 트랜잭션은 그 푯말을 보고 기다릴지, 포기할지 결정된다) 36 | 37 | - Lock이 해제될때까지 기다린다 or 일정 시간만 기다리다 포기한다 or 기다리지 않고 포기한다 38 | 39 | - 예) DDL을 이용하여 테이블 구조를 변경하려 할때 40 | 41 | - 테이블 구조를 변경하려는 트랜잭션은 해당 테이블에 TM Lock이 설정돼있는지 확인한다 42 | 43 | - RX모드로 설정한 트랜잭션이 하나라도 있다면 작업을 멈춘다 44 | - DDL이 먼저 수행중이라면 DML을 수행하려는 트랜잭션이 기다린다 45 | 46 | - 블로킹 vs 교착상태 47 | 48 | - 블로킹 : 선행 트랜잭션이 설정한 Lock 때문에 후행 트랜잭션이 기다리는 상태 49 | - 교착상태 : 두 트랜잭션이 각각 특정 리소스에 Lock을 설정한 상태에서 두 트랜잭션 모두 맞은편 트랜잭션의 리소스에 Lock을 설정하려고 하는 상황 50 | - 오라클에서는 교착상태가 발생하면 이를 먼저 인지한 트랜잭션이 문장 수준 롤백을 진행한 후 에러 메시지를 던진다 51 | 52 | - 트랜잭션 동시성 제어는 비관적 동시성 제어와 낙관적 동시성 제어로 나뉜다 53 | 54 | 1. 비관적 동시성 제어 55 | - 사용자들이 같은 데이터를 동시에 수정할 것으로 가정하고, 한 사용자가 데이터를 읽는 시점에 Lock을 걸고 조회/갱신이 완료될때까지 이를 유지한다 56 | - `select ... for update`로 비관적 락을 걸 수 있다 57 | - 동시성을 심각하게 떨어뜨릴 우려가 있지만, `for update wait ...`, `for update nowait` 옵션으로 동시성을 증가시킬 수 있다 58 | 2. 낙관적 동시성 제어 59 | - 사용자들이 같은 데이터를 동시에 수정하지 않을 것으로 가정하고, 데이터를 읽을때 Lock을 설정하지 않는다. 읽는 시점에 변경된 데이터인지 반드시 검사해야 한다 60 | - 모든 컬럼에 대해 변경이 됐는지 확인하거나, 수정시간에 대한 컬럼을 따로 만들어서 수정시간이 변경됐는지 확인한다 61 | 62 | - 데이터 품질과 동시성 향상을 위해선 63 | 64 | - for update 사용을 두려워하지 말아야 한다 65 | - for update가 필요한 상황이면 정확히 사용하고, 번거롭더라도 wait 또는 nowait 옵션을 활용하여 예외처리에 주의를 기울어야 한다 66 | - 불필요하게 Lock을 오래 유지하지 않고, 트랜잭션의 원자성을 보장하는 범위 내에서 가급적 빨리 커밋해야 한다 67 | 68 | - 채번 방식 69 | 70 | - 채번 : 신규 데이터를 입력할때 PK 중복을 방지하기 위한 과정 71 | 72 | - 채번 방식 종류 73 | 74 | 1. 채번 테이블 75 | 76 | - 채번하기 위한 별도의 테이블을 관리하는 방식 77 | - 채번 레코드를 읽어서 1을 더한 값으로 변경하고, 그 값을 새로운 레코드를 입력하는데 사용한다 78 | - 두 트랜잭션이 중복 값을 채번할 가능성을 원천적으로 방지해준다 79 | - 장점 80 | - 범용성이 좋다 81 | - 중복 레코드 발생에 대비하지 않아도 된다 82 | - INSERT 과정에 결번을 방지할 수 있다 83 | - PK가 복합컬럼일때도 사용할 수 있다 84 | - 단점 85 | - 채번 레코드를 변경하기 위한 로우 Lock 경합 때문에 성능이 안좋다 (그래서 동시 INSERT가 아주 많으면 사용하기 힘들다) 86 | 2. 시퀀스 오브젝트 87 | 88 | - 시퀀스 오브젝트? 오라클 내부에서 관리하는 채번 테이블 89 | 90 | - 장점 91 | - 성능이 빠르다 92 | - 중복 레코드 발생에 대비하지 않아도 된다 93 | 94 | - 단점 95 | - 테이블 별로 시퀀스 오브젝트를 생성하고 관리하는 부담이 있다 96 | - 시퀀스 채번 과정에서 Lock에 의한 성능 이슈가 있다 (단, 캐시 사이즈를 적절히 설정하면 빠른 성능을 제공한다) 97 | - PK가 단일컬럼일때만 사용할 수 있다 98 | - 결번이 생길 수 있다 99 | 3. MAX + 1 조회 100 | - 예 : `select max(거래일련번호) + 1 from 거래`의 값을 새로 insert할 row의 pk로 사용 101 | - 장점 102 | - 시퀀스나 채번 테이블을 관리하지 않아도 된다 103 | - 동시 트랜잭션에 의한 충돌이 많지 않으면 성능이 매우 좋다 104 | - PK가 복합컬럼이어도 사용할 수 있다 105 | - 단점 106 | - 레코드 중복에 대비한 세밀한 예외처리가 필요하다 107 | - 다중 트랜잭션에 의한 동시 채번이 심하면 시퀀스보다 성능이 많이 나빠질 수 있다 -------------------------------------------------------------------------------- /6장_DML_튜닝/6장-1_김민석.md: -------------------------------------------------------------------------------- 1 | # 6장 소트 튜닝 2 | 3 | ## 6.1 기본 DML 튜닝 4 | 5 | ### DML 성능에 영향을 미치는 요소 6 | 7 | #### 인덱스 8 | 9 | 인덱스가 늘어나면 Insert / Delete / Update 시 부하가 늘어난다. 특히 Update 시에는 변경과 인덱스 수조 내에서 정렬을 다시해야 하는 이중적인 부하가 발생한다. 10 | 11 | #### 무결성 제약 12 | 13 | 무결성 제약 중 PK/FK 제약은 Check / Not Null 보다 성능에 더 큰 영향이 있다. 실제 데이터를 조회하기 때문이다. 14 | 15 | #### 조건절 16 | 17 | 2/3장에서 학습한 인덱스 튜닝 원리 참고. 18 | 19 | #### 서브쿼리 20 | 21 | 4장에서 학습한 조인 튜닝 원리 참고. 22 | 23 | #### Redo 로깅 24 | 25 | DML 수행 시마다 Redo 로그 생성하기 때문에 DML에 성능에 영향을 끼친다. 26 | 27 | #### Undo 로깅 28 | 29 | DML 수행 시마다 Undo 로그 생성하기 때문에 DML에 성능에 영향을 끼친다. 30 | 31 | #### Lock 32 | 33 | Lock은 DML 성능에 직접적인 영향을 끼친다. 성능과 데이터품질은 트레이드오프 관계. 34 | 35 | #### 커밋 36 | 37 | - DB 버퍼캐시 38 | - 서버 프로세스는 버퍼캐시를 통해 데이터를 일괄 처리한다. 39 | - Redo 로그버퍼 40 | - Redo 성능을 위해 Redo 파일 중간에 버퍼 위치. 41 | - 트랜잭션 데이터 저장 과정 42 | - DML 문을 실행하면 Redo 로그버퍼에 변경사항 기록 43 | - 버퍼블록에서 데이터 변경 44 | - 커밋 45 | - Redo 로그버퍼 파일에 일괄 저장 46 | - 버퍼블록을 데이터파일에 일괄 저장 47 | - 커밋 48 | - 커밋을 오래동안 안하는 것도 안좋지만 너무 자주하는 것도 IO를 발생시키기 때문에 좋지 않다 49 | 50 | ### 데이터베이스 Call과 성능 51 | 52 | - SQL Call 세 단계 53 | - Parse call: SQL 파싱과 최적화 수행 54 | - Execute call: SQL 실행 55 | - Fetch call: 데이터를 읽어서 사용자에게 전송 56 | 57 | - 발생 시점에 따른 Call 분류 58 | - User call: DBMS 외부로부터 인입되는 call 59 | - Recursive call: DBMS 내부에서 발생하는 call 60 | 61 | - 절차적 루프 처리 62 | - 절차적으로 반복되는 call일 경우 그나마 recursive 콜일 경우는 낫다. user call은 느려질 수밖에 없다. 63 | - One SQL의 중요성 64 | - 로직이 복잡하지 않으면 되도록 하나의 SQL로 끝나는 게 성능에 좋다. 65 | 66 | ### Array Processing 활용 67 | 68 | 복잡한 업무 로직을 One SQL로 구현하는 것은 어렵다. 이럴 때 Array Processing을 활용한다. 69 | 70 | ### 인덱스 및 제약 해제를 통한 대량 DML 튜닝 71 | 72 | 동시 트랜잭션이 없는 배치 환경에서는 PK 및 인덱스 제약 헤제를 통하여 성능을 향상 시킬 수 있다. 73 | 74 | ### 수정가능 조인 뷰 75 | 76 | 인라인 뷰가 포함된 update 쿼리문은 튜닝을 통하여 속도 향상이 가능하다. 77 | 78 | ### Merge 문 활용 79 | 80 | 데이터웨어하우스에서는 두 시스템 간 데이터 동기화가 가장 흔히 발생하는 작업이다. 81 | 이때 오라클9i에서 도입된 MERGE 문을 활용할 수 있다. 82 | 83 | ## 6.2 Direct Path I/O 84 | 85 | 정보계 시스템(DW/OLAP)이나 배치작업에서는 버퍼캐시 경유하지 않는 것이 더 이득이다. 버퍼캐시를 경유하지 않고 바로 데이터 블록을 읽고 쓰는 것을 Direct Path I/O 라고 한다. 86 | 87 | ### Direct Path I/O 88 | 89 | 아래 경우에 사용한다. 90 | 91 | 1. 병렬 쿼리로 Full Scan울 수행 할 때 92 | 2. 병렬 DML을 수행할 때 93 | 3. Direct Path Insert를 수행할 떄 94 | 4. Temp 세그먼트 블록들을 읽고 쓸 때 95 | 5. direct 옵션을 지정하고 export를 수행할 때 96 | 6. nocache 옵션을 지정한 LOB 컬럼을 읽을 때 97 | 98 | ### Direct Path Insert 99 | 100 | Insert 작업 시 Direct Path Insert 를 사용하면 아래와 같은 이점이 있다 101 | 102 | - Freelist를 참조하지 않는다 103 | - 블록을 버퍼캐시에서 탐색하지 않는다 104 | - 버퍼캐시를 사용하지 않는다. 105 | - Undo 로깅을 안한다 106 | - Redo 로깅을 안하게 할 수 있다. 107 | 108 | ### 병렬 DML 109 | 110 | UPDATE / DELETE 는 기본적으로 Direct Path Write가 불가능하기 때문에 병렬 DML 로 유도한다. 111 | 112 | 113 | -------------------------------------------------------------------------------- /6장_DML_튜닝/images/partition1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/6장_DML_튜닝/images/partition1.jpeg -------------------------------------------------------------------------------- /6장_DML_튜닝/images/partition2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/6장_DML_튜닝/images/partition2.jpeg -------------------------------------------------------------------------------- /6장_DML_튜닝/images/partition3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/6장_DML_튜닝/images/partition3.jpeg -------------------------------------------------------------------------------- /6장_DML_튜닝/조수진.md: -------------------------------------------------------------------------------- 1 | # Direct Path I/O 활용 2 | 일반적으로 블록 I/O는 버퍼캐시를 경유 3 | 4 | - 온라인 트랜잭션은 기준성 데이터, 특정 고객, 특정 상품, 최근 거래 등을 반복적으로 읽기 때문에 유용 5 | - 정보계 시스템 혹은 배치성 프로그램에서는 주로 대량 데이터를 처리하기 때문에 버퍼캐시를 경우하는 것이 오히려 성능을 떨어뜨림 6 | 7 | ### Direct Path I/O 8 | 9 | - 버퍼 캐시 목적: 자주 쓰는 블록에 대한 I/O call을 줄여서 시스템 전반의 성능을 높이기 위함 10 | - 대량 데이터를 읽고 쓸 때는 캐시 히트율이 낮아서 효율이 떨어짐 11 | - 읽을 때도, 건건이 디스크에서 버퍼캐시에 적재하고 읽어야하므로 효율 낮음 12 | 13 | → Direct Path I/O는 버퍼캐시를 경유하지 않고 곧바로 데이터를 읽고 쓸 수 있는 기능 14 | 15 | ### Direct Path I/O 작동 16 | 17 | 1. 병렬 쿼리로 Full Scan 수행 시 18 | 2. 병렬 DML을 수행 시 19 | 3. Direct Path Insert를 수행 시 20 | 4. Temp 세그먼트 블록들을 읽고 쓸 때 21 | 5. direct 옵션을 지정하고 export를 수행할 때 22 | 6. nocache 옵션을 지정한 LOB 컬럼을 읽을 때 23 | 24 | ### 일반적인 Insert 과정 25 | 26 | - 데이터를 입력할 수 있는 블록을 freelist에서 찾는다 27 | - freelist에 할당받은 블록을 버퍼캐시에서 찾는다 28 | - 버퍼캐시에 없으면 데이터 파일에서 읽어 버퍼캐시에 적재 29 | - Insert 내용을 undo 세그먼트에 기록 30 | - Insert 내용을 redo 세그먼트에 기록 31 | 32 | ### Direct Path Insert 33 | 34 | - freelist 참조하지 않고 HWM 바깥 영역에 데이터를 순차적으로 입력 35 | - 버퍼캐시에서 블록 탐색하지 X 36 | - 버퍼캐시에 적재하지 않고 데이터파일에 직접 기록 37 | - Undo 로깅 최소화할 수 있음 38 | - Redo 로깅을 최소화할 수 있음(데이터 딕셔너리 변경만 로깅) 39 | - table을 nologging 모드로 전환하고 Direct path insert 실행 40 | 41 | `alter table t NOLOGGING;` 42 | 43 | 44 | ### Direct Path Insert 사용 방법 45 | 46 | - Insert ... select 문에 append 힌트 사용 47 | - parallel 힌트를 이용해 병렬 모드로 INSERT 48 | - direct 옵션을 지정하고 SQL Loader(sqlldr)로 데이터 적재 49 | - CTAS(create table ... as select) 문 수행 50 | 51 | ### Direct Path Insert 사용시 주의점 52 | 53 | - Exclusive 모드 TM Lock이 걸린다 54 | 55 | → 커밋전에 다른 트랜잭션이 해당 테이블에 DML을 수행하지 못한다 56 | 57 | → 트랜잭션이 빈번한 주간에 이 옵션을 사용하는 것은 절대 금물 58 | 59 | - Freelist를 조회하지 않고 HWM 영역에 입력하므로 테이블의 여유공간을 재활용하지 않는다 60 | 61 | → Range 파티션 테이블이면 과거 데이터를 DELETE가 아닌 파티션 DROP 방식으로 지워야 공간 반환이 제대로 이루어진다 62 | 63 | → 비파티션 테이블이면 주기적으로 Reorg 작업을 수행해줘야 한다 64 | 65 | 66 | ### 병렬 DML 67 | 68 | 병렬 처리는 대용량 데이터가 전제이므로 오라클은 병렬 DML에 항상 Direct Path Write 방식 사용 69 | 70 | Insert와 달리 Update, delete는 기본적으로 Direct Path Write을 위해 병렬 DML로 처리하는 방법이 유일 71 | 72 | - append 힌트 사용 불가능 73 | 74 | ```sql 75 | alter session enable paralle dml; // 병렬 DML 활성화 76 | 77 | insert /*+ parallel(c 4) */ into 고객 c 78 | select /*+ fulll(o) paralle(o 4) */ * from 외부가입고객 o; 79 | 80 | update /*+ full(c) paralle(c 4) */ 고객 c set 고객상태코드 = 'WD' 81 | where 최종거래일시 < '20100101'; 82 | 83 | delete /*+ full(c) paralle(c 4) */ from 고객 c 84 | where 탈퇴일시 < '20100101'; 85 | ``` 86 | 87 | → 대상 레코드 찾는 작업(select, wherer 절) & 데이터 추가/변경/삭제 모두 병렬로 진행 88 | 89 | - parallel 힌트는 사용했지만 병렬 DML을 활성화하지 않으면 레코드 검색은 병렬로 진행, 추가/변경/삭제는 QC가 혼자 담당해서 병목이 생긴다 90 | - QC(Query Coordinator): 최초로 DB에 접속해서 SQL을 수행한 프로세스 91 | - 병렬 Insert는 append 힌트 지정하지 않아도 Direct path insert로 동작하지만, 병렬 DML이 동작하지 않을 경우를 대비해 append도 같이 써주는 것을 추천 92 | 93 | ```sql 94 | insert /*+ append parallel(c 4) */ into 고객 c 95 | select /*+ fulll(o) paralle(o 4) */ * from 외부가입고객 o; 96 | ``` 97 | 98 | - 오라클 12c 부터 `enable_paralle_dml` 힌트 지원 99 | 100 | ```sql 101 | insert /*+ enable_parallel_dml parallel(c 4) */ into 고객 c 102 | select /*+ full(o) parallel(o 4) */ * from 외부가입고객 o; 103 | ``` 104 | 105 | ### 병렬 DML이 잘 동작하는지 확인하는 방법 106 | 107 | - update(또는 delete/insert)가 ‘PX COORDINATOR’아래쪽에 나타나면 Update를 각 병렬 프로세스가 처리 108 | - 반대로 update가 ‘PX COORDINATOR’보다 위쪽에 나타나면 update를 QC가 처리 109 | 110 | ![Untitled](/img/parallel%20dml%20execute%20plan%20pic.png) -------------------------------------------------------------------------------- /7장_SQL_옵티마이저/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/7장_SQL_옵티마이저/.empty -------------------------------------------------------------------------------- /7장_SQL_옵티마이저/7.1_김민석.md: -------------------------------------------------------------------------------- 1 | # 7.1 통계정보와 비용 계산 원리 2 | 3 | ## 7.1.1 선택도와 카디널리티 4 | 5 | 6 | 선택도 : 전체 레코드 중 조건절에 의해 선택되는 레코드 비율 7 | 8 | 9 | '=' 조건으로 검색 할 때 경우 10 | 11 | `선택도 = 1 / NDV` 12 | 13 | NDV : 컬럼값 종류 개수(성별이라면 남/여 2) 14 | 15 | 16 | 카디널리티 : 전체 레코드 중에서 조건절에 의해 선택되는 레코드 개수 17 | 18 | `카디널리티 = 총 로우 수 * 선택도 = 총 로우 수 / NDV` 19 | 20 | *옵티마이저는 카디널리티 값을 통하여 비용을 산출하고 비용을 통하여 옵티마이징 방식을 결정한다. 따라서 카디널리티값의 기초가 되는 선택도 값의 계산이 매우 중요하다. 21 | 22 | 23 | ## 7.1.2 통계정보 24 | 25 | ### 오브젝트 통계 26 | 27 | #### 테이블 통계 28 | 29 | 테이블 통계 조회 30 | ```sql 31 | select 32 | num_rows -- 총 레코드 계수 33 | ,blocks -- 테이블 블록 수 = '사용된' 익스텐트에 속한 총 블록 수 34 | ,avg_row_len -- 레코드당 평균 길이 35 | ,sample_size -- 샘플링한 레코드 수 36 | ,last_analyzed -- 통계정보 수집일시 37 | from all_tables 38 | where owner='SCOTT' 39 | and table_name = 'EMP' 40 | ; 41 | ``` 42 | 43 | #### 인덱스 통계 44 | 45 | 46 | 인덱스 통계 조회 47 | ```sql 48 | select blevel -- 브랜치 레벨. 인덱스 루트에서 블록에 도달하기 직전까지 읽게되는 블록 수 49 | ,leaf_blocks -- 인덱스 리프 블록 총 개수 50 | ,num_rows -- 인덱스에 저장된 레코드 개수 51 | ,distnct_keys -- 인덱스 키값의 조합으로 만들어지는 값의 종류 개수. c1(3) * c2(4) = 12. 선택도 계산에 사용. 52 | ,avg_leaf_blocks_per_key -- 인덱스 키값을 모두 '=' 조건으로 조회할 때 읽게 될 리프 블록 개수 53 | ,avg_data_blocks_per_key -- 인덱스 키값을 모두 '=' 조건으로 조회할 때 읽게 될 테이블 블록 개수 54 | ,clustering_factor -- 인덱스 키값 기준으로 테이블 데이터가 모여 있는 정도. 인덱스를 스캔하면서 테이블 레코드를 찾을 때 읽게 될 테이블 블록 개수를 미리 계산 한 수치 55 | ,sample_size 56 | ,last_analyzed 57 | from all_indexes 58 | where owner = 'SCOTT' 59 | and table_name = 'EMP' 60 | and index_name = 'EMP_X01' 61 | ; 62 | 63 | 64 | ``` 65 | 66 | #### 컬럼 통계 67 | 68 | 컬럼 통계 조회 69 | 70 | ```sql 71 | select num_distinct -- 컬럼 값의 종류 개수(NDV) 72 | , density -- '=' 조건일 때 선택도. 1/NDV 73 | , avg_col_len -- 컬럼 평균 길이(bytes) 74 | , low_value -- 최소값 75 | , high_value -- 최대값 76 | , num_nulls -- null 인 레코드 수 77 | , last_analyzed 78 | , sample_size 79 | from all_tab_columns 80 | where owner = 'SCOTT' 81 | and table_name = 'EMP' 82 | and column_name = 'DEPTNO' 83 | ; 84 | ``` 85 | 86 | 컬럼 히스토그램 87 | 88 | '=' 조건 선택도는 1/NDV 나 Density 값을 활용. 89 | 90 | 하지만 데이터 분포가 균열하지 않다면 적용하기 쉽지 않기 때문에 이런 경우 옵티마이저는 통계 외에 히스토그램 사용. 91 | 92 | 히스토그램 : 컬럼 값 별로 데이터 비중이나 빈도를 미리 계산해 놓은 통계정보. 실제 테이터를 읽어서 계산해 둔 값. 93 | 94 | - 히스토그램 유형 95 | - 도수분포 : 값별로 빈도수 저장 96 | - 높이균형 : 각 버킷의 높이가 동일하도록 데이터 분포 관리 97 | - 상위도수분포 : 많은 레코드를 가진 상위 n개 값에 대한 빈도수 저장 98 | - 하이브리드 : 도수분포와 높이균형 특성 결합 99 | 100 | 101 | 히스토그램 조회 102 | 103 | ```sql 104 | select endpoint_value, endpoint_number 105 | from all_histograms 106 | where owner = 'SCOTT' 107 | and table_name = 'EMP' 108 | and column_name = 'DEPTNO' 109 | order by endpoint_value 110 | ; 111 | ``` 112 | 113 | ### 시스템 통계 114 | 115 | 시스템 통계 : 애플리케이션 및 하드웨어 성능 특성의 측정값 116 | 117 | - CPU 속도 118 | - 평균 Single Block I/O 속도 119 | - 평균 Multiblock I/O 속도 120 | - 평균 Multiblock I/O 개수 121 | - I/O 서브시스템의 최대 처리량 122 | - 병렬 Slave의 평균적인 처리량 123 | 124 | ## 7.1.3 비용 계산 원리 125 | 126 | - 단일 테이블을 인덱스로 액세스할 때의 비용 계산 원리 127 | 128 | '=' 조건 검색 시 129 | 130 | ``` 131 | 비용 = BLEVEL -- 인덱스 수직적 탐색 비용 132 | + AVG_LEAF_BLOCKS_PER_KEY -- 인덱스 수평적 탐색 비용 133 | + AVG_DATA_BLOCKS_PER_KEY -- 테이블 랜덤 액세스 비용 134 | ``` 135 | 136 | - blevel : 브랜치 레벨. 인덱스 루트에서 블록에 도달하기 직전까지 읽게되는 블록 수 137 | - avg_leaf_blocks_per_key : 인덱스 키값을 모두 '=' 조건으로 조회할 때 읽게 될 리프 블록 개수 138 | - avg_data_blocks_per_key : 인덱스 키값을 모두 '=' 조건으로 조회할 때 읽게 될 테이블 블록 개수 139 | 140 | 141 | '=' 조건 검색 아닐 시(아래 컬럼 통계까지 활용) 142 | 143 | ``` 144 | 비용 = BLEVEL -- 인덱스 수직적 탐색 비용 145 | + LEAF_BLOCKS * 유효 인덱스 선택도 -- 인덱스 수평적 탐색 비용 146 | + CLUSERING_FACTOR * 유효 테이블 선택도 -- 테이블 랜덤 액세스 비용 147 | ``` 148 | 149 | - leaf_blocks : 인덱스 리프 블록 총 개수 150 | - clustering_factor : 인덱스 키값 기준으로 테이블 데이터가 모여 있는 정도. 인덱스를 스캔하면서 테이블 레코드를 찾을 때 읽게 될 테이블 블록 개수를 미리 계산 한 수치 151 | - 유효 인덱스 선택도 : 전체 인덱스 레코드 중 액세스 조건에 의해 선택될 것으로 예상되는 레코드 비중 152 | - 유효 테이블 선택도 : 전체 인덱스 레코드 중 인덱스 컬럼에 대한 모든 조건절에 의해 선택 될 것으로 예상되는 레코드 비중 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 친절한 SQL 튜닝 스터디 2 | 3 | https://www.notion.so/SQL-2b8c5a039c204901ad8afd109b34ffcd 4 | -------------------------------------------------------------------------------- /img/pic3-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/img/pic3-3.png -------------------------------------------------------------------------------- /img/pic3-47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/img/pic3-47.png -------------------------------------------------------------------------------- /img/pic_1-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/img/pic_1-12.png -------------------------------------------------------------------------------- /img/pic_1-27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/img/pic_1-27.png -------------------------------------------------------------------------------- /img/pic_1-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/img/pic_1-4.png -------------------------------------------------------------------------------- /img/table3-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Meet-Coder-Study/nice-sql-tuning/6ebb1473a41c04d97351f3527a5c2311b4fc4973/img/table3-8.png --------------------------------------------------------------------------------