0. 들어가기 전
저는 이전까지 DB 벤더 중에서 학습용으로 MySQL만 사용해왔었습니다.
그러나 이번 실무에서 PostgreSQL을 사용하게 되면서 PostgreSQL에 대해서 알아보고자 글을 작성하게 되었습니다.
PostgreSQL의 모든 내용을 다루기에는 너무나 방대한 양이기에 제가 궁금한 부분, 중요한 부분들만 다뤄보도록 하겠습니다.
하나의 주제를 잡아서 주제별로 PostgreSQL의 공식문서를 보고 글을 작성해보겠습니다.
처음은 PostgreSQL에서 커넥션을 맺고 쿼리를 어떻게 처리하는지 PostgreSQL의 내부 구조를 간략하게 추상적으로 살펴보도록 하겠습니다.
https://www.postgresql.org/docs/17/overview.html
1. How Connections Are Established : 커넥션 맺는 방법
PostgreSQL은 ‘process per user’ client/server model을 사용합니다. (Client당 1개의 Server 프로세스 할당)
이때, Server의 프로세스는 다음과 같은 2개로 구분할 수 있습니다.
- Supervisor Process (postmaster)
- Client와 직접적으로 커넥션을 맺고 Backend Process를 생성하는 역할
- Backend Process
- Client와 연결되어 Client에서 요청한 쿼리를 처리하여 반환하는 역할
Client의 쿼리 요청이 들어왔을 때의 과정을 자세히 살펴보면 다음과 같습니다.
- Supervisor Process가 커넥션을 맺는 특정 TCP/IP Port를 listen
- 커넥션 요청이 감지될 때마다 새로운 Backend Process 생성
- Client는 생성된 Backend Process에게 쿼리 요청
- Backend Process는 쿼리를 처리하여 결과를 Client에게 반환
이를 간단하게 도식화하면 다음과 같은 구조로 커넥션을 맺고 요청을 주고 받습니다.
2. The Parser Stage : 전달받은 Query 파싱 & 변환
1번 단계에서 Client는 커넥션을 맺고 요청할 Query를 전달합니다.
간단하게 이해하기 위해서 간단한 Query를 예시로 이후 과정을 설명해보도록 하겠습니다.
SELECT id, name FROM member WHERE age > 30
위의 쿼리를 Client가 요청했을 때 이후 과정을 살펴봅시다.
먼저, 해당 쿼리는 PostgreSQL Server에서 이해할 수 있는 형태로 구조화된 것이 아니라 단순 Plain Text 형태로 전달됩니다.
일반 Application으로 비유를 들면 다음과 같은 단순 String 변수로 전달되는 것입니다. (Java 예시)
String query = "SELECT id, name FROM member WHERE age > 30";
따라서 PostgreSQL에서는 해당 Plain Text를 여러 Tree를 생성하며 구조화하는 여정을 거치게 됩니다.
먼저 이 단계에서는 전달받은 Query Text를 파싱하고 변환하는 과정을 거칩니다.
- Parser : 파싱 단계
- Transformation Process : 변환 단계
2-1 . Parser : 파싱 단계
파싱 단계에서는 간단하게 전달받은 Query의 문법에 오류가 없는지 체크하고, 없다면 Parse Tree를 생성하여 반환합니다.
ChatGPT를 통해 간단히 Parse Tree를 다음과 같이 표현해봤습니다.
SELECT
/ | \
target_list FROM WHERE
/ | | |
id name member age > 30
이처럼 단순한 Plain Text를 파싱하여 다음과 같은 구조를 형성하여 다음 단계에 전달합니다.
2-2. Transformation Process : 변환 단계
변환 단계에서는 파싱 단계에서 전달받은 Parse Tree를 기반으로 여러 조건을 확인하는 단계입니다.
위의 Parse Tree를 전달받았다고 하면, 다음과 같은 검증을 거칩니다.
- Table & Column 확인
- member 테이블 존재 확인
- id, name, nickname, age 컬럼이 member 테이블에 존재하는지 확인
- 연산자 및 데이터 타입 확인
- 조건인 age 컬럼이 숫자형 데이터 타입인지 확인하고, 적용 가능한 연산인지 확인
이 과정에서 중요한 점은, 실제 DB 객체를 확인하여 검증하는 것이기 때문에 객체 정보가 있는 system catalog를 조회한다는 점입니다.
이렇게 검증이 끝나고 나면, 해당 해석을 담는 Query Tree를 생성하여 전달합니다.
ChatGPT를 통해 간단히 Query Tree를 다음과 같이 표현해봤습니다.
SELECT
/ | \
target_list FROM WHERE
/ | | |
id name member age > 30
| | |
column column table
※ Parser Stage가 2단계(파싱 - 변환)로 나뉘는 이유
결론적으로는 파싱 단계에서는 단순 파싱으로 트랜잭션이 필요가 없고, 변환 단계에서는 트랜잭션이 필요하기 때문입니다.
이전에 변환 단계에서 실제 DB 객체와 비교하여 검증을 수행하기 때문에 system catalog를 조회한다고 언급했었습니다.
이러한 작업은 트랜잭션 위에서 수행되어야 하기 때문에 변환 단계에서는 트랜잭션이 필요합니다.
이때 Parser Stage가 2단계가 아니라 파싱과 변환이 함께 묶였다면,
트랜잭션이 필요하지 않은 파싱 단계에서도 트랜잭션을 가지기 때문에 트랜잭션이 길어지게 됩니다.
따라서, Parser Stage를 2단계로 나누어 변환 단계에서만 트랜잭션 위에서 작업이 수행되도록 한 것입니다.
3. The PostgreSQL Rule System : The Rewrite System
다시 돌아와서, 변환 단계를 거쳐 Query Tree를 전달받은 상태입니다.
이 단계에서는 쿼리를 '재작성'하는 Rewrite System이 동작하여 적용된 Rule이 있는지 확인하고 Query Tree를 재작성하게 됩니다.
보통 PostgreSQL의 Rule은 CUD 쿼리에 적용하는 Rule을 생성할 수 있고 View를 생성하게 되면 자동으로 Rule이 적용됩니다.
PostgreSQL의 Rule System 관련해서는 내용이 깊기 때문에 추가적인 내용은 아래 공식문서에서 확인해보시면 좋을 것 같습니다.
https://www.postgresql.org/docs/17/rules.html
현재 예제에는 따로 적용된 Rule이 없기 때문에 그대로 Query Tree가 재작성 되지 않고 전달되게 됩니다.
간단하게 알아보기 위해, 아래의 View를 조회한다고 가정해봅시다.
* View
CREATE VIEW active_users AS
SELECT * FROM users WHERE is_active = true;
---
SELECT * FROM active_users;
이때, Parser Stage를 거쳐 Query Tree가 전달되었을 때 `active_users`는 테이블이 아니라 View이므로
실제 데이터에서 조회할 수가 없게 됩니다.
따라서, Rewrite System에서 해당 쿼리를 Rule을 통해 실제 데이터를 조회하는 쿼리로 분석하여 Query Tree를 재작성합니다.
(View를 생성하면 자동으로 관련 Rule이 생성됩니다.)
그래서 특정 Rule이 존재한다면, Rewrite System에서 해당 Rule을 적용한 Query Tree로 재작성 후 전달합니다.
4. Planner / Optimizer : 최적의 Execution Plan 생성
해당 단계에서는 전달받은 Query Tree를 기반으로 실제 데이터를 어떻게 읽고 처리할지에 관한 Plan Tree를 생성하여 전달합니다.
전달받은 Query Tree로 Plan을 생성했을 때, 여러가지 방법들이 존재할 수 있습니다.
만약, 조회하려는 컬럼에 인덱스가 존재한다면 다음과 같은 방법이 있을 수 있습니다.
- Seq Scan : 순차적으로 모두 스캔
- Index Scan : 인덱스 스캔
이렇게 다양한 Plan을 모두 생성하는 역할을 Planner가 수행하고,
해당 Plan을 받아서 Optimizer가 해당 Plan들을 모두 검증하고 가장 빠르게 실행될 것으로 예측되는 Plan을 선정합니다.
그 후에 해당 Plan을 기반으로 Plan Tree를 생성하여 전달합니다.
ChatGPT를 통해 간단히 Plan Tree를 다음과 같이 표현해봤습니다.
Seq Scan
/ \
Output Filter
/ | \ |
id name age age > 30
|
member (table)
4. Executor : Plan 실행
Executor는 마지막 단계로, 간단합니다.
전달받은 Plan Tree의 노드들을 재귀적으로 하위노드 -> 상위노드로 처리하면서 실제 DB에서 필요한 row들을 추출하는 역할을 합니다.
이렇게 최적의 Plan을 실행하고 필요한 row를 추출해서 Client에게 전달하게 됩니다.
5. The Path Of a Query : 쿼리 처리 과정 요약
지금까지 알아본 PostgreSQL에서 쿼리가 어떻게 처리되는지에 관한 과정들을 요약해보겠습니다.
- Client(Application)에서 PostgreSQL 서버와 커넥션을 맺고, 쿼리 명령을 전달하고 결과를 기다린다.
- Parser Stage 에서 Client가 전달한 쿼리의 문법 오류가 있는지 체크하고 query tree 를 생성
- rewrite system에서 query tree에 적용될 rules를 찾고 query tree에 적용한 후, 재작성
- planner/optimizer 는 query tree 를 가져와서 executor에 입력될 plan tree를 생성한다.
- executor 는 plan tree 를 재귀적으로 살펴보고 plan 에 따라 행들을 탐색한다.
글로는 이렇게 요약이 되고, 조금 딱딱한 것 같아서 Flow를 알아보기 쉽게 그림으로 다음과 같이 표현해봤습니다.
'DB' 카테고리의 다른 글
[PostgreSQL] 2. PostgreSQL Index 알아보기 (Index Type, Index Scan) (1) | 2024.11.16 |
---|---|
[DB] DB PK 생성 전략 알아보기 (feat. Auto Increment, UUID, ULID, Snowflake ID, TSID) (2) | 2024.09.22 |
[DB] AWS DynamoDB 알아보기 (0) | 2024.09.13 |
[DB] MySQL 및 InnoDB의 DB Lock 알아보기 (1) | 2023.11.19 |
[DB] MySQL InnoDB의 인덱스(feat. 클러스터링 인덱스, 세컨더리 인덱스, 인덱스 스캔 종류, 다중 컬럼 인덱스) (1) | 2023.11.13 |