개요
- 실무에서 자주 등장하는 Aggregate 오퍼레이션의 종류는 크게 세가지 이다.
- Aggregate
- HashAggregate
- GroupAggregate
- Aggregate는 SUM(), AVG(), COUNT(), MAX(), MIN()와 같은 집계 함수를 처리할 때 주로 등장한다.
- HashAggregate와 GroupAggregate는 Group By를 할 때에 주로 등장한다.
- 이번 글에서는 위 세가지 Aggregate 오퍼레이션에 대해서 정리해 보려고 한다.
필수 사전 지식
테이블 생성
- 먼저, 테스트를 위해 아래와 같이 테이블을 만든다.
CREATE TABLE aggregates_test_table (
id SERIAL PRIMARY KEY,
category INT,
value INT
);
Aggregate
- Aggregate는 PostgreSQL에서 SUM(), AVG(), COUNT(), MAX(), MIN()과 같은 집계함수를 실행할 때 주로 사용된다.
데이터 세팅
INSERT INTO aggregates_test_table (category, value)
SELECT (random() * 100000)::int AS category, (random() * 100000)::int AS value
FROM generate_series(1, 100000) s;
- 이제 실행계획을 살펴보자.
- 위와 같이 Aggregate 오퍼레이션을 통해서 aggregate_test_table을 Seq Scan으로 Full Scan해서 max값을 찾고 있다.
- 만약, 인덱스가 있다면 max(), min() 같은 집계함수는 성능 개선이 가능하다.
- 집계함수 중에서 AVG()와 SUM()같이 데이터를 다 읽어야 하는 경우에는 인덱스를 활용할 수 없지만, MAX(), MIN()과 같은 특정 데이터만 찾는 경우에는 인덱스를 활용할 수 있다.
- 성능 또한 25ms에서 0.081ms로 빨라진 것을 확인할 수 있다.
HashAggregate
- HashAggregate는 PostgreSQL에서 group by와 같은
그룹화 연산
을 수행할 때 사용되는 방법 중 하나이다. - HashAggregate는
Group By에 사용되는 칼럼들로
메모리상에해시테이블
을 만들고, 해당 해시테이블을 바탕으로 SUM, COUNT, AVG와 같은 집계 함수를 처리하는 방식이다. - 해시 테이블의 크기는 GROUP BY 절에서 사용되는
칼럼의 카디널리티(고윳값의 수)와 데이터의 양
에 의해 결정된다. - 카디널리티가 낮아야
중복된 값이 많다는 것
이기 때문에 HashAggregate에 효율적이다. - 메모리 사용량이 설정된 work_mem
한계를 초과
하면, 성능 저하가 발생할 수 있다. - HashAggregate는
미리 정렬되어있을 필요가 없고
, 너무 크지 않다면대용량 처리
에 유리하다. (너무 크다면 메모리 자원이 한정적이기 때문에 분리하다.) - 정렬이 필요없기 때문에 정렬에 소요되는 시간이 절약될 수 있지만, 대용량의 데이터가 카디널리티까지 높아서 해시테이블이 커진다면, HashAggregate는 비효율적일 수 있다.
- 또한, 정렬된 데이터가 필요 없기 때문에
인덱스가 없는 경우
주로 선택된다. - 이제 데이터를 넣고 실행계획을 살펴보자
데이터 세팅
INSERT INTO aggregates_test_table (category, value)
SELECT (random() * 100)::int AS category, (random() * 1000)::int AS value
FROM generate_series(1, 100000) s;
- 10만건의 데이터를 생성했다. 여기서 category는 100개의 랜덤 한 값으로 생성했다.
- 따라서, 데이터는 많고 category 칼럼에 카디널리티는 낮은 데이터셋으로 생성했다.
- 또한, 인덱스도 따로 설정하지 않았다. 인덱스를 설정했다고 해도 카디널리티가 낮기 때문에 인덱스를 타지 않을 것이다.
- 위 실행계획을 보면 HashAggregate가 선택되었고, category 칼럼을 Group Key로 사용한 것을 확인할 수 있다.
GroupAggregate
- GroupAggregate는 HashAggregate과 마찬가지로
그룹화 연산
을 수행하는 또다른 방법이다. - GroupAggregate는 GROUP BY에 사용되는 칼럼들로 이미
정렬된 데이터를 기반으로 하여 그룹화를 수행
하고, 해당 그룹화된 데이터를 바탕으로 SUM, COUNT, AVG와 같은 집계 함수를 처리하는 방식이다 - 그렇기 때문에 GroupAggregate는 이미
정렬된 데이터셋이나 적절한 인덱스
가 있는 경우에 선택될 때 가장 효과적으로 동작한다. - GroupAggregate는
대량의 데이터를 버퍼링하지 않기 때문에
, 메모리에 올리기 너무 큰 데이터의 경우에 HashAggregate 대신 GroupAggregate가 선택되기도 한다. - 다시 말해서, GroupAggregate는
현재 처리 중인 그룹에 대한 데이터만 메모리에 유지
하기 때문에, 전체적인 메모리 사용량이 HashAggregate에 비해 적을 수 있다.
데이터 세팅
truncate aggregates_test_table;
INSERT INTO aggregates_test_table (category, value)
SELECT (random() * 10000000)::int AS category, (random() * 1000)::int AS value
FROM generate_series(1, 1000000) s;
create index idx_aggregates_test_table_category
on aggregates_test_table(category);
- 위와 같이, 100만 건의 데이터에 대해서 category 칼럼에 대한 카디널리티가 높게 데이터셋으로 생성했다. (카디널리티 값은 -1에 가까울수록 높디.)
카디널리티를 높게 설정한 이유는 인덱스를 태우기 위함
이다.- 또한, category에 인덱스를 걸어주었다.
- Group이라고 표시된 것이 GroupAggregate를 말한다.
- 인덱스가 있고 데이터의 크기가 100만 건으로 상당히 크기 때문에 옵티마이저는 HashAggregate 대신 GroupAggregate를 선택했다.
- 인덱스가 있다고 하더라고 데이터의 크기가 크지 않다면 HashAggregate가 선택
데이터 크기에 따른 옵티마이저의 Aggregate 선택
truncate aggregates_test_table;
INSERT INTO aggregates_test_table (category, value)
SELECT (random() * 1000000000)::int AS category, (random() * 1000)::int AS value
FROM generate_series(1, 138990) s;
analyze aggregates_test_table;
explain (analyze , buffers )
select category
from aggregates_test_table
group by category;
- Memory Usage: 10289kB ->
최대 10MB 정도의 메모리를 사용
했다는 것을 의미한다. - Disk Usage: 1560kB -> 1560kB의 디스크 사용량은 연산 중 일부 데이터가
메모리 용량을 초과하여 디스크에 임시 저장
되었음을 나타낸다. - Buffers: shared hit=752, temp read=108 written=272
- shared hit=752 -> 공유 버퍼 캐시에서 752번 데이터를 읽었음 의미한다.
- temp read=108 written=272 -> 임시 디스크 공간에서 데이터를
108번 읽고 272번 썼다는 것
을 나타낸다.
메모리 용량을 초과하여 디스크에 임시 저장
되었다는 것은 옵티마이저가 HashAggregate 대신 GroupAggregate을 사용하려고 할 수 있다.- 데이터 수를 조금만 올려보자.
truncate aggregates_test_table;
INSERT INTO aggregates_test_table (category, value)
SELECT (random() * 1000000000)::int AS category, (random() * 1000)::int AS value
FROM generate_series(1, 139999) s;
analyze aggregates_test_table;
explain (analyze , buffers )
select category
from aggregates_test_table
group by categ
- Heap Fetches: 139999 -> 실제 테이블(Heap)에서 139999번의 행을 가져왔다는 것을 의미한다.
- shared hit=140358 -> 공유 버퍼 캐시에서 140358번 데이터를 읽어왔음을 의미한다.
- 위 테스트로 보아, 옵티마이저가 테이터 크기가 어느 임계치를 넘어가면 GroupAggregate를 선택하는 것을 확인할 수 있다.
- 내가 테스트해보면서 느낀 건, PostgreSQL의 옵티마이저는 메모리에 큰 부하가 없는 경우, HashAggregate를 자주 선택하는 것 같다. (내 예측이다.)
- 또한, 인덱스가 없는 경우, 메모리에 부하가 커도 GroupAggregate를 선택하는 경우는 거의 보지 못했다.
References
'Database' 카테고리의 다른 글
서브쿼리 변환하기 1편 (이론) (0) | 2023.11.26 |
---|---|
PostgreSQL 실행계획 분석하기 6편 (서브쿼리) (0) | 2023.11.25 |
PostgreSQL 실행계획 분석하기 4편 (Sort, Limit) (0) | 2023.11.22 |
PostgreSQL 실행계획 분석하기 3편 (Join Methods) (0) | 2023.11.19 |
PostgreSQL 실행계획 분석하기 2편 (Table Scan) (0) | 2023.11.18 |