개요
- 실무에서 종종 서브쿼리를 사용한다. 하지만 서브쿼리는 잘 알고 사용하지 않으면 비효율적으로 동작할 확률이 높다.
- 그래서 이번 글에서는 PostgreSQL 옵티마이저가 서브쿼리를 처리하는 방법에 대해서 알아보려고 한다.
- 옵티마이저는 크게 아래 3가지 방법으로 서브쿼리를 처리한다.
- 필터 방식 (Subplan)
- 세미 조인
- 서브쿼리 Collapse (서브쿼리 병합)
- 이제 본격적으로 위에서 언급한 3가지 처리방식을 알아보자.
필수 사전 지식
- PostgreSQL 옵티마이저
- PostgreSQL 실행계획 분석하기 1편 (실행계획 읽는 방법)
- PostgreSQL 실행계획 분석하기 2편 (Table Scan)
- PostgreSQL 실행계획 분석하기 3편 (Join Methods)
- PostgreSQL 실행계획 분석하기 4편 (Sort, Limit)
- PostgreSQL 실행계획 분석하기 5편 (Aggregates)
테이블 세팅
create table team
(
team_id bigint,
team_name varchar(100),
created_at timestamp with time zone
);
create table member
(
member_id bigint,
member_name varchar(100),
team_id integer,
join_date timestamp with time zone
);
필터 방식 (Subplan)
- Subplan은 서브쿼리를
별도의 쿼리 계획(Plan)으로 처리
하는 방식이다. - 필터 방식은 세미 조인이 개발되기 전에 사용된 방식이다.
서브쿼리가 메인 쿼리의 각 행에 대해 반복적으로 실행
되어야 할 때 주로 사용한다.- 그렇기 때문에, 필터 방식은 세미 조 인 중에서
성능적으로 NL 세미 조인과 비슷
하다. 따라서 옵티마이저는 보통의 경우 필터 방식보단세미 조인 방식을 더 선호
한다. - 일반적으로 서브쿼리가
상관 서브쿼리(correlated subquery)
일 때 이 방식이 사용된다. - 이제 실행계획에서 Subplan을 확인해 보자. 데이터는 team 테이블에 15건, member 테이블에 100건의 데이터를 넣었다.
INSERT INTO team (team_id, team_name, created_at)
SELECT i,
'Team ' || i,
CURRENT_TIMESTAMP - INTERVAL '1 day' * (i % 365)
FROM generate_series(1, 15) AS i;
INSERT INTO member (member_id, member_name, team_id, join_date)
SELECT i,
'Member ' || i,
(i % 15) + 1,
CURRENT_TIMESTAMP - INTERVAL '1 day' * (i % 365)
FROM generate_series(1, 100) AS i;
- 위 결과를 보면, loops가 15인 것을 확인할 수 있다. 이는
team 테이블의 건수
만큼 member 테이블을 순회하는 것이다. Rows Removed by Filter: 7는
15번의 반복에서 평균 7개씩의 레코드만큼을 필터링했다는 것이다.- 따라서, 대략 15 * 7 인 105개의 레코드가 제거됐다는 의미로 생각할 수 있다.
세미 조인
- 세미 조인은
서브쿼리의 결과가 메인 쿼리의 행 선택에만 영향을 미치고, 결과 집합에 직접 포함되지 않는 경우
사용된다. EXISTS 또는 IN 연산자
와 함께 사용되는 서브쿼리에 주로 사용된다.- 세미 조인은 서브쿼리의 결과가 필요한 메인 쿼리의 행을
효율적으로 필터링
하는 데 사용된다. - 이제 실행계획을 확인해 보자. 데이터는 team 테이블에 15건, member 테이블에 10000건의 데이터를 넣었다.
truncate member;
truncate team;
INSERT INTO team (team_id, team_name, created_at)
SELECT i,
'Team ' || i,
CURRENT_TIMESTAMP - INTERVAL '1 day' * (i % 365)
FROM generate_series(1, 15) AS i;
INSERT INTO member (member_id, member_name, team_id, join_date)
SELECT i,
'Member ' || i,
(i % 15) + 1,
CURRENT_TIMESTAMP - INTERVAL '1 day' * (i % 365)
FROM generate_series(1, 10000) AS i;
NL Semi Join
은 첫 번째 테이블의 행이 두 번째 테이블과 매칭되면 반복을 중단하는 방식으로 동작한다.(actual time=0.001..0.002 rows=8 loops=15)
이 부분을 해석해보면, 15번에 반복에서 평균 8개의 행을 검사했다는 의미이다.- member가 10000건인데 평균 8밖에 되지 않은 이유는 위에서 언급했듯이 NL Semi Join은 조건에 만족하는 순간 검사를 중단하기 때문이다.
- 이번에는 member를 기준 테이블로 조인을 걸어서 실행계획을 살펴보자.
Hash Semi Join
은 보통서브쿼리의 집합이 메인쿼리의 집합보다 작은 경우
에 동작한다.- Hash Join과의 차이점은 Hash Semi Join 경우에
Probe 테이블의 행이 Build 테이블의 해시 테이블과 매칭되면 반복을 중단
한다. - 정리하면,
NL Semi Join
과Hash Semi Join
모두 테이블의 행을 효율적으로필터링
하는데 유용한 조인방식이다. 그렇기 때문에EXISTS, IN에서
사용되는 서브쿼리를 처리하는데 주로 사용된다,
서브쿼리 Collapse (서브쿼리 병합)
- Subquery Collapse는 서브쿼리를 메인 쿼리와 병합하여 하나의 쿼리로 처리하는 최적화 기법이다.
- 병합으로 인해 성능상의 이점이 있을 때 사용된다.
- 이번에는 데이터를 team 테이블에 1000건, member 테이블에 100000건의 데이터를 넣었다.
- 이제 실행계획을 살펴보자.
- 서브쿼리를 사용했음에도 불구하고, 옵티마이저가 판단하기에 Hash Join으로 처리하는 것이 더 효율적이라고 판단한 경우에는,
서브쿼리 대신 Hash Join을 사용하여 쿼리를 최적화
한 것을 확인할 수 있다. - 서브쿼리를 메인 쿼리와 병합함으로써, 옵티마이저는 Hash Join과 같은 더
유용한 조인 방식
을 사용할 수 있기 때문에 옵티마이져는 Collapse를 선택한 것 같다.
References
'Database' 카테고리의 다른 글
서브쿼리 변환하기 2편 (With Java, QueryDSL) (0) | 2023.11.26 |
---|---|
서브쿼리 변환하기 1편 (이론) (0) | 2023.11.26 |
PostgreSQL 실행계획 분석하기 5편 (Aggregates) (0) | 2023.11.25 |
PostgreSQL 실행계획 분석하기 4편 (Sort, Limit) (0) | 2023.11.22 |
PostgreSQL 실행계획 분석하기 3편 (Join Methods) (0) | 2023.11.19 |