Ken
"데이터를 옮기는 건 ClickPipes가 다 했다. 진짜 문제는 지난 몇 년간 쌓인 PostgreSQL 분석 SQL을 다시 쓰는 일이다." — David Wheeler가pg_clickhouse를 출시하며 던진 한 문장에 이 익스텐션의 존재 이유가 다 들어 있다.2025년 12월 ClickHouse가 공개한
pg_clickhouse(Apache 2.0)는 PostgreSQL의 FDW(Foreign Data Wrapper) 메커니즘 위에 적극적 푸시다운을 얹어, PG 쪽 쿼리를 고치지 않고 ClickHouse에서 실행시키는 익스텐션이다. 2026년 1월 ClickBench에서 모든 PG 분석 익스텐션 중 가장 빠른 1위를 차지하며 네이티브 ClickHouse 성능에 근접한다.이 글은 PG 분석 익스텐션 지형 안에서 pg_clickhouse의 위치를 짚고, 푸시다운이 실제로 어디까지 동작하는지, 언제 쓰고 언제 쓰지 말아야 하는지를 use case 중심으로 정리한다. 실습은 ClickHouse가 배포한 도커 이미지(
ghcr.io/clickhouse/pg_clickhouse:18) 위에서 진행했다.
1. 시작하며 — 데이터보다 쿼리가 더 무거운 이유
전형적인 시나리오. Postgres 위에 빌드한 SaaS 앱이 잘 커서 분석 쿼리가 OLTP의 발목을 잡기 시작한다. Read replica → CDC → 별도 분석 DB로의 길은 잘 알려져 있고, ClickPipes로 데이터 이전은 분 단위로 끝난다. 그런데 그 다음 단계가 길다.
옮기는 대상 | 대표 도구 | 통상 소요 시간 |
데이터 (스키마 + 행) | ClickPipes, Debezium, 자체 ETL | 시간 ~ 일 |
쿼리 (대시보드/ORM/크론) | (사람이 손으로) | 주 ~ 분기 |
대시보드 200개, ORM 코드 수천 곳, 크론잡 수십 개에 박힌 분석 SQL을 ClickHouse 방언(quantile(), argMax(), uniq(), -If combinator 등)으로 다시 쓰는 데 진짜 시간이 든다. pg_clickhouse는 이 단계를 통째로 건너뛰게 해주는 것을 목표로 만들어졌다.
핵심 아이디어 한 줄: ClickHouse 테이블이 PG의 일반 테이블처럼 보이고, PG 분석 SQL이 들어오면 자동 번역돼 ClickHouse가 실행하고, 결과 행만 PG로 돌아온다.
2. PG 분석 익스텐션 지형 안에서의 pg_clickhouse
PG로 분석을 빠르게 하려는 시도는 2024–2026년 사이 폭발적으로 늘었다. 큰 줄기는 두 카테고리:
차이가 갖는 운영상 의미:
축 | A. 내장형 (pg_duckdb / pg_mooncake) | B. FDW형 (pg_clickhouse) |
데이터 위치 | PG 인스턴스 또는 PG가 관리하는 object store | 별도 ClickHouse 클러스터 |
컴퓨트 스케일링 | PG 노드와 묶임 | ClickHouse와 독립 |
OLTP에 대한 영향 | 같은 프로세스/메모리 풀 — 격리 약함 | 격리 강함 (다른 호스트) |
기존 ClickHouse 자산 활용 | 불가 | 그대로 활용 |
라이선스 | DuckDB(MIT) / 기타 | Apache 2.0 |
pg_clickhouse를 선택해야 하는 결정 근거는 단순하다 — "이미 ClickHouse(또는 ClickHouse Cloud)에 데이터가 있고, PG OLTP는 OLTP대로 두고 싶다." 내장형이 PG와 분석 엔진을 한 박스에 묶는 반면, FDW는 두 시스템을 명확히 분리한 채 쿼리만 다리를 놓는다.
기존 clickhouse_fdw(2019–2020 Ildus Kurbangaliev / Adjust 기반)와의 차이도 짚어둘 만하다. pg_clickhouse는 그 코드를 import해 시작했지만, push-down 적극화에 집중해 대대적으로 재작성했다 — SEMI JOIN 지원, ordered-set aggregate 변환, FILTER (WHERE) → -If combinator 번역 등이 핵심 차별점이다.
3. 아키텍처 — Foreign Data Wrapper + binary 프로토콜
pg_clickhouse는 PostgreSQL의 SQL/MED(SQL Management of External Data, 2003년 표준) 위에 구축된다. PG가 9.3(2011) 이후 지원해온 FDW 메커니즘이라 PG 측 코드 변경 0에 익스텐션 한 줄로 들어간다.
운영자가 기억해야 할 두 가지 전송 옵션:
driver | 포트 | 특성 |
binary | 9000 | 네이티브 TCP. 더 빠르고 스트리밍 가능. 권장 |
http | 8123 | HTTP. 방화벽/프록시 친화적, https로 TLS 가능 |
ClickHouse Cloud로 붙으려면 TLS 필수 — pg_clickhouse는 두 프로토콜 모두에 TLS를 지원한다 (기존 clickhouse_fdw에는 없던 부분).
4. 5분 셋업 — 도커 한 줄
ClickHouse가 PG 13~18용 사전 빌드 이미지(ghcr.io/clickhouse/pg_clickhouse:{13..18})를 제공해 설치 단계가 자체로 사라진다. 도커로 둘을 비공개 네트워크에 띄우고 익스텐션을 켠다:
# docker-compose.yml (핵심만)
services:
postgres:
image: ghcr.io/clickhouse/pg_clickhouse:18 # PG 18.4 + pg_clickhouse 0.3
networks: [pgch_net]
ports: ["5432:5432"]
clickhouse:
image: clickhouse/clickhouse-server:26.5
networks: [pgch_net]
ports: ["8123:8123", "9000:9000"]
networks: { pgch_net: { driver: bridge } }PG에 접속해서:
CREATE EXTENSION pg_clickhouse;
CREATE SERVER ch_srv
FOREIGN DATA WRAPPER clickhouse_fdw
OPTIONS (driver 'binary', host 'clickhouse', port '9000', dbname 'default');
CREATE USER MAPPING FOR CURRENT_USER
SERVER ch_srv
OPTIONS (user 'default', password '');세 줄로 연결까지 끝. clickhouse_fdw라는 이름은 익스텐션이 등록하는 FDW 핸들러의 이름이지 별도 익스텐션을 깔라는 뜻이 아니다 (혼동 포인트 1번).
연결 검증은 raw-query escape hatch로:
SELECT clickhouse_raw_query('SELECT version()', 'host=clickhouse port=8123');
-- 26.5.1.882보안 노트:clickhouse_raw_query는 기본적으로EXECUTE권한이 슈퍼유저에만 부여돼 있다. 일반 분석 사용자에게는GRANT EXECUTE를 명시적으로 골라서 줘야 한다. 다음 섹션 9에서 자세히.
5. IMPORT FOREIGN SCHEMA — 100개 테이블을 한 줄에
기존 ClickHouse 측 스키마가 있다고 가정. lab.events(100K) + lab.users(1K)가 있다고 하면:
-- PG 쪽: 받을 schema 만들고 한 번에 import
CREATE SCHEMA imported_lab;
IMPORT FOREIGN SCHEMA lab FROM SERVER ch_srv INTO imported_lab;끝. ClickHouse 측 모든 테이블이 PG의 imported_lab.* 아래 foreign table로 자동 생성된다. 컬럼 타입은 ClickHouse → PG 매핑 표로 자동 변환된다.
5.1 타입 매핑 — 알아야 할 함정 몇 개
ClickHouse | PostgreSQL | 주의 |
UInt8, UInt16 | smallint, integer | |
UInt32, UInt64 | bigint | UInt64 오버플로 시 wrap 대신 ERROR |
Int* | 동급 PG int | |
Float32, Float64 | real, double precision | |
Decimal(p,s) | numeric(p,s) | |
Date, Date32 | date | |
DateTime, DateTime64 | timestamptz | http 드라이버 + 파라미터화 쿼리는 CH < 25.8에서 TZ 이슈 |
String | text | 바이너리는 NUL 바이트에서 잘림 → bytea 권장 |
LowCardinality(T) | T와 동일 | |
UUID | uuid | |
IPv4, IPv6 | inet | |
Bool | boolean | |
JSON | jsonb / json |
부분 임포트 변형:
IMPORT FOREIGN SCHEMA lab LIMIT TO (events) FROM SERVER ch_srv INTO imported_lab;
IMPORT FOREIGN SCHEMA lab EXCEPT (users) FROM SERVER ch_srv INTO imported_lab;운영 함정 #2 — 대소문자 식별자. ClickHouse 측 컬럼명이 대소문자 혼합이면 PG에서는 큰따옴표로 감싸야 한다("CamelCol"). 임포트는 잘 되지만 쿼리 시점에 깨진다.6. 푸시다운 — EXPLAIN (VERBOSE)의 Remote SQL이 진실을 말한다
여기가 pg_clickhouse의 핵심이고, 글의 절반을 차지해야 할 부분이다. 익스텐션이 진짜 일하는지 보는 단 하나의 방법은 EXPLAIN (VERBOSE)의 Remote SQL: 줄을 읽는 것.
6.1 단순 집계 푸시다운
EXPLAIN (VERBOSE)
SELECT category, count(*), avg(amount)
FROM imported_lab.events
WHERE created_at >= now() - INTERVAL '7 days'
GROUP BY category; Foreign Scan
Relations: Aggregate on (events)
Remote SQL: SELECT category, count(*), avg(amount) FROM lab.events
WHERE ((created_at >= now() - INTERVAL '7 days'))
GROUP BY categoryAggregate on (events) — PG가 행을 가져와서 집계하는 게 아니라, ClickHouse가 집계까지 끝내고 결과만 보낸다는 뜻. 이게 안 보이면 pushdown이 깨진 것이다.
6.2 JOIN 푸시다운 — 두 foreign table을 ClickHouse가 직접 조인
EXPLAIN (VERBOSE)
SELECT u.country, count(DISTINCT e.user_id) AS uniq_users
FROM imported_lab.events e
JOIN imported_lab.users u USING (user_id)
WHERE e.event_type = 'purchase'
GROUP BY u.country; Foreign Scan
Relations: Aggregate on ((events) INNER JOIN (users))
Remote SQL: SELECT r2.country, count(DISTINCT r1.user_id)
FROM lab.events r1
INNER JOIN lab.users r2 ON (r1.user_id = r2.user_id)
WHERE r1.event_type = 'purchase'
GROUP BY r2.countryPG 측 옵티마이저가 두 foreign table을 한 묶음으로 인식하고 조인을 ClickHouse 내부로 밀어 넣었다. 일반 FDW가 가장 자주 실패하는 케이스 — 보통은 양쪽에서 행을 다 가져와 PG 메모리에서 hash join하다가 OOM. 적극적 푸시다운의 의의가 여기서 가장 크다.
6.3 PG → ClickHouse 함수 자동 번역
pg_clickhouse가 흥미로워지는 지점은 함수 이름이 다른 경우를 알아서 번역한다는 것.
PostgreSQL | → 변환 | ClickHouse |
percentile_cont(0.5) WITHIN GROUP (ORDER BY price) | → | quantile(0.5)(price) |
min(price) FILTER (WHERE town='X') | → | minIf(price, town='X') |
date_trunc('hour', ts) | → | toStartOfHour(ts) |
regexp_replace(s, p, r) | → | replaceRegexpAll(s, p, r) |
count(DISTINCT x) | → | uniqExact(x) |
row_number() OVER (...) | → | row_number() OVER (...) |
특히 **FILTER (WHERE) → -If 콤비네이터** 변환이 인상적이다. PG의 ordered-set aggregate를 *ClickHouse의 parametric aggregate*로 매핑하면서, FILTER절을 ClickHouse 함수 접미사-If`로 변형한다. 두 엔진의 의미를 가장 효율적인 형태로 정렬하는 작업.
6.4 SEMI JOIN — 출시 첫 분기의 최대 진전
TPC-H 쿼리 4번 같은 EXISTS 서브쿼리가 가장 잘 보여주는 사례. 원래 FDW들은 EXISTS를 분리된 foreign scan + nested loop로 풀어서 수십 초~분 단위 소요. pg_clickhouse는 0.1.0에서 PG SEMI JOIN을 그대로 ClickHouse의 LEFT SEMI JOIN으로 푸시다운하도록 만들었다.
-- TPC-H Q4
SELECT o_orderpriority, count(*)
FROM orders
WHERE o_orderdate >= date '1993-07-01' AND o_orderdate < date '1993-10-01'
AND EXISTS (SELECT * FROM lineitem
WHERE l_orderkey = o_orderkey AND l_commitdate < l_receiptdate)
GROUP BY o_orderpriority;번역 결과:
Remote SQL: SELECT r1.o_orderpriority, count(*)
FROM tpch.orders r1
LEFT SEMI JOIN tpch.lineitem r3
ON ((r3.l_commitdate < r3.l_receiptdate))
AND ((r1.o_orderkey = r3.l_orderkey))
WHERE ((r1.o_orderdate >= '1993-07-01'))
AND ((r1.o_orderdate < '1993-10-01'))
GROUP BY r1.o_orderpriority성능 차이는 극적이다 — 0.1.0 출시 블로그에서 공개한 TPC-H SF1 측정:
Query | Native PG | pg_clickhouse (SEMI 전) | pg_clickhouse (SEMI 후) |
Q1 | 4,478 ms | 82 ms | 73 ms |
Q3 | 1,454 ms | 74 ms | 74 ms |
Q4 | 650 ms | timeout | 67 ms |
Q5 | 452 ms | timeout | 104 ms |
Q9 | 3,028 ms | timeout | 136 ms |
Q17 | 1,802 ms | timeout | 9 ms |
Q18 | 6,185 ms | timeout | 10 ms |
요약: SEMI JOIN 지원만으로 timeout 났던 8개 쿼리가 살아났고, 22개 중 21개가 1초 이내로 떨어졌으며, 12개가 완전 푸시다운됐다.
ClickBench 측에서도 2026년 1월 결과 기준 pg_clickhouse가 모든 PG 분석 익스텐션 중 가장 빠르며, 네이티브 ClickHouse와의 격차도 작다. 오버헤드는 쿼리 재작성 + 네트워크 왕복 + PG 타입 변환에서만 발생.
6.5 푸시다운 함수 레퍼런스
자주 찾게 되는 것만 모은 미니맵:
전체 목록은 공식 레퍼런스 참고. 이 표에 없는 함수는 PG 측에서 실행되므로 — 행이 PG로 끌려와 PG가 계산한다 — 푸시다운이 깨지는 일등 원인이다.
7. 세션 단위 ClickHouse 설정 전달
ClickHouse 측 설정을 쿼리별로 조절해야 할 때:
SET pg_clickhouse.session_settings = 'connect_timeout 5, max_block_size 8192';기본값은 'join_use_nulls 1, group_by_use_nulls 1, final 1' — 즉 NULL semantics를 PG에 맞추고 FINAL을 기본으로 켠다. ReplacingMergeTree/CollapsingMergeTree를 쓰는 ClickHouse 테이블이라면 이 기본값이 정확한 dedup된 결과를 보장한다.
성능을 위해 final 0로 끄거나 max_threads, max_memory_usage 같은 자원 한도를 워크로드별로 다르게 줄 수 있다.
8. clickhouse_raw_query — 탈출구와 그 비용
pg_clickhouse가 모든 SQL을 푸시다운할 수는 없다. ClickHouse 전용 기능(예: dictGet()을 SELECT 리스트에 직접 사용, ANN 인덱스, arrayJoin 등)을 써야 한다면 raw escape가 답이다.
SELECT clickhouse_raw_query(
'SELECT dictGet(country_dict, ''name'', toUInt32(123))',
'host=clickhouse port=8123'
) AS country_name;특히 딕셔너리 활용이 흥미로운 패턴 — ClickHouse에 DICTIONARY를 만들어 두면 WHERE dictGet(...) = 'X'는 푸시다운되고, SELECT 리스트의 dictGet은 raw query로 우회한다. 코드 패턴:
8.1 보안 — clickhouse_raw_query는 잠금 상태로 출고된다
기본값으로 EXECUTE 권한이 슈퍼유저에게만 있다. 일반 분석 사용자가 호출하면 즉시 거절. 임의 ClickHouse SQL을 실행할 수 있다는 건 SQL injection 벡터가 곧장 열린다는 뜻이라 의도된 설계.
선택적 부여가 운영의 정답:
SECURITY DEFINER + 입력 검증 래퍼를 통해 어떤 dictionary에 대해 어떤 key 타입만 허용할지 좁히는 패턴이 표준.
9. 언제 pg_clickhouse를 써야 하나 — 의사결정 가이드
핵심 use case 3가지로 정리하면:
Use case 1 — Lift-and-shift 마이그레이션
PG 위에 빌드한 SaaS의 OLAP 워크로드를 분리하려는데, 대시보드/ORM 코드의 분석 SQL이 광범위하게 박혀 있다. ClickPipes로 데이터를 옮긴 뒤 search_path만 imported_lab,public처럼 바꾸면 기존 쿼리가 그대로 ClickHouse에서 돈다. 마이그레이션의 위험을 수개월 단위 SQL 재작성에서 몇 시간 단위 search_path 변경으로 줄인다.
Use case 2 — OLTP와 OLAP의 명시적 컴퓨트 격리
ClickHouse는 별도 호스트(또는 별도 Cloud 인스턴스). 분석 워크로드가 늘어도 PG OLTP CPU/메모리에 전혀 영향 없음. pg_duckdb처럼 PG 프로세스 내부로 분석 엔진을 박는 모델은 격리가 약하다는 점에서 정반대 결정.
Use case 3 — 동일 SQL 자산을 PG OLTP + CH OLAP 양쪽에서 운영
레거시 PG 데이터 + 신규 CH 데이터를 같은 분석 SQL로 다루고 싶을 때. 같은 BI 도구, 같은 ORM에서 FQN을 schema로 구분해 PG와 CH 테이블을 섞어 쓴다. pg_clickhouse가 차이를 숨겨준다.
반대로 pg_clickhouse가 맞지 않는 경우
- PG 자체로 데이터를 들고 있어야 하는 분석 (예: 단일 노드에서 끝나는 보고서, 외부 분석 DB 운영을 도입 못하는 환경) →
pg_duckdb/pg_mooncake가 더 적합. - 무거운 DML 워크로드를 푸시다운하고 싶을 때 → 0.x 단계에서는
INSERT위주만 지원,COPY도 내부적으로INSERT사용. 분석 read에 특화된 익스텐션이라 DML은 후순위 로드맵이다. - PG 13 이전 환경 → 미지원.
10. 운영상 함정 — 출시 1년 시점의 caveats
업스트림 문서가 솔직하게 공개한 항목 + 실습 중 부딪힐 만한 것들:
카테고리 | 함정 | 우회 |
쓰기 | COPY가 내부적으로 INSERT 사용 | 대량 적재는 ClickPipes / 직접 CH INSERT |
타입 | UInt64 오버플로 시 wrap 대신 ERROR | 캐스팅 명시 ( AS bigint 등으로 검사) |
타입 | TEXT 컬럼에 바이너리 — NUL 바이트에서 잘림 | bytea 사용 |
식별자 | 대소문자 혼합 컬럼명은 큰따옴표 강제 | snake_case로 정렬 또는 view 한 겹 |
윈도우 | 랭킹 윈도우 함수의 frame spec은 푸시다운 시 무시 | 결과 동일 확인 후 frame 제거 |
HTTP | http 드라이버 + parameterized query + DateTime: CH < 25.8 TZ 이슈 | binary 드라이버 또는 CH 25.8+ |
푸시다운 | 모든 PG 함수가 번역되지는 않음 | EXPLAIN VERBOSE로 Remote SQL 항상 확인 |
푸시다운 | TPC-H 22개 중 10개가 완전 푸시다운 안 됨 (0.1.0 기준) | 로드맵 진행 중 (0.1.4에서 ClickBench 커버리지 추가) |
가장 빈번한 실수: foreign table에 LIMIT 없이 SELECT * → 수백만 행이 PG로 끌려옴. 항상 LIMIT 또는 집계로 시작한다. 그리고 항상 EXPLAIN VERBOSE로 Remote SQL을 확인.
11. 마무리
pg_clickhouse를 한 줄로 요약하면 — "PG의 FDW 메커니즘을, 분석 워크로드에 정말로 쓸 수 있는 수준까지 푸시다운으로 끌어올린 익스텐션". 기존 clickhouse_fdw가 멈춰 있던 곳에서 SEMI JOIN, ordered-set aggregate 변환, -If combinator 번역까지 더해 TPC-H 22 중 21개를 sub-1s로 만들고, ClickBench 1위에 올랐다.
기억할 세 가지:
- 그것은 새로운 분석 엔진이 아니다. ClickHouse를 그대로 쓴다 — PG 분석 SQL을 ClickHouse에 자동 전달하는 다리일 뿐. 따라서 ClickHouse의 모든 성능과 모든 운영 의무가 그대로 적용된다.
EXPLAIN (VERBOSE)의 Remote SQL이 유일한 진실의 원천이다. Aggregate가Aggregate on (table)로 표시되는지 항상 확인. 그게 안 보이면 PG가 행을 끌어오고 있는 것이다.clickhouse_raw_query는 마지막 카드. 푸시다운이 닿지 않는 ClickHouse 전용 기능에만 쓰고,EXECUTE권한을 함부로 풀지 말 것.
PG → CH 마이그레이션의 진짜 비용은 데이터 이전이 아니라 쿼리 자산 이전이라는 인식에서 출발한 이 익스텐션은, 2026년 한국 SaaS의 흔한 패턴인 "PG OLTP + CH OLAP + 그 둘을 같은 BI에서 보기" 를 별도 어댑터 레이어 없이 가능하게 한다. 도커 한 줄로 시도해 보고, 자신의 워크로드에서 어느 쿼리가 푸시다운되고 어느 게 깨지는지를 EXPLAIN VERBOSE로 직접 보는 것 — 그게 도입의 정답에 가장 빠르게 다가가는 길이다.
참고 자료
- Introducing pg_clickhouse — David Wheeler (출시 글)
- pg_clickhouse is the fastest Postgres extension on ClickBench
- 공식 레퍼런스 문서
- Postgres managed by ClickHouse (beta)
- GitHub — ClickHouse/pg_clickhouse
- PGXN 배포 — pgxn.org/dist/pg_clickhouse
- 도커 이미지 — ghcr.io/clickhouse/pg_clickhouse
- 본 글의 모든 실측 인용은 ClickHouse 출시 블로그의 TPC-H SF1 데이터 (pg_clickhouse 0.1.0, PG 18.4, ClickHouse 25.x). 직접 검증은
pg_clickhouse 0.3+ ClickHouse 26.5.1.882 기준