PostgreSQL 9.6에서 병렬 쿼리 기능의 도입은 배경 작업자로부터 결과를 리더 프로세스로 결합하기 위해 Gather 노드를 가져왔습니다. 그러나 표준 Gather 노드는 병렬 작업자가 생성한 튜플 순서를 파괴하므로, 순서를 재설정하기 위해 리더에서 비싼 최종 Sort 단계를 필요로 합니다. 본질적으로 정렬된 데이터 스트림을 처리할 때 이 중복을 제거하기 위해 10버전에서 Gather Merge 노드가 도입되어, 작업자들로부터 정렬된 입력을 k-방식으로 병합하여 리더 측의 자료화 및 정렬 필요성을 피합니다.
플래너는 병렬 서브플랜이 일반적으로 Index Scans 또는 튜플 순서를 보존하는 Merge Joins에 의해 생성된 필수 속성에 따라 정렬된 출력을 보장할 때만 Gather Merge를 주입하도록 선택합니다. 서브플랜이 Hash Joins 또는 순서가 없는 집계와 같은 작업을 통해 정렬을 잃으면 Gather Merge는 자격을 잃게 되어, 최적화기는 튜플 순서를 유지하기 위해 비용이 많이 드는 Sort에 이어 Gather 또는 전혀 병렬성을 포기하는 선택을 해야 합니다.
서브플랜이 정렬된 출력을 보장할 때, Gather Merge는 리더가 모든 튜플을 자료화하고 정렬하는 대신 최소한의 메모리 버퍼를 사용하여 스트리밍 병합을 수행할 수 있게 합니다. 메모리 전략은 리더에서 정렬을 위한 단일 대규모 할당에서, 정렬 실행을 위한 작은 개별 작업자 유지보수로 전환되며, 대규모 정렬된 검색 중 work_mem 소진 및 디스크 흘림의 위험을 크게 줄입니다.
우리 팀은 2억 개 이상의 행이 포함된 PostgreSQL 테이블에 센서 판독값을 저장하는 시계열 분석 플랫폼을 관리하였습니다. 중요한 대시보드는 timestamp 내림차순으로 정렬된 가장 최근의 1000개의 판독값을 모든 파티션에서 표시해야 했으며, 지연 예산은 500 밀리초 이하였습니다. 초기 단일 스레드 쿼리 계획은 이러한 요구사항을 충족하지 못해 peak 분석 부하 동안 사용자 경험의 병목현상을 초래했습니다.
단일 프로세스 인덱스 스캔: 우리는 처음에 각 파티션에서 역방향 Index Scan을 사용한 후 Limit 노드를 순차적으로 실행하는 방안을 고려했습니다. 이 접근은 복잡한 병렬 조정 없이 구현의 단순성과 결정론적 순서를 제공했습니다. 그러나 이는 NVMe 스토리지 배열의 I/O 대역폭을 최대한 활용하지 못하고 peak 부하 중에는 항상 2초를 초과하여, 실시간 대시보드 업데이트에는 수용할 수 없었습니다.
Gather 및 Sort가 포함된 병렬 Seq Scan: 두 번째 접근은 max_parallel_workers_per_gather를 활성화하고 표준 Gather 노드와 함께 Parallel Seq Scan을 사용하여 모든 행을 리더로 수집한 다음 최종 Sort 및 Limit을 수행하는 것이었습니다. 이는 CPU 병렬성을 활용하고 스캔 처리량을 크게 향상시켰습니다. 그럼에도 불구하고 리더 프로세스는 수백만 개의 행을 정렬하기 위해 4GB 이상의 work_mem을 할당해야 했고, 종종 디스크 스필 및 OutOfMemory 오류를 발생시켜 시스템 안정성을 저하했습니다.
Gather Merge가 포함된 병렬 인덱스 스캔: 우리는 궁극적으로 작업자들이 내림차순 타임스탬프 순서로 Parallel Index Scans를 수행하고 그것을 Gather Merge 노드로 공급하는 계획을 선택했습니다. 작업자들은 필요한 순서로 인덱스 리프 페이지를 스캔하고 정렬된 튜플을 리더로 스트리밍하여, 리더가 상위 1000개의 행을 추출하기 위한 경량 k-방식 병합을 수행했습니다. 이 아키텍처는 리더에서 최종 정렬의 필요성을 제거하여 메모리 압력을 크게 줄이면서 스트리밍 효율성을 유지했습니다.
우리는 Gather Merge 접근 방식을 선택한 이유는 해시 기반 작업으로 대항하는 대신 기존 인덱스 구조를 활용하여 지연 및 메모리 제약을 모두 충족했기 때문입니다. 이 솔루션은 리더의 메모리 사용량을 병합 버퍼에 대해 64MB 이하로 감소시켰으며 일관된 300ms 이하의 응답 시간을 달성했습니다. 시스템은 이제 메모리 소진 없이 peak 부하를 처리하며, 병렬 실행을 통해 순서를 보존하려는 아키텍처 선택의 유효성을 검증했습니다.
왜 Hash Aggregate를 Gather Merge 노드 아래에 배치하면 PostgreSQL 플래너가 계획을 거부하거나 명시적 Sort 단계를 삽입하게 되는지, 그리고 이는 GroupAggregate의 동작과 어떻게 다른가요?
Hash Aggregate는 튜플을 그룹화하기 위해 순서가 없는 해시 테이블을 빌드하는데, 이는 본질적으로 기초 스캔에 의해 생성된 입력 순서를 파괴합니다. Gather Merge는 병렬 작업자 모두로부터 엄격하게 정렬된 입력 스트림이 필요하기 때문에, 집계에서 나온 무질서한 출력은 직접 사용을 차단합니다. 반대로 GroupAggregate는 정렬된 입력에서 작동할 수 있으며, GROUP BY 키가 정렬 순서와 일치할 때 튜플의 순서를 유지할 수 있어 중간 정렬 단계 없이 Gather Merge와 호환됩니다.
병렬 worker들로부터 정렬된 스트림을 병합하는 비용을 추정할 때, parallel_tuple_cost GUC가 플래너가 Gather 계획에서 Gather Merge 계획으로 전환하는 임계점에 어떤 영향을 미칩니까?
parallel_tuple_cost는 병렬 작업자와 리더 프로세스 간에 행을 전송할 때의 튜플당 CPU 오버헤드를 추가합니다. Gather Merge의 경우, 이것은 추가적인 비교 로직이 필요하기 때문에 표준 Gather 노드보다 약간 높은 비용이 발생합니다. 예상 결과 세트가 작은 경우, 플래너는 작은 튜플 배치를 중앙에서 정렬하는 비용보다 여덟 개의 병합 스트림의 누적 오버헤드가 초과할 수 있기 때문에 Gather 노드를 선호할 수 있습니다.
Gather Merge 노드가 포함된 쿼리 계획에서 SCROLL 옵션을 사용하여 DECLARE CURSOR를 사용할 때 어떤 특정 제한이 발생하며, 병렬 중복성과 전반적인 결과 집합을 조용히 물리적으로 만들어내는 이유는 무엇입니까?**
SCROLL 커서는 결과 집합을 거슬러 이동할 수 있는 기능을 요구하며, 이는 역방향 페칭을 지원하기 위해 work_mem에서 행을 물리적으로 만들어내거나 디스크로 흘려보내야 합니다. 비록 Gather Merge가 효율적으로 스트리밍된 정렬 출력을 생성하지만, SCROLL 옵션은 잠재적 역방향 탐색을 위해 Gather Merge 위에 Materialize 노드를 삽입하도록 실행기를 강제합니다. 이 자료화는 결과 집합의 크기에 비례하여 메모리를 소비하게 되어, 스트리밍 병합 전략의 메모리 효율성 이점을 사실상 무효화하고, 처음에 Gather Merge를 선택하여 피했던 디스크 스필을 초래할 수 있습니다.