콘텐츠로 이동

(KO) PostgreSQL 통계 — 수집기 프로세스에서 확장 통계와 공유 메모리 누적 통계까지

목차:

이 서브시스템이 진화해야 했던 이유 (초기 설계의 한계)

섹션 제목: “이 서브시스템이 진화해야 했던 이유 (초기 설계의 한계)”

PostgreSQL에서 “통계”는 하나의 서브시스템이 아니라 둘이다. 이 구분이 전체 타임라인을 읽는 핵심이다. 두 시스템은 다루는 데이터, 소비하는 주체, 갱신 주기, 장애 방식이 모두 다르고, 각자 독립적으로 발전했다. 두 개를 혼동하는 것이 대표적인 실수인데, 모듈 문서(postgres-cumulative-stats.mdpostgres-extended-statistics.md) 모두 첫머리에서 이 점을 경고한다.

**플래너 통계(planner statistics)**는 데이터 형태를 표본 추출로 추정한 값이다. ANALYZE는 테이블 행에서 고정 크기의 저수지(reservoir) 표본을 추출하고, 각 열을 pg_statistic 행 하나로 압축한다. 행에는 null 비율, 평균 폭, 고유값 개수(stadistinct), 최빈값(MCV) 목록, 나머지 값의 등깊이 히스토그램이 들어간다. 플래너는 이 정보를 읽어 WHERE 절의 선택도(selectivity)를 계산하고, 선택도를 행 수 추정치로 변환한다. 이 행 수 추정치가 쿼리 계획 전체에서 가장 결정적인 숫자다. 통계는 간헐적으로 갱신되고, 근사치라는 점은 설계 의도다.

초기 설계의 구조적 한계는 열 단위 통계가 열 독립성을 가정한다는 점이다. 두 상관 열에 동시에 필터를 걸면 — 교과서적 예시는 WHERE city = 'New York' AND state = 'NY'인데, 도시가 주(州)를 이미 결정한다 — 플래너는 두 열의 선택도를 독립 사건처럼 곱한다. 그 결과는 실제보다 훨씬 작아지고, 행 수 추정치는 1에 수렴하며, 플래너는 중첩 루프 계획을 선택한다. 실제 기수성이 수백만 행이라면 재앙적인 결과다. 열별 표본을 아무리 늘려도 이 문제는 해결되지 않는다. 두 열이 상관돼 있다는 정보 자체가 단일 열 요약 집합에는 애초에 없기 때문이다. PG10부터 PG14까지 이어진 확장 통계 작업이 메운 공백이 바로 이것이다.

**누적 활동 통계(cumulative activity statistics)**는 지금까지 무슨 일이 일어났는지를 정확히 세는 이벤트 카운터다. 순차 스캔 횟수, 삽입·갱신 튜플 수, 마지막 vacuum 이후 쌓인 죽은 튜플 수, 작성된 WAL 바이트, 디스크에서 읽은 블록 대 버퍼 캐시 적중 블록 수가 여기에 해당한다. 이 값은 표본이 아니라 관련 연산마다 증가하며, pg_stat_* 뷰 패밀리 전체를 구동하고, 오토베큠이 어느 테이블을 다음에 청소할지 결정하는 기준이 된다.

초기 설계의 한계는 운영 측면이었다. 초기 버전부터 PG14까지 이 카운터는 단일 전용 통계 수집기(stats collector) 프로세스에 집중돼 있었다. 모든 백엔드는 카운터 델타를 UDP 데이터그램으로 직렬화해 루프백 소켓으로 수집기에 보냈다. 수집기는 모든 카운터의 유일한 권위 있는 인메모리 사본을 소유했고, 주기적으로 파일에 전체 내용을 기록했으며, pg_stat 쿼리에 응답할 때는 요청한 백엔드가 그 파일을 읽어 역직렬화하는 방식을 썼다. 이 설계에는 규모가 커질수록 심해지는 구조적 비용이 세 가지 있었다.

  1. 손실 발생. UDP 데이터그램은 부하 상황에서 조용히 버려질 수 있었다. 카운터는 재전송도, 오류 표시도 없이 그냥 낮게 집계됐다.
  2. 처리량 상한선. 클러스터 전체의 통계 트래픽을 받아 병합하는 프로세스가 하나뿐이었다. 코어 수가 많은 바쁜 머신에서 이 단일 소비자가 병목이 됐다.
  3. 읽기 비용. pg_stat 쿼리마다 테이블 하나의 카운터 하나를 읽으려 해도 직렬화된 통계 파일 전체를 파싱해야 했다.

두 한계 모두 버그가 아니었다. 각 시대에 합리적인 설계였다. 그러나 PostgreSQL이 더 큰 스키마(열 상관이 예외가 아닌 일반인 환경)와 더 크고 바쁜 클러스터(수집기의 단일 프로세스 모델이 따라가지 못하는 환경)로 나아가면서, 두 서브시스템 모두 재설계해야 했다. 두 재설계는 병렬 경로로 진행됐고, 아래 타임라인이 그 요약이다.

timeline
    title PostgreSQL 통계 진화
    section 플래너 통계 (데이터 형태)
        pre-10 : 열 단위 pg_statistic만 존재 : nullfrac, stadistinct, MCV, 히스토그램 : 열 독립성 가정
        PG10 2017 : CREATE STATISTICS : 함수적 종속성 : 다변량 n-distinct
        PG12 2019 : 다변량 MCV 목록 : 상관 다중 절 선택도
        PG14 2021 : 표현식 통계 : 상속 인식 확장 통계 (stxdinherit)
    section 누적 통계 (활동 카운터)
        pre-15 : 단일 UDP 통계 수집기 프로세스 : 손실, 단일 병목, 파일 전체 읽기
        PG15 2022 : 수집기 제거 : DSA + dshash 공유 메모리 : 백엔드별 펜딩 버퍼
        PG16 2023 : pg_stat_io : I/O를 고정 stat kind로
        PG18 2025 : 백엔드별 I/O 통계 : pg_stat_io에 WAL + 바이트 단위 열

Era 0 — 열 단위 pg_statistic + UDP 통계 수집기 프로세스

섹션 제목: “Era 0 — 열 단위 pg_statistic + UDP 통계 수집기 프로세스”

두 경로가 모두 출발하는 기준점이다. 이후 릴리스 각각이 이 기준선 대비 변경분이므로, 초기 설계를 정확히 파악하는 일이 중요하다.

플래너 측 — pg_statistic, 열 하나당 행 하나. analyze.cANALYZE300 * statistics_target 행 크기의 표본을 2단계 블록 표본 추출과 Vitter 저수지 알고리즘으로 추출한 뒤, 열마다 타입별 compute_stats 루틴을 실행했다. 결과는 stanullfrac, stawidth, stadistinct와 최대 몇 개의 슬롯을 담은 단일 pg_statistic 행이었다. 각 슬롯은 STATISTIC_KIND_* 코드로 태그된다. MCV 목록(STATISTIC_KIND_MCV), 비MCV 잔여 값의 등깊이 히스토그램(STATISTIC_KIND_HISTOGRAM), 물리적·논리적 상관(STATISTIC_KIND_CORRELATION)이 그것이다. 슬롯 설계는 의도적으로 개방형이었다. 타입의 typanalyze가 임의의 통계 형태를 채울 수 있었고, 덕분에 나중에 확장 통계를 추가할 때 열 단위 경로의 카탈로그를 뜯어고치지 않아도 됐다.

플래너는 selfuncs.c에서 이 정보를 소비했다. var_eq_const는 MCV 목록에서 정확한 빈도를 찾거나 (1 - sumcommon - nullfrac) / otherdistinct로 대체했다. scalarineqsel은 히스토그램을 이진 탐색해 경계 빈 내부를 보간했다. 모두 열 단위 계산이었다. 다중 절 선택도는 각 절 선택도의 곱이었고, 독립성 가정은 카탈로그 구조 자체에 새겨져 있었다. 메커니즘 상세는 postgres-extended-statistics.md에 있다.

누적 측 — 통계 수집기. 포스트마스터가 포크하는 전용 보조 프로세스였다. 백엔드는 공유 카운터에 직접 쓰지 않고, 델타를 로컬에 누적한 뒤 루프백 데이터그램 소켓으로 주기적으로 UDP 메시지를 직렬화해 수집기에 보냈다. 수집기는 모든 메시지를 자신의 전용 해시테이블 — 유일한 인메모리 사본 — 에 병합하고, 타이머에 따라 pg_stat_tmp/ 아래 통계 파일로 기록했다. SELECT * FROM pg_stat_user_tables를 실행하는 백엔드는 새 파일 작성을 요청하고, 그 파일을 읽어 역직렬화하는 방식으로 결과를 얻었다.

flowchart LR
    subgraph backends["백엔드 (pre-15)"]
        B1["백엔드 1<br/>로컬 델타"]
        B2["백엔드 2<br/>로컬 델타"]
        B3["백엔드 N<br/>로컬 델타"]
    end
    COLL["통계 수집기 프로세스<br/>모든 카운터의 유일한 사본"]
    FILE[("pgstat.stat 파일<br/>(파일 전체 직렬화)")]
    READER["pg_stat_* 뷰를<br/>읽는 백엔드"]
    B1 -. "UDP 데이터그램<br/>(버려질 수 있음)" .-> COLL
    B2 -. "UDP 데이터그램<br/>(버려질 수 있음)" .-> COLL
    B3 -. "UDP 데이터그램<br/>(버려질 수 있음)" .-> COLL
    COLL -- "타이머 기록" --> FILE
    FILE -- "쿼리마다 전체 파일<br/>읽기 + 역직렬화" --> READER

앞 절에서 꼽은 세 약점 — 손실, 단일 소비자 병목, 파일 전체 읽기 — 이 모두 이 다이어그램에 드러난다. 수집기는 유일한 쓰기 주체이므로 총합은 내부적으로 일관성을 유지하지만, 숫자가 존재하는 유일한 장소이기도 하고, UDP 폭주를 홀로 소화하는 유일한 프로세스이기도 하며, 모든 쿼리마다 전체를 다시 읽어야 하는 파일의 원천이기도 했다. PG15가 이 세 가지를 동시에 해결했다.

PG10 (2017) — CREATE STATISTICS: 함수적 종속성 + 다변량 n-distinct

섹션 제목: “PG10 (2017) — CREATE STATISTICS: 함수적 종속성 + 다변량 n-distinct”

PG10은 확장 통계(extended statistics) 기능과 CREATE STATISTICS 명령을 도입했다. 통계 객체의 정의(대상 열, 종류)를 담는 새 카탈로그 pg_statistic_ext와 함께, 통계 데이터도 별도로 저장하는 구조였다. 첫 릴리스는 두 종류를 제공했고, 둘 다 열 독립성 문제를 정면으로 겨냥했다.

함수적 종속성(STATS_EXT_DEPENDENCIES, 'f'). 함수적 종속성 a -> b는 열 a의 값을 알면 열 b의 값이 결정된다는 뜻이다(도시/주 예시). PostgreSQL은 “예/아니오” 불리언을 저장하지 않고, 정도(degree) — 표본에서 종속성이 얼마나 자주 성립하는지를 나타내는 [0,1] 범위 숫자 — 를 저장한다. dependencies.c에서 a 기준으로 정렬한 뒤 각 동일 a 그룹 내에서 b가 상수인지 확인하는 방식으로 계산한다. 계획 시점에 종속성은 추정기가 상관 절을 곱하기를 멈추도록 한다. ab를 함수적으로 결정한다면, b 절이 독립적인 선택도를 거의 추가하지 않으므로 결합 추정치는 a의 선택도 쪽으로 끌어올려진다.

다변량 n-distinct(STATS_EXT_NDISTINCT, 'd'). 단일 stadistinct는 “열 a에 고유값이 몇 개인가?”를 답한다. 다변량 버전은 “(a, b, c) 조합이 몇 가지인가?”를 답한다. mvdistinct.c에서 열 그룹에 Haas–Stokes Duj1 추정기를 적용한다. GROUP BY a, b 기수성과 그룹 등치 선택도의 분모를 직접 수정하는데, 두 열이 상관돼 있을 때 독립성 곱이 심하게 틀리는 부분이 바로 이 분모다.

구조적 변화는 정의데이터가 이제 pg_statistic과 별개의 카탈로그 객체로 분리됐다는 점, 그리고 **선택 사항(opt-in)**이라는 점이다. DBA가 직접 CREATE STATISTICS를 실행해 열 그룹을 지정해야 한다. 열 단위 경로는 손대지 않았다. 확장 통계는 플래너가 추가로 참조하는 보정 레이어다. ANALYZE는 테이블의 pg_statistic_ext 행을 순회하며, 각 행의 요청된 종류를 채우는 확장 통계 빌더를 호출하도록 변경됐다. 추정기 측 결합 규칙과 빌드 진입점은 postgres-extended-statistics.md에 정리돼 있다.

PG12 (2019) — 확장 통계의 다변량 MCV 목록

섹션 제목: “PG12 (2019) — 확장 통계의 다변량 MCV 목록”

PG10의 두 종류는 집합적 상관 — 고유 조합이 몇 가지인지, 한 열이 다른 열을 결정하는지 — 을 다루지만, 특정 값 조합의 선택도는 추정할 수 없었다. 함수적 종속성은 종속성이 균등하게 성립한다고 가정하고, 어느 (city, state) 쌍이 흔한지는 말하지 못한다. 치우치고 상관된 데이터에서는 그 균등성 가정 자체가 틀린다.

PG12는 세 번째 확장 통계 종류인 다변량 MCV 목록(STATS_EXT_MCV, 'm', mcv.c 구현)을 추가했다. 열 단위 MCV 목록의 다중 열 버전이다. 선언된 열들의 가장 흔한 값 조합 목록이며, 각 조합의 관측 빈도와 함께 기준 빈도(독립성 가정 빈도)도 저장한다. 플래너가 각 조합이 독립성에서 얼마나 벗어나는지 알 수 있게 하기 위해서다. ANALYZE는 조합을 표본으로 추출하고, 열 단위 MCV에 쓰는 것과 같은 통계적 임계값을 넘는 조합을 유지한다.

추정기 변경이 핵심이다. 절 선택도를 완전히 곱하는(독립성) 방식과 완전히 수렴시키는(엄격한 함수적 종속성) 방식 사이에서, 다변량 MCV 경로는 매칭 조합의 실제 결합 빈도를 읽고 잔여 분에는 혼합 규칙을 적용한다.

P(a, b) = f * Min(P(a), P(b)) + (1 - f) * P(a) * P(b)

f는 MCV 목록이 분포의 얼마나 많은 부분을 설명하는지를 반영한다. MCV 목록이 흔하고 상관된 분포의 머리 부분을 정확하게 커버하고, 독립성 곱이 긴 꼬리를 처리한다. PG10 두 종류보다 표현력이 높기 때문에, 통상적인 CREATE STATISTICS s (ndistinct, dependencies, mcv) ON a, b FROM t는 세 종류를 모두 요청한다. 세 종류는 서로 보완하며 중복이 아니다. 결합 로직(statext_clauselist_selectivity, mcv_clauselist_selectivity)은 postgres-extended-statistics.md에 상세히 나온다.

PG12에서 구조적 형태는 바뀌지 않았다. 새 카탈로그도, 새 최상위 경로도 없었다. 세 번째 'm' 종류가 같은 pg_statistic_ext의 활성화 종류 집합과 같은 ANALYZE 빌드 루프에 추가됐을 뿐이다. 이것이 바로 PG10의 개방형 설계가 허용하도록 만들어진 확장 방식이다.

PG14 (2021) — 표현식 통계와 상속 인식 확장 통계

섹션 제목: “PG14 (2021) — 표현식 통계와 상속 인식 확장 통계”

PG13까지 확장 통계 카탈로그는 사이의 상관을 기술할 수 있었지만, 열에 한정됐다. 표현식을 기반으로 한 술어 — WHERE lower(email) = '...', WHERE (a + b) > 100, WHERE date_trunc('day', ts) = ... — 에는 통계가 전혀 없었다. 플래너는 하드코딩된 기본 선택도로 폴백했고, 그 값은 수십 배 단위로 틀리는 일이 잦았다.

PG14는 두 가지 변경으로 이 공백을 메웠다.

표현식 통계(STATS_EXT_EXPRESSIONS, 'e'). CREATE STATISTICS가 열 이름뿐 아니라 임의 표현식도 받을 수 있게 됐다. ANALYZE는 선언된 각 표현식을 표본에서 평가한 뒤, 결과 값에 일반 열 단위 통계 기계를 돌린다. nullfrac, stadistinct, MCV 목록, 히스토그램이 표현식 단위로 확장 통계 데이터에 저장된다. extended_stats.c에서는 이것이 빌드·직렬화 루프의 STATS_EXT_EXPRESSIONS 분기로 나타난다. 통계 객체가 가진 표현식들을 eval_const_expressions로 접어 하나씩 분석한다. 결과적으로 표현식 하나짜리 CREATE STATISTICS 객체는 “이 표현식에 실제 열과 같은 통계를 부여하라”처럼 동작한다. 실제 인덱스를 만들고 유지 비용을 감수하는 함수 인덱스 우회로가 이전에도 있었지만, 표현식 통계는 통계와 인덱스를 분리한다.

상속 인식 확장 통계(stxdinherit). pg_statistic_ext_data 카탈로그에 불리언 stxdinherit 열이 추가되고, 기본 키가 (stxoid, stxdinherit) 두 열로 확장됐다. 열 단위 pg_statistic의 오래된 stainherit 플래그를 거울처럼 따른 구조다. 파티션 테이블이나 상속 테이블은 이제 확장 통계를 두 벌 저장한다. stxdinherit = false는 부모 테이블 자체 행만 설명하고, stxdinherit = true는 부모와 모든 상속·파티션 자식을 포함해 설명한다. 플래너는 비용을 계산 중인 스캔 범위에 맞는 것을 선택한다. 이전에는 파티션 루트의 확장 통계가 사실상 쓸모가 없었다. 전체 자식 분포를 기록할 자리가 없었기 때문이다. 형태 변화는 카탈로그 헤더에서 직접 확인된다.

// pg_statistic_ext_data — src/include/catalog/pg_statistic_ext_data.h
Oid stxoid; /* statistics object this data is for */
bool stxdinherit; /* true if inheritance children are included */
/* ... pg_ndistinct, pg_dependencies, pg_mcv_list payload columns ... */

PG14로 플래너 통계 경로는 현재 형태에 사실상 도달했다. 독립성 기준선을 위한 열 단위 pg_statistic, 열 그룹 표현식에 대한 선택 사항의 확장 통계, 각각 자체 행과 자식 포함 변형. 이후는 이 틀 안에서의 세부 개선이다. REL_18 기준 현재 메커니즘은 postgres-extended-statistics.md에 있다.

PG15 (2022) — 수집기 제거: 공유 메모리 누적 통계

섹션 제목: “PG15 (2022) — 수집기 제거: 공유 메모리 누적 통계”

두 경로에서 일어난 단일 변경 중 가장 크다. 경로도 바뀌었다. 플래너 통계는 아무것도 바뀌지 않았고, 누적 통계 서브시스템이 완전히 재설계됐다. PG15(커밋 5891c7a8e, Andres Freund)는 통계 수집기 프로세스를 완전히 제거하고, UDP 메시지·단일 소유자·파일 전체 읽기 모델을 src/backend/utils/activity/ 아래의 공유 메모리 서브시스템으로 교체했다.

이 재설계는 Era 0의 세 약점 각각을 직접 공략했다.

  • 손실 → 정확. UDP가 없어졌다. 백엔드가 공유 메모리에서 직접 카운터를 갱신하므로(버퍼링된 로컬 펜딩 항목으로), 델타가 조용히 사라지지 않는다.
  • 단일 병목 → 중앙 소비자 없음. 폭주 데이터를 받아내는 프로세스가 없다. 모든 백엔드가 세밀한 잠금 아래 공유 메모리의 자기 슬라이스에 직접 쓴다.
  • 파일 전체 읽기 → 대상 조회. pg_stat 읽기는 파일 전체를 역직렬화하는 대신 해시테이블에서 필요한 항목 하나를 찾는다.

데이터 구조 분리. 통계는 두 집단으로 나뉜다. 가변 개수 종류 — 릴레이션별, 함수별, 복제 슬롯별, 구독별, 백엔드별 — 는 PgStat_HashKey {kind, dboid, objid}를 키로 하는 DSA 기반 dshash에 산다. dshash 항목은 참조 카운트, 세대 카운터, “삭제됨” 플래그, 별도 할당된 가변 크기 통계 본체를 가리키는 dsa_pointer만 담는다. 고정 개수 종류 — 체크포인터, bgwriter, 아카이버, WAL, SLRU, (PG16부터) IO — 는 평범한 공유 메모리 제어 블록에 각각 한 슬롯씩 산다. 클러스터당 각각 정확히 하나뿐이기 때문이다.

핫 경로는 로컬을 유지한다. 백엔드가 튜플마다 공유 메모리를 건드리지는 않는다. 참조 카운팅된 PgStat_EntryRef 핸들로 로컬 해시테이블(pgStatEntryRefHash)에 접근하는 프로세스 로컬 펜딩 항목에 갱신을 버퍼링한다. pgstat_report_stat()은 각 항목 전용 LWLock 아래 펜딩 목록을 공유 메모리에 최대 초당 1회(커밋 시점에 강제) 플러시한다. 수명은 참조 카운팅으로 관리하고, 삭제는 트랜잭션 방식으로(커밋/중단 WAL 레코드에 기록해 복제본과 복구 후 일관성 유지), 체크포인터는 종료 시점에 전체 내용을 pg_stat/pgstat.stat에 직렬화해 시작 프로세스가 재적재하거나 크래시 후 폐기하도록 한다. dshash 항목 레이아웃, 참조 카운트·세대 경쟁 처리, 플러시 콜백 등 메커니즘 전체는 postgres-cumulative-stats.md에 정리돼 있다.

변경 전후 구조 비교:

flowchart TB
    subgraph before["변경 전 — PG14 이하"]
        bB1["백엔드"] -. "UDP 델타" .-> bC["통계 수집기<br/>(유일한 소유자)"]
        bC --> bF[("pgstat.stat<br/>파일 전체")]
        bF -- "전체 역직렬화" --> bR["읽기 백엔드"]
    end
    subgraph after["변경 후 — PG15+"]
        aB["백엔드<br/>로컬 펜딩 항목<br/>(PgStat_EntryRef)"]
        aB -- "pgstat_report_stat()<br/>항목별 LWLock 아래 최대 초당 1회 플러시" --> aSHM
        subgraph aSHM["공유 메모리"]
            aDSH["dshash: 가변 종류<br/>키 {kind, dboid, objid}<br/>-> dsa_pointer to body"]
            aFIX["고정 제어 블록<br/>checkpointer, bgwriter,<br/>archiver, WAL, SLRU, IO"]
        end
        aSHM -- "대상 조회" --> aR["읽기 백엔드"]
        aSHM -. "체크포인터가 종료 시 기록" .-> aFILE[("pg_stat/pgstat.stat")]
        aFILE -. "시작 프로세스 재적재<br/>크래시 후 폐기" .-> aSHM
    end
    before --> after

PostgreSQL이 도출한 교훈은 메시지 전달 대 공유 메모리의 고전적 트레이드오프다. 수집기의 UDP 모델은 완벽한 쓰기 격리(오직 하나의 프로세스만 총합을 변경)를 제공했지만, 손실, 직렬화 병목, 읽기 비용이라는 대가를 치렀다. 항목별 잠금을 쓰는 공유 메모리는 “단일 쓰기 주체”라는 단순함을 포기하는 대신 정확성, 확장성, 저렴한 읽기를 얻었다. 일관성은 단일 소유자 대신 참조 카운팅과 트랜잭션 방식 삭제로 회복한다.

PG16 (2023) — pg_stat_io: I/O를 일급 누적 stat kind로

섹션 제목: “PG16 (2023) — pg_stat_io: I/O를 일급 누적 stat kind로”

PG16 이전에는 I/O 집계가 분산돼 있었다. 블록 읽기·적중 카운터는 릴레이션별 통계에 분산됐고, 누가 어떤 이유로 I/O를 일으키는지를 클러스터 전체에서 분류해 볼 방법이 없었다. “순차 스캔의 대량 읽기 전략과 일반 버퍼 접근이 읽기 트래픽에서 각각 얼마나 차지하는지, vacuum과 체크포인터가 각각 얼마나 읽는지”를 쉽게 답할 수 없었다.

PG16은 완전히 새로운 고정 누적 stat kind인 PGSTAT_KIND_IO와 그 위의 pg_stat_io 뷰를 추가했다. 누적 서브시스템이 PG15에서 플러그인 가능한 stat kind 중심으로 이미 재설계돼 있었으므로, I/O 추가는 자체 플러시 콜백을 가진 새 고정 kind를 등록하는 일이었다. 재설계가 아니라 PG15 아키텍처의 이점을 회수하는 것이었다. 새 kind는 같은 공유 메모리 제어 블록과 같은 pgstat_report_stat() 플러시 규율에 그대로 편입됐다.

pg_stat_io백엔드 타입(클라이언트 백엔드, 오토베큠 워커, 체크포인터, bgwriter, …), I/O 객체(릴레이션, 임시 릴레이션), I/O 컨텍스트(normal, vacuum, bulkread, bulkwrite) 차원으로 카운트를 보고한다. 각 셀은 읽기, 쓰기, 확장, 적중, 축출, 재사용, fsync 등을 집계한다. 특정 (백엔드 타입, 컨텍스트) 쌍으로 물리 I/O를 귀속시킬 수 있게 됐는데, 수년간 가장 많이 요청된 I/O 관측가능성 기능이었다. REL_18에서도 살아있는 stat kind 테이블에서 새 kind가 확인된다.

// pgstat_kind.h — 고정 개수 kind, REL_18
#define PGSTAT_KIND_ARCHIVER 7
#define PGSTAT_KIND_BGWRITER 8
#define PGSTAT_KIND_CHECKPOINTER 9
#define PGSTAT_KIND_IO 10 /* PG16 추가 */
#define PGSTAT_KIND_SLRU 11
#define PGSTAT_KIND_WAL 12

PG18 (2025) — 백엔드별 I/O, pg_stat_io의 WAL·바이트 단위 열

섹션 제목: “PG18 (2025) — 백엔드별 I/O, pg_stat_io의 WAL·바이트 단위 열”

PG18은 PG15 아키텍처를 건드리지 않으면서 I/O 집계를 두 축으로 개선했다.

백엔드별 I/O 통계. PG18은 PGSTAT_KIND_BACKEND(kind 6) — 백엔드별 키를 쓰는 가변 개수 kind — 와 pg_stat_get_backend_io() 함수를 추가했다. 이제 I/O는 백엔드 타입 집계 수준이 아니라 개별 세션 단위로 귀속될 수 있다. 디스크를 강하게 사용하는 장기 실행 세션을 타입 수준 총합에서 추론하지 않고 직접 식별할 수 있다. 가변 개수 kind이므로 릴레이션별·함수별 통계를 담는 것과 같은 dshash 기계에 산다. 항목은 세션 수명 동안 같은 참조 카운팅 PgStat_EntryRef 경로로 생성·소멸한다.

pg_stat_io에 WAL과 바이트 단위 열 통합. PG18의 pg_stat_io 뷰에는 바이트 단위 열(read_bytes, write_bytes, 연산 단위 크기를 나타내는 op_bytes)과 WAL I/O 객체를 릴레이션 I/O와 구별하는 object 열이 추가됐다. WAL 읽기·쓰기를 릴레이션 I/O와 같은 뷰, 같은 바이트 단위로 볼 수 있게 됐다. REL_18의 뷰 정의에서 이를 직접 확인할 수 있다.

-- pg_stat_io — src/backend/catalog/system_views.sql (REL_18)
CREATE VIEW pg_stat_io AS
SELECT
b.backend_type,
b.object, -- relation vs WAL (PG18)
b.context,
...
b.read_bytes, -- 바이트 단위 집계 (PG18)
...

고정 크기 블록이 아니라 바이트로 집계하는 이유가 있다. WAL과 일부 I/O 경로는 8 KB 페이지 단위가 아닌 단위로 동작한다. op_bytes가 연산 단위를 기록하므로 카운트에서 정확한 바이트 수를 계산할 수 있다.

PG16·PG18의 변경은 의도적으로 가산적이다. 각각은 새 stat kind이거나 기존 kind에 새 열을 추가한 것으로, PG15 공유 메모리 인프라 위에 얹혔다. 어느 것도 중앙 프로세스나 파일 전체 읽기를 다시 도입하지 않는다.

REL_18에서 두 경로는 각각 안정적인 형태에 도달했고, 모듈 문서가 메커니즘 수준 상세를 설명한다.

플래너 통계. ANALYZE(src/backend/commands/analyze.c)는 독립성 기준선으로 열 단위 pg_statistic 행을 계속 생성한다. nullfrac, width, stadistinct, MCV, 히스토그램, 상관이 들어간다. 그 위에 확장 통계(src/backend/statistics/extended_stats.cdependencies.c / mvdistinct.c / mcv.c 빌더)가 선택적으로 상관 인식 통계를 제공한다. 선언된 열 그룹 표현식을 대상으로 네 종류 — 함수적 종속성('f'), 다변량 n-distinct('d'), 다변량 MCV('m'), 표현식 통계('e') — 가 각각 자체 행과 자식 포함(stxdinherit) 변형으로 pg_statistic_ext_data에 저장된다. 추정기는 독립성 곱 대신 MCV-잔여 혼합으로 상관 절을 결합한다. 전체 메커니즘은 postgres-extended-statistics.md에 있다.

누적 통계. 통계 수집기 프로세스는 없다. src/backend/utils/activity/pgstat.c(및 pgstat_*.c 형제 파일)의 공유 메모리 서브시스템이 가변 개수 kind는 DSA/dshash에, 고정 개수 kind는 제어 블록에 담는다. REL_18에서 내장 kind는 PGSTAT_KIND_DATABASE(1)부터 PGSTAT_KIND_WAL(12)까지 실행되며, PGSTAT_KIND_BACKEND(6, 백엔드별, PG18)와 PGSTAT_KIND_IO(10, PG16)를 포함한다. 24 이상의 ID는 사용자 정의(익스텐션) stat kind를 위해 예약돼 있다. 백엔드는 로컬에 펜딩 델타를 버퍼링하고 항목별 LWLock 아래 최대 초당 1회 플러시한다. 체크포인터는 종료 시 pg_stat/pgstat.stat에 직렬화하고, 시작 프로세스가 재적재하거나 크래시 후 폐기한다. 전체 메커니즘은 postgres-cumulative-stats.md에 있다. 실시간 상태 대응물 — 스냅샷 방식의 웨이트 이벤트와 진행 보고 — 은 postgres-wait-events-progress.md에 다룬다.

다음 단계. PostgreSQL 19(다음 메이저 릴리스, 개발 중)는 두 경로 어느 쪽에도 구조적 재설계를 도입하기보다, PG15 공유 메모리 프레임에 새 stat kind와 익스텐션 정의 kind를 계속 추가하고 플래너 통계를 점진적으로 개선하는 가산적 패턴을 이어갈 것으로 예상된다. PG15의 수집기 제거와 PG10–PG14의 확장 통계 프레임이 이후 모든 발전의 토대가 된 두 아키텍처 결정이다.

모듈 문서 (현재 상태 메커니즘 — 여기서 재유도하지 않음):

릴리스 노트 (릴리스 귀속):

  • PostgreSQL 10 릴리스 노트 — CREATE STATISTICS (함수적 종속성, 다변량 n-distinct).
  • PostgreSQL 12 릴리스 노트 — 확장 통계의 다변량 MCV 목록.
  • PostgreSQL 14 릴리스 노트 — 표현식 통계; 상속 인식 확장 통계.
  • PostgreSQL 15 릴리스 노트 — 통계 수집기 제거; 누적 통계를 공유 메모리로 이전 (커밋 5891c7a8e).
  • PostgreSQL 16 릴리스 노트 — pg_stat_io.
  • PostgreSQL 18 릴리스 노트 — 백엔드별 I/O 통계; pg_stat_io의 WAL과 바이트 단위 열.

핵심 소스 파일 (REL_18, 커밋 273fe94 기준):

  • src/backend/utils/activity/pgstat.c — 누적 통계 핵심.
  • src/include/utils/pgstat_kind.h — stat kind ID 레지스트리 (PGSTAT_KIND_*).
  • src/backend/statistics/extended_stats.c — 확장 통계 빌드·추정 오케스트레이션.
  • src/backend/commands/analyze.cANALYZE 표본 추출과 열 단위 compute_stats.
  • src/include/catalog/pg_statistic_ext.h, src/include/catalog/pg_statistic_ext_data.h — 확장 통계 카탈로그 (정의와 데이터, stxdinherit 포함).
  • src/backend/catalog/system_views.sqlpg_stat_iopg_stat_* 뷰 정의.