개요
- 쿼리를 효율적으로 짜기 위해서는 옵티마이저의 동작원리를 잘 알아야 된다.
- 즉, 옵티마이저의 마음을 이해하는 것이 중요하다.
- 하지만 생각보다 옵티마이저의 동작원리를 이해하는 것은 복잡하고 깊은 내용이다.
- 그렇지만 간단하게라도 옵티마이저에 대해서 정리하고 넘어가려고 한다.
먼저 쿼리가 어떤식으로 실행되는지 간단하게 알아보자.
쿼리 실행 흐름
Parser
- 입력받은 쿼리의 문법을 검증하고 Parse Tree를 생성한다.
Analyzer
- Parser로 부터 전달받은 Parse Tree를 바탕으로 Query Tree를 생성한다.
- pg_catalog에 담겨 있는 정보를 바탕으로 테이블, 칼럼, 제약조건 등에 대한 Sematic 검증을 수행한다.
- Query Tree는 테이블, 칼럼을 참조하기 위한 정보를 포함한다.
Rewiter
- Query Tree가 Optimizer에게 전달되기 전에 사용자가 Rule System에 저장한 규칙을 기반으로 Query Tree를 수정한다.
Optimizer
- Query Tree에 담긴 참조정보를 바탕으로 데이터를 찾고 아래 3단계를 거쳐서 실행계획을 생성한다.
- Query Transformer
- 사용자에 의해 제공된 원래 쿼리를 분석하고 변환한다.
- 쿼리는 더 효율적으로 실행될 수 있도록 재작성될 수 있다. 예를 들어, 서브쿼리는 조인으로 변환될 수 있다.
- 또한, 필요하지 않은 칼럼을 제거하거나, 쿼리 조건을 간소화하는 등의 최적화가 이루어질 수 있다.
- Cost Estimator
- 다양한 실행 경로의 비용을 추정한다.
- 각각의 실행 계획에 대한 비용을 평가하기 위해 데이터베이스의 통계 정보를 사용한다.
- 비용을 계산하는 방법은 아래에서 좀 더 자세히 살펴보자.
- Plan Generator
- 비용 추정을 바탕으로 최적의 실행 계획을 선택한다.
- 여러 가능한 실행 계획 중에서 비용이 가장 낮은 계획을 선정한다.
- 여기서 결정된 실행 계획은 실제 쿼리를 실행하는 데 사용된다.
- 생성된 Plan Tree를 Executor에게 전달한다.
Executor
- Optimizer로 부터 전달 받은 Plan Tree안에 있는 Plan Node들을 순서에 맞시 실행하면서 필요한 Row들을 받아온다.
이제 본격적으로 옵티마이저에 대해서 알아보자.
옵티마이저란?
옵티마이저는 쿼리를 실행할 때 가장 효율적인 방법을 찾아주는 역할을 한다.
Query 가 수행될 수 있는 모든 실행 경로에 대해서 비교해보고 가장 비용이 적은 실행 경로를 실행계획으로 수립한다.
옵티마이저는 크게 2가지로 유형으로 나뉜다.
- 첫 번째는
RBO(Rule Based Optimizer)
이다.- RBO는 사전에 정의된
규칙에 의해 실행계획이 수립되는 방식
이다. - 장점은
실행계획을 예측하기 쉽고, 매우 직관적
이다. - 단점은 상황에 따라서 비효율적일 수 있다. 사전에 정의된 규칙이 모든 상황에 최적일 수 없기 때문이다.
- 이러한 단점으로 인해서 RBO 방식의 옵티마이저는 거의
사용되지 않는다
.
- RBO는 사전에 정의된
- 두 번째는
CBO(Cost Based Optimizer)
이다.- CBO는 말 그대로
비용을 계산하는 방식
이다. - 이 비용은
통계정보를 바탕으로 계산
된다. 따라서상황에 따라서 가장 비용이 적은 방법을 찾는다.
- 따라서 요즘 같이 복잡한 환경 속에서 매우 유리한 방식이다.
PostgreSQL의 옵티마이저도 CBO 방식이다.
- 단점은 상황에 따라서 옵티마이저가 실행계획을 수립하기 때문에,
실행계획을 정확히 예측하기 어렵다.
- CBO는 말 그대로
통계정보는 옵티마이저가 실행계획을 세우는 기준이 된다. 따라서 통계정보에 대해서도 알고 넘어갈 필요가 있다.
통계정보
- 옵티마이저는
테이블, 인덱스, 칼럼에
대한통계정보
를 바탕으로 비용을 계산한다. - 통계정보는
pg_class
와pg_stats
를 통해서 확인할 수 있다. - 이제
pg_class
와pg_stats
테이블에 있는 데이터를 자세히 살펴보자.
테이블 세팅
- 아래와 같이 employee 테이블이 있다고 가정하고, 데이터는 10000건이 들어있다.
create table employee
(
employee_id bigint generated by default as identity
constraint employee_pkey
primary key,
begin_date varchar(8),
employee_name varchar(255),
end_date varchar(8)
);
pg_class
pg_class
테이블에는블록 수
와레코드 수
를 확인할 수 있다.
- relpages: 블록 수 84개 (relpages는 테이블이나 인덱스가 차지하는 디스크 상의 블록 수를 말한다)
- reltuples: 튜블 수 10000개
pg_stats
- pg_stats는 PostgreSQL에서 제공하는 시스템 뷰로, 데이터베이스 내의 각 컬럼에 대한 통계 정보를 제공한다.
pg_stats
테이블에는NULL값의 비율
,칼럼의 평균 길이
,NDV
,컬럼 정렬 상태 등을
확인할 수 있다.
null_frac
NULL값의 비율
을 의미한다.모두 NULL 이면 1, 모두 NULL이 아니면 0
이다.- 만약 30%가 NULL이라면 0.3이다.
- 위 사진에서 보면 employee_id는 pk값이기 때문에 not null이다. 따라서 값이 0인 것을 확인할 수 있다.
avg_width
칼럼의 평균 길이
를 의미한다.- 이 값은 실행계획에서
width값을 계산
할 때 사용된다.
n_distinct
NDV (Number ofDistinct Value)를 의미
한다.- NDV값에 따라 양수 또는 음수로 표현된다.
- NDV가 테이블 건수 대비
10% 이내
이면-(NDV/레코드수)
- 따라서 pk와 같이 전부 유니크한 값이라면
-1
이다. employee_id값이 -1인 것을 확인할 수 있다. - 위 칼럼 중에서 beginDate가 900이라는 의미는 10000건 중에서 고유한 값이 900이라는 의미이다.
- 또한, end_date가 -0.2라는 건,
10000건 중에서 2000건이 고유한 값
이라는 의미다.10%가 넘었기 때문에 음수로 표현되며 소수점으로 표현
되는 것이다. - 고유의 값이 많을수록 인덱스를 효율적으로 사용할 수 있다. 중복값이 너무 많으면 인덱스가 만들어져 있다고 해도 옵티마이저가 인덱스를 사용하지 않을 수 있다.
- 즉,
-1에 가까운 값일수록 인덱스의 칼럼으로 선택하기 좋은 칼럼이다.
correlation
- 칼럼의
정렬상태
를 나타낸다. -1 ~ 1
사이의 값으로 표현된다.- 완벽히
정렬되어있다면
1
, 완벽히역순으로 정렬
되어 있다면-1
이다. - 따라서
정렬이 안되어있으면 0에 가까운 값이다.
(1, -1은 가장 정렬이 잘되어있는 값이다.) - 옵티마이저가
스캔방식을 선택할 때 정렬상태가 중요한 요소
로 사용된다.- 정렬상태가 불량하면 인덱스가 있다고 하더라도 옵티마이저는 Index Scan 대신
Bitmap Heap Scan을 선택할 확률이 크다
.
- 정렬상태가 불량하면 인덱스가 있다고 하더라도 옵티마이저는 Index Scan 대신
- 0에 가까울수록 정렬이 안되어 있기 때문에 beginDate가 가장 정렬이 안되어 있는 칼럼이다.
이제 통계정보를 바탕으로 COST를 계산하는 방법을 알아보자.
PostgreSQL의 옵티마이저가 COST를 계산하는 방법
- COST는 크게 IO비용과 CPU비용으로 구분된다.
- 비용을 계산하기 위해서는 계산에 사용되는 파라미터값을 알아야 한다.
- 각각의 파라미터값는 아래와 같다.
- 위와 같이 CPU 비용을 계산하는 파라미터와 I/O 비용을 계산하는 파라미터가 있다.
Seq San 비용 계산 방법
Start-Up Cost: 0 (비용 필요 없음)
Run Cost: CPU 연산비용 + 테이블 페이지 I/O 비용
- 위 수식은 아래와 같이 정리할 수 있다.
- 테이블 페이지I/O 비용 = repages * seq_page_cost
- CPU 연산 비용 = reltuples * cpu_tuple_cost + reltuples * cpu_operator_cost
- 위 파라미터 정보를 바탕으로 비용을 계산해 보자. 209.00은 Seq Scan으로 84개의 블록을 읽는 I/O 비용 + 10000건을 추출하는 CPU 비용을 뜻한다.
Index Scan 비용 계산 방법
Start-Up Cost: 인덱스 탐색 비용 + 인덱스 페이지 I/O 비용
Run Cost: CPU 연산 비용 + 테이블 페이지 I/O 비용
- Index Scan 방식은 블록 수와 튜플수 만으로는 계산할 수 없다.
- CPU 연산비용 아래와 같이 좀 더 디테일하게 정리할 수 있다.
- CPU 연산비용 = 인덱스 튜플 연산 비용 + 테이블 튜플 연산 비용
- 인덱스 튜플 연산 비용 = 선택도 * (cpu_index_tuple_cost + 조건문 계산 비용) * 인덱스 튜플 수
- 테이블 튜플 연산 비용 = 선택도 * (cpu_tuple_cost + 조건문 계산 비용) * 테이블 튜플 수
- 선택도는 쿼리 조건과 일치하는 튜플의 비율을 말한다.
- 조건문 계산 비용는 쿼리의 WHERE 절 등에 사용된 조건문을 평가하는 데 드는 CPU 비용을 말한다.
- 페이지 I/O 비용도 아래와 같다.
- 페이지 I/O 비용 = 인덱스 페이지 I/O 비용 + 테이블 페이지 I/O 비용이다.
- 인덱스 페이지 I/O 비용 = 선택도 * 인덱스 페이지 수 * random_page_cost
- 테이블 페이지 I/O 비용 = (상관 관계)^2 * (최저 읽기 비용 - 최대 읽기 비용)
- 최저 읽기 비용은 한 페이지만 랜덤 I/O로 읽고 나머지 페이지는 순차적 I/O로 읽은 비용이다.
- 최대 읽기 비용은 모든 페이지를 랜덤 I/O로 읽은 비용이다.
위 수식을 보다시피 Index Scan의 경우 비용을 계산하는 방법은 매우 복잡하다. 실제 계산하는 건 우선은 넘어가도록 하자...
References
'Database' 카테고리의 다른 글
PostgreSQL 실행계획 분석하기 2편 (Table Scan) (0) | 2023.11.18 |
---|---|
PostgreSQL 실행계획 분석하기 1편 (실행계획 읽는 방법) (0) | 2023.11.16 |
인덱스 (With PostgreSQL) (0) | 2023.11.04 |
서브쿼리 (With QueryDSL) (0) | 2023.10.22 |
쿼리연습 (겹치는 날짜 검사하기) (0) | 2023.04.07 |