PostgreSQL 안에 DuckDB를 심으면 어떤 일이 생기나 — pg_analytics와 pg_duckdb 해부
분석 쿼리 때문에 PostgreSQL을 떠나 Redshift나 BigQuery를 고민해본 적이 있다면, 이 글이 그 결정을 잠깐 늦춰줄지도 모른다. 서비스 초기엔 PostgreSQL 하나로 충분했다가, 월간 통계 쿼리 하나가 30초를 넘기는 순간부터 "드디어 데이터 웨어하우스 도입할 때가 됐나"라는 생각이 스멀스멀 올라온다. ETL 파이프라인 구축, 데이터 동기화 지연, 추가 인프라 비용… 솔직히 소규모 팀이 감당하기엔 무거운 선택이다.
pg_analytics는 "PostgreSQL 안에 DuckDB 분석 엔진을 통째로 박아 넣는다"는 아이디어에서 출발했다. DuckDB의 벡터화 실행 엔진을 PostgreSQL Executor Hook으로 가로채, OLAP성 쿼리를 별도 클러스터 없이 처리한다. 이 글에서는 그 동작 원리를 해부하고, 2026년 기준 실전에서 쓸 수 있는 후계자 pg_duckdb로 어떻게 이어졌는지, 그리고 어느 시점에서 이 접근이 한계에 부딪히는지를 같이 살펴본다.
PostgreSQL OLAP의 구조적 한계와 DuckDB-in-Postgres가 나온 배경
PostgreSQL은 왜 집계 쿼리에 느린가
PostgreSQL은 행(row) 기반 힙 테이블 구조와 Volcano 실행 모델을 쓴다. 쿼리 플래너가 실행 계획을 세우고, 연산자마다 한 번에 한 튜플씩 꺼내 올리는 방식이다. OLTP 포인트 쿼리에선 탁월하지만, 수백만 행을 GROUP BY해야 하는 집계 쿼리에선 CPU 캐시 활용률이 떨어지고 벡터 연산도 할 수 없다.
반면 DuckDB는 컬럼형 벡터화 실행(vectorized execution) 을 쓴다. 한 번에 최대 2,048개의 값을 컬럼 배치로 묶어 처리하기 때문에 CPU 캐시 히트율이 높고, 멀티스레드 병렬 처리와 Parquet 네이티브 읽기까지 지원한다. OLAP 집계·대규모 조인에서 표준 PostgreSQL 플래너를 앞서는 이유다.
pg_analytics가 선택한 두 가지 확장 포인트
ParadeDB가 개발한 pg_analytics는 PostgreSQL의 두 가지 공식 확장 포인트를 영리하게 결합했다.
첫 번째: FDW(Foreign Data Wrapper) API
S3, GCS 같은 오브젝트 스토리지와 Apache Iceberg, Delta Lake 같은 오픈 테이블 포맷을 PostgreSQL 테이블처럼 SELECT할 수 있게 노출한다. 외부 데이터를 읽어오는 창구 역할이다.
두 번째: Executor Hook API
이 부분이 핵심이다. Executor Hook은 PostgreSQL이 쿼리를 파싱·플래닝한 뒤 실행 단계에 진입할 때 외부 코드가 끼어들 수 있도록 열어둔 콜백 지점(ExecutorStart_hook, ExecutorRun_hook 등)이다. pg_analytics는 이 훅에 자신을 걸어 분석성 쿼리를 가로채 DuckDB 엔진으로 리다이렉트하고, DuckDB가 결과를 내면 PostgreSQL 레이어가 클라이언트에 전달한다. 클라이언트 입장에서는 그냥 평범한 PostgreSQL 쿼리를 날린 것처럼 보인다.
아래 시퀀스는 외부 테이블(S3 Parquet) 경로를 처리할 때의 흐름이다. 힙 테이블을 대상으로 하는 쿼리는 Executor Hook이 라우팅 판단만 하고 DuckDB가 PostgreSQL 버퍼를 읽어 처리하는 식으로 경로가 달라진다.
S3 Parquet 파일을 PostgreSQL로 쿼리하기
pg_analytics 설치 (개념적 흐름)
주의: pg_analytics 저장소는 2025년 3월 아카이브(read-only)됐다. 아래 DDL은 공식 저장소가 유지보수 중단된 시점의 기억을 되짚는 개념적 예시이며, 실제 FDW 이름·옵션 키는 당시 릴리스 노트와 소스 코드로 재확인해야 한다. 신규 프로젝트에는 pg_duckdb 사용을 권장한다.
-- pg_analytics 로드 (postgresql.conf의 shared_preload_libraries 설정 후 재시작 필요)
CREATE EXTENSION pg_analytics;shared_preload_libraries에 확장을 추가해야 하므로 PostgreSQL 서버 재시작이 필요하다. 클라우드 관리형 서비스라면 해당 확장이 허용 목록에 있는지 먼저 확인해야 한다.
S3의 Parquet 파일을 외부 테이블로 노출 (개념적 예시)
-- FDW 서버 등록 — 실제 FDW 이름은 사용 중인 pg_analytics 릴리스에서 확인 필요
CREATE SERVER s3_server
FOREIGN DATA WRAPPER <pg_analytics_fdw>
OPTIONS (region 'us-east-1');
-- S3 자격증명 매핑 (키 기반 접근 예시)
CREATE USER MAPPING FOR CURRENT_USER
SERVER s3_server
OPTIONS (
access_key_id '<AWS_ACCESS_KEY_ID>',
secret_access_key '<AWS_SECRET_ACCESS_KEY>'
);
-- IAM 역할 기반(IRSA, EC2 인스턴스 프로파일 등)으로 접근하는 환경이라면
-- USER MAPPING 없이 자격증명이 체인에서 자동 로드되도록 둘 수도 있다.
-- 외부 테이블 생성 — NYC 택시 데이터 예시
CREATE FOREIGN TABLE nyc_trips (
vendor_id TEXT,
tpep_pickup_datetime TIMESTAMP,
tpep_dropoff_datetime TIMESTAMP,
passenger_count INTEGER,
trip_distance REAL,
fare_amount REAL,
total_amount REAL
)
SERVER s3_server
OPTIONS (path 's3://my-bucket/nyc_trips/2024-01/*.parquet');이 외부 테이블은 PostgreSQL 힙에 데이터가 없다. SELECT를 날리는 순간 DuckDB가 S3에서 Parquet 파일을 직접 읽는다.
집계 쿼리 실행
-- 시간대별 평균 요금 집계
SELECT
date_trunc('hour', tpep_pickup_datetime) AS hour_bucket,
COUNT(*) AS trip_count,
ROUND(AVG(fare_amount)::numeric, 2) AS avg_fare
FROM nyc_trips
WHERE tpep_pickup_datetime >= '2024-01-01'
AND tpep_pickup_datetime < '2024-02-01'
GROUP BY 1
ORDER BY 1;클라이언트 입장에서는 그냥 평범한 PostgreSQL 쿼리다. 내부에서는 Executor Hook이 이 쿼리를 DuckDB로 보내고, DuckDB가 S3에서 필요한 컬럼만 읽어 벡터화 집계를 수행한다.
pg_duckdb로의 전환 — 현재 권장 방식
pg_duckdb는 세션 레벨에서 DuckDB 실행 엔진을 활성화한다. S3를 쓰려면 HTTP(S) 접근을 담당하는 httpfs 확장 로드와 자격증명 GUC 설정이 선행돼야 한다.
-- 세션에서 DuckDB 실행 엔진으로 라우팅
SET duckdb.force_execution = true;
-- S3 접근을 위한 선행 설정
SELECT duckdb.install_extension('httpfs');
SET duckdb.s3_region = 'us-east-1';
SET duckdb.s3_access_key_id = '<AWS_ACCESS_KEY_ID>';
SET duckdb.s3_secret_access_key = '<AWS_SECRET_ACCESS_KEY>';
-- 또는 세션에서 duckdb.create_secret()을 이용해 자격증명을 보관할 수도 있다.
-- S3의 Parquet 파일 직접 조회
SELECT vendor_id, COUNT(*), AVG(fare_amount)
FROM read_parquet('s3://my-bucket/nyc_trips/2024-01/*.parquet')
GROUP BY vendor_id;read_parquet() 함수를 SQL 안에서 바로 쓸 수 있다는 점이 pg_duckdb의 큰 편의 중 하나다. Apache Iceberg와 Delta Lake 지원도 확장 중이다.
트레이드오프 — 어디서 써야 하고 어디서 멈춰야 할까
장단점 한눈에 보기
| 항목 | 내용 |
|---|---|
| 별도 인프라 불필요 | Snowflake, BigQuery, Redshift 없이 기존 PostgreSQL에서 처리 |
| ETL 파이프라인 제거 | 데이터 복제·동기화 불필요, 운영 복잡도 감소 |
| SQL 호환 | 기존 PostgreSQL 클라이언트·쿼리 문법 그대로 유지 |
| 벡터화 실행 효과 | OLAP 집계·조인에서 표준 PostgreSQL 대비 유의미한 성능 차이 (단, 컬럼형 포맷일 때) |
| 오픈 포맷 지원 | Parquet, Iceberg, Delta Lake, S3 직접 쿼리 |
| MIT 라이선스 | pg_duckdb 기준, 상용 사용 제약 없음 |
| 한계 | 내용 |
|---|---|
| 단일 노드 제약 | 서버 한 대의 CPU·메모리에 묶임. 수평 확장이 필요한 규모에는 분산 DWH가 여전히 유리 |
| 힙 테이블 스캔은 단일 스레드 | PostgreSQL 행 기반 힙 테이블을 스캔할 때는 여전히 단일 스레드로 처리됨. Parquet 등 컬럼형 포맷일 때만 풀 성능이 나옴 |
| 포인트 쿼리에는 역효과 | 단순 포인트 쿼리나 인덱스로 최적화된 쿼리는 DuckDB 라우팅 시 오히려 느려질 수 있음 |
| SQL 방언 차이 | DuckDB가 지원하지 않는 PostgreSQL 타입(예: 정밀도 없는 numeric)이나 문법이 있어 일부 쿼리는 수정 필요 또는 표준 플래너로 폴백 |
| 트랜잭션 일관성 주의 | 체크포인트 이전 미반영 데이터를 DuckDB가 읽을 경우 불일치 가능성 존재 |
| shared_preload_libraries 필요 | 서버 재시작 필요. 클라우드 관리형 서비스에서는 지원 여부 확인 필요 |
쿼리 유형별 엔진 선택 흐름
실무에서 자주 하는 실수
numeric 타입 충돌: PostgreSQL에서 NUMERIC(정밀도 지정 없이)을 쓴 컬럼이 있으면 DuckDB로 넘어갈 때 오류나 폴백이 발생한다. 집계 결과에 ::numeric을 명시적으로 캐스팅하거나, 스키마 정의 단계에서 NUMERIC(18, 4) 식으로 정밀도를 지정해두면 된다.
힙 테이블에 기대를 너무 많이 걸기: pg_duckdb나 pg_analytics의 성능 향상은 Parquet 같은 컬럼형 포맷에서 극적으로 나타난다. 기존 PostgreSQL 힙 테이블을 DuckDB로 스캔하면 단일 스레드 제약 때문에 기대만큼 빠르지 않을 수 있다.
force_execution = true를 전역으로 켜두기: SET duckdb.force_execution = true를 연결 레벨 기본값으로 켜두면 인덱스 최적화가 가능한 OLTP 쿼리까지 DuckDB로 라우팅되어 오히려 느려진다. 분석 세션에서만 선택적으로 활성화하는 게 좋다.
pg_analytics 이후 — 생태계가 어떻게 진화했나
pg_analytics 저장소는 2025년 3월 아카이브됐다. ParadeDB는 그 이후 자사의 BM25 기반 전문 검색 확장인 pg_search에 개발 역량을 집중했고(분석 기능 자체가 검색 확장으로 흡수된 건 아니다), DuckDB-in-Postgres 패턴은 DuckDB, Inc.와 MotherDuck이 주도하는 pg_duckdb가 계승했다. 2026년 기준 pg_duckdb는 1.x 안정화 단계에 들어섰고, 참고 자료의 "Beta Release" 포스트는 그 이전 단계의 기록이다. Neon과 Microsoft(Azure Database for PostgreSQL)도 pg_duckdb 통합을 공개적으로 검토·전개해 왔다.
파생 프로젝트인 pg_ducklake도 눈에 띈다. 테이블 메타데이터는 PostgreSQL 힙에, 실제 데이터는 Parquet 포맷으로 유지하는 방식으로 쿼리 성능과 스토리지 효율을 동시에 추구한다.
2025년 PGConf.dev에서는 MotherDuck이 "PostgreSQL이 더 빠른 분석을 위해 DuckDB에서 배울 수 있는 것들"을 발표하는 등, 두 엔진의 아키텍처적 수렴이 커뮤니티의 주요 화두가 됐다. HTAP(Hybrid Transactional/Analytical Processing) — 동일 인스턴스에서 OLTP와 OLAP를 모두 처리하는 아키텍처 — 가 하나의 방향성으로 자리잡고 있는 셈이다.
마무리
이 글의 가장 중요한 포인트를 다시 한 번 짚자면, 성능 향상의 폭은 데이터가 어디에 어떤 포맷으로 있는가에 거의 전부 달려 있다. Parquet·Iceberg 기반 데이터 레이크 위에서는 DuckDB-in-Postgres가 분석 쿼리의 체감 속도를 바꿔놓지만, 기존 PostgreSQL 힙 테이블만을 상대로는 단일 스레드 스캔이라는 벽에 금방 부딪힌다. "PostgreSQL 하나로 OLAP까지" 라는 문구에 끌려 들어왔을 때 가장 자주 어긋나는 지점이다.
HTAP라는 방향성도 아직은 포지셔닝에 가깝다. 한 인스턴스가 OLTP의 지연 민감성과 OLAP의 리소스 집약성을 동시에 감당해야 하므로, 워크로드 격리(별도 복제본, 리소스 그룹, 전용 분석 세션)에 대한 운영 설계가 결국 필요하다. pg_duckdb가 그 격리의 "SQL 레이어"는 깔끔하게 풀어주지만, 그 아래 CPU·메모리·I/O는 여전히 운영자의 몫이다.
그래서 지금 시점의 현실적인 쓰임은 이렇게 정리된다. 데이터 레이크와 PostgreSQL을 하나의 인터페이스로 묶고 싶거나, 별도 DWH를 들이기엔 아직 작은 규모의 분석 워크로드를 소화해야 하는 팀에게는 충분히 매력적인 선택지다. 반대로 멀티테라바이트 규모 이상의 분산 분석, 또는 수십 명이 동시에 무거운 집계를 돌리는 공유 분석 환경이 필요하다면 전용 DWH와의 역할 분담이 여전히 합리적이다.
pg_analytics가 먼저 길을 열었고, pg_duckdb가 그 설계를 더 가볍고 명시적인 형태로 다듬고 있다. 신규 프로젝트라면 아카이브된 pg_analytics 대신 pg_duckdb부터 보면 된다.
참고 자료
- GitHub - paradedb/pg_analytics (아카이브)
- GitHub - duckdb/pg_duckdb
- MotherDuck 블로그: pg_duckdb — Splicing Duck and Elephant DNA
- MotherDuck 블로그: pg_duckdb Beta Release
- DuckDB 공식 블로그: Querying Postgres Tables Directly from DuckDB
- DEV Community: DuckDB analytics on your PostgreSQL tables in Azure PostgreSQL
- Tinybird 블로그: Outgrowing Postgres — How to run OLAP workloads on Postgres
- PGXN: pg_analytics 0.3.7