PostgreSQL이 분석 쿼리로 버벅일 때, DuckDB를 옆에 붙이면 달라지는 것들
운영 PostgreSQL에서 월별 매출 집계나 코호트 분석 같은 쿼리를 돌려본 적 있다면 그 답답함을 알 것이다. 쿼리가 돌아가는 동안 다른 API 응답이 느려지고, DBA는 pg_stat_activity를 들여다보며 "분석 쿼리가 shared_buffers를 다 잡아먹고 있어요"라고 알려준다. 그렇다고 Snowflake나 BigQuery를 당장 붙이기엔 비용도, 파이프라인 구축 공수도 부담스럽다.
DuckDB 사이드카 패턴은 이 딜레마를 꽤 현실적으로 해소해준다. PostgreSQL을 트랜잭션 백엔드로 그대로 두고, DuckDB를 분석 전용 엔진으로 옆에 붙이는 방식이다. 분석 쿼리 실행이 PostgreSQL 프로세스 밖에서 일어나기 때문에, 운영 DB의 버퍼 풀·WAL·커넥션 자원을 소모하지 않는다. 핵심은 부하 분리다.
이 글에서는 DuckDB의 postgres 확장(구 postgres_scanner)으로 PostgreSQL을 DuckDB에 직접 연결하는 방법, Parquet 파일로 내보내 쿼리하는 방법, 그리고 2026년 기준으로 실제로 쓸 수 있는 패턴들을 살펴본다. 벤치마크 숫자를 과장하지 않고, 단점도 솔직하게 짚어볼 예정이다.
왜 PostgreSQL 하나로 버티기가 어려운가
행 지향 vs. 컬럼 지향, 이 차이가 전부다
PostgreSQL은 행(row) 단위로 데이터를 저장한다. 트랜잭션에서는 이게 완벽하다. 특정 주문 하나를 빠르게 읽거나 업데이트할 때 필요한 모든 컬럼이 디스크의 같은 자리에 있으니까. 반면 "지난 12개월 동안 특정 카테고리 상품의 월별 구매 합계"를 구하려면 수백만 행을 스캔하면서 딱 2~3개 컬럼만 사용한다. 나머지 컬럼 데이터는 읽어놓고 버리는 셈이다.
DuckDB는 컬럼 지향 스토리지와 벡터화(vectorized) 실행 엔진으로 이런 쿼리를 처리한다. 필요한 컬럼만 읽고, CPU SIMD 명령어로 배치 처리한다. 이게 OLAP 성능 차이의 근본 원인이다.
사이드카 패턴에서 독자가 정확히 알아야 하는 건 "집계 연산이 실제로 어느 프로세스에서 돌아가느냐"다. 아래 다이어그램은 그 경계를 보여준다.
사이드카 패턴이 지금 주목받는 이유
DuckDB 자체는 몇 년 전부터 있었다. 최근 들어 이른바 Budget HTAP(Hybrid Transactional/Analytical Processing, 트랜잭션과 분석을 한 스택으로 처리하는 아키텍처)의 저비용 대안으로 얘기가 많아진 건 생태계가 갖춰졌기 때문이다. MotherDuck이 2025년 5월 pg_duckdb v1.0을 정식 릴리스하면서 PostgreSQL 프로세스 안에 DuckDB 엔진을 내장하는 역방향 통합도 가능해졌고(MotherDuck 릴리스 노트), DuckDB 본체도 Iceberg·Delta 등 오픈 테이블 포맷 확장을 꾸준히 넓혀 왔다. 별도 데이터 웨어하우스 없이 PostgreSQL + DuckDB 조합으로 OLAP 성능을 확보하는 패턴이 확산되고 있다.
postgres 확장 — DuckDB에서 PostgreSQL 직접 쿼리하기
연결부터 쿼리까지
DuckDB의 공식 postgres 확장(2026년 기준 공식 명칭, 구 postgres_scanner)을 설치하면 ATTACH 명령으로 실행 중인 PostgreSQL 인스턴스에 연결할 수 있다.
-- DuckDB 셸 또는 애플리케이션 내에서
INSTALL postgres;
LOAD postgres;
-- PostgreSQL 인스턴스 연결
ATTACH 'host=localhost port=5432 dbname=mydb user=analyst password=secret'
AS pg (TYPE postgres);
-- PostgreSQL 테이블을 DuckDB에서 집계
SELECT
date_trunc('month', created_at) AS month,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM pg.public.orders
GROUP BY 1
ORDER BY 1;이렇게 하면 집계 연산은 DuckDB가 처리하고, PostgreSQL은 데이터 전송만 담당한다. PostgreSQL의 바이너리 프로토콜을 활용해 텍스트 변환 오버헤드가 없고, 필요한 컬럼과 조건에 맞는 행만 요청하는 프레디케이트 푸시다운(predicate pushdown)을 지원한다.
처음에는 "어차피 데이터를 PostgreSQL에서 다 읽어오면 의미가 있나?" 싶을 수 있다. 그러나 실제 부담을 주는 건 데이터 전송이 아니라 집계 연산 자체다. shared_buffers를 흔들어 놓는 정렬·해시 집계·조인이 운영 DB 밖으로 빠져나가는 게 이 패턴의 요지다.
pg_duckdb — 반대 방향의 통합
MotherDuck이 2025년에 릴리스한 pg_duckdb는 방향이 반대다. PostgreSQL 프로세스 안에 DuckDB 엔진을 내장해서, 기존 PostgreSQL SQL 안에서 DuckDB 엔진이 동작한다.
-- PostgreSQL에서 pg_duckdb 설치 후
CREATE EXTENSION pg_duckdb;
-- S3 Parquet 파일을 PostgreSQL SQL로 쿼리 (pg_duckdb가 처리)
SELECT * FROM read_parquet('s3://my-bucket/events/*.parquet')
WHERE event_date >= '2026-01-01';
-- PostgreSQL 테이블 + S3 Parquet 파일 조인
SELECT c.customer_id, c.name, COUNT(e.event_id) AS event_count
FROM customers c
JOIN read_parquet('s3://my-bucket/events/*.parquet') e
ON c.customer_id = e.customer_id
GROUP BY c.customer_id, c.name;이 패턴의 장점은 애플리케이션 코드 변경이 최소화된다는 것이다. 기존 PostgreSQL 연결 그대로, SQL만 추가하면 된다.
세 가지 시나리오
어떤 방법을 선택할지는 데이터 신선도 요구사항과 운영 복잡도 감수 의지에 따라 갈린다.
CDC(Change Data Capture) 기반 준실시간 파이프라인도 유효한 선택지지만 별도 스트리밍 스택이 필요해 이 글의 범위 밖으로 남겨 두었다. 필요하면 마지막에 간단히 언급한다.
시나리오 1 — 야간 배치 Parquet 내보내기
가장 단순하고 운영 DB 부하가 전혀 없는 패턴이다. 매일 밤 트래픽이 낮은 시간에 PostgreSQL 테이블을 Parquet으로 내보내고, 분석 쿼리는 모두 DuckDB가 파일을 직접 읽어서 처리한다.
주의할 점: PostgreSQL의 기본 COPY는 Parquet 포맷을 지원하지 않는다. Parquet으로 바로 내보내려면 pg_duckdb가 설치되어 있거나, 별도의 도구(예: duckdb CLI에서 postgres 확장을 통해 SELECT 후 COPY ... TO ... (FORMAT parquet))를 써야 한다.
-- pg_duckdb로 Parquet 내보내기 (pg_duckdb 확장이 설치되어 있어야 함)
COPY (SELECT * FROM orders WHERE created_at >= CURRENT_DATE - INTERVAL '90 days')
TO '/data/exports/orders_recent.parquet' (FORMAT parquet);pg_duckdb가 없다면 vanilla PostgreSQL의 COPY ... TO ... (FORMAT csv)로 CSV/TSV를 뽑고, DuckDB 쪽에서 COPY (SELECT * FROM read_csv_auto('...')) TO '...' (FORMAT parquet);로 변환하는 2단계 방식이 무난하다.
# Python에서 DuckDB로 내보낸 파일 쿼리
import duckdb
con = duckdb.connect()
result = con.execute("""
SELECT
date_trunc('month', created_at) AS month,
product_category,
COUNT(*) AS order_count,
SUM(amount) AS revenue
FROM read_parquet('/data/exports/orders_recent.parquet')
GROUP BY 1, 2
ORDER BY 1, revenue DESC
""").fetchdf()T+1 데이터로 충분한 보고서, 대시보드라면 이게 제일 낫다. 구현 복잡도가 낮고 PostgreSQL은 배치 내보내기 시간 외엔 아무 영향도 받지 않는다.
언제 피해야 하는가. 실시간 대시보드, 사기 탐지, 운영 알림처럼 "오늘 오전 데이터"가 필요한 워크로드. 배치 지연이 SLA에 걸리면 신뢰성 이슈로 번진다.
시나리오 2 — postgres 확장으로 실시간 집계
T+1로는 부족하고, CDC 파이프라인을 구축하기엔 아직 이른 상황이라면 ATTACH 방식이 현실적인 선택이다.
import duckdb
# DuckDB 연결 및 postgres 확장 로드 (구문 하나씩 분리 호출)
con = duckdb.connect()
con.execute("INSTALL postgres")
con.execute("LOAD postgres")
# PostgreSQL 연결
con.execute("""
ATTACH 'host=pg-analytics-replica.internal port=5432
dbname=mydb user=analyst password=secret'
AS pg (TYPE postgres, READ_ONLY)
""")
# 실시간 집계 쿼리 — 집계는 DuckDB가 처리
df = con.execute("""
SELECT
date_trunc('week', o.created_at) AS week,
p.category,
COUNT(DISTINCT o.user_id) AS unique_buyers,
SUM(o.amount) AS revenue
FROM pg.public.orders o
JOIN pg.public.products p ON o.product_id = p.id
WHERE o.created_at >= NOW() - INTERVAL '3 months'
GROUP BY 1, 2
ORDER BY 1 DESC, revenue DESC
""").fetchdf()대형 테이블에서 이 방식을 쓸 때 네트워크가 병목이 될 수 있다. 특히 WHERE 조건 없이 수억 행을 전송하는 패턴은 오히려 운영 DB에 부담을 준다. 프레디케이트 푸시다운이 적용되도록 WHERE 절을 명확하게 작성하는 것이 중요하다.
언제 피해야 하는가. 필터 조건이 약한 애드혹 탐색 쿼리를 다수 분석가가 동시에 primary 대상으로 던지는 상황. 이 경우엔 시나리오 3로 넘어가는 게 낫다.
시나리오 3 — read replica + pg_duckdb
운영 primary를 완전히 보호하고 싶다면 read replica에 pg_duckdb를 설치하는 패턴이 현재 무난한 선택이다. MotherDuck의 pg_duckdb v1.0 릴리스 발표에서는 TPC-DS 벤치마크 일부 쿼리가 PostgreSQL 실행 시 90초에서 pg_duckdb로 137밀리초까지 단축된 사례가 소개되어 있다(단순 산술로는 약 650배 차이). 워크로드마다 폭이 크니 자기 데이터로 재측정해야 한다.
-- read replica PostgreSQL에서 pg_duckdb 설치
CREATE EXTENSION pg_duckdb;
-- 이후 일반 PostgreSQL SQL처럼 쓰면 DuckDB 엔진이 처리
-- 복잡한 집계 쿼리도 PostgreSQL 문법 그대로
SELECT
region,
product_line,
SUM(quantity * unit_price) AS total_revenue,
AVG(quantity * unit_price) AS avg_order_value,
COUNT(DISTINCT customer_id) AS unique_customers
FROM orders
WHERE order_date BETWEEN '2025-01-01' AND '2025-12-31'
GROUP BY region, product_line
HAVING SUM(quantity * unit_price) > 100000
ORDER BY total_revenue DESC;primary에는 아무 변경도 없고, replica는 PostgreSQL 스트리밍 복제를 통해 동기화된다. 분석 팀은 기존 PostgreSQL 연결 문자열만 replica로 바꾸면 된다.
언제 피해야 하는가. 복제 지연을 견디지 못하는 워크로드(예: 방금 트랜잭션 커밋한 사용자에게 바로 대시보드로 확인시켜야 하는 케이스). 또한 관리형 PostgreSQL 서비스에서 pg_duckdb 확장을 아직 지원하지 않는다면 자체 호스팅 replica가 필요하다.
트레이드오프 — 솔직한 평가
| 항목 | 내용 | 심각도 |
|---|---|---|
| 단일 라이터 제약 | DuckDB는 한 프로세스만 쓰기 가능. 다수 쓰기 세션이 필요한 워크로드에는 부적합 | 분석 전용이면 대부분 무관 |
| 접근 제어 부재 | GRANT, Row-Level Security, 역할 계층 없음 | 멀티테넌트 환경에서 주의 필요 |
| 네트워크 병목 | postgres 확장으로 대형 테이블 전송 시 발생 가능 | WHERE 절 최적화로 완화 가능 |
| 데이터 신선도 | Parquet 배치 방식은 T+1 데이터 | 요구사항에 따라 선택 |
| CDC 복잡성 | 근실시간 동기화를 위해 Debezium + Kafka 같은 스트리밍 스택 필요 | 팀 역량과 트레이드오프 |
| 동시 분석 사용자 | 다수 사용자가 동시에 무거운 쿼리를 던지면 단일 DuckDB 프로세스가 병목 | MotherDuck 같은 클라우드 옵션으로 해소 가능 |
DuckDB는 자체 ACID 트랜잭션을 지원하므로 "MVCC 자체가 없다"는 서술은 부정확하다. 정확히는 다중 라이터 동시성 모델이 없다는 뜻이고, 이는 위 표의 첫 항목과 사실상 동일한 이야기다.
흔히 저지르는 실수
WHERE 절 없이 postgres 확장 쓰기. 수천만 행짜리 테이블을 필터 없이 전송하면, 집계는 DuckDB가 빠르게 하더라도 PostgreSQL에서 네트워크로 데이터를 쏟아내는 동안 운영 DB 자원을 잡아먹는다. 프레디케이트 푸시다운이 동작하도록 쿼리를 작성해야 한다.
분석 사용자에게 DuckDB 직접 접근 허용하기. DuckDB에는 PostgreSQL 같은 세밀한 접근 제어가 없다. GRANT나 RLS가 필요한 환경이라면 애플리케이션 레이어에서 접근 제어를 별도로 구현해야 한다.
배치 내보내기 주기를 잘못 설정하기. 분석 팀이 "오늘 데이터"를 기대하는데 어제 배치 파일만 보고 있다면 신뢰 문제가 생긴다. 파일 기반 방식은 데이터 신선도 SLA를 팀 내에서 명확히 합의한 후 도입하면 좋다.
실무자에게 권하고 싶은 시작점
의사결정 프레임은 앞서 정리했으니 여기서는 실제로 어떤 순서로 밟는 게 덜 아픈지 이야기해 본다.
첫 걸음은 시나리오 1(야간 Parquet 배치)로 시작하기. 운영 DB에 새 확장을 설치할 필요가 없고, DuckDB 파일 하나로 팀 내부 리허설을 돌려볼 수 있다. 대부분의 대시보드·주간 리포트가 이 단계에서 이미 해소된다.
신선도 요구가 올라오면 시나리오 3(read replica + pg_duckdb)로 이전. primary를 건드리지 않으면서 SQL 방언도 유지되기 때문에 팀 학습 비용이 가장 낮다. 자체 호스팅 replica 하나를 운영할 여력이 있는 팀이라면 여기가 도착점이 될 가능성이 크다.
시나리오 2(postgres 확장 직접 연결)는 개발·스테이징 환경이나 애드혹 분석 도구용으로 남겨두기. 프로덕션 primary에 직접 붙이는 건 트래픽·쿼리 패턴을 파악한 뒤에나 고려하는 게 안전하다.
초 단위 신선도가 필요해지는 순간이 오면 그때 비로소 CDC(Debezium, pg_duckpipe 등)를 논의한다. Kafka 스택을 운영할 인력·모니터링 체계가 있을 때 얻는 이득이 크지, 무리해서 도입하면 사이드카의 원래 장점인 "가벼움"을 잃어버린다.
한 문장으로 요약하면, 팀이 유지관리할 수 있는 가장 단순한 조합에서 출발해 신선도 요구가 올라올 때만 한 단계씩 위로 올라가라는 것이다.
참고 자료
본문 인용 출처
- pg_duckdb v1.0 릴리스 및 TPC-DS 벤치마크 수치 — Announcing Pg_duckdb Version 1.0 (MotherDuck)
- postgres 확장(구 postgres_scanner) 원리 소개 — Querying Postgres Tables Directly from DuckDB (DuckDB 블로그)
공식 문서
추가로 읽어볼 만한 자료(커뮤니티 2차 출처)
- PostgreSQL + DuckDB Integration: 3 Methods to Boost Analytical Performance – MotherDuck
- Postgres Powered by DuckDB: The Modern Data Stack in a Box – Crunchy Data
- Postgres to MotherDuck: Stream Real-Time Analytics with CDC – Estuary
- pg_duckpipe: Real-time CDC for streaming Postgres Table into Columnar Ducklake – DEV Community
- Turbocharging Postgres Analytics with DuckDB – Medium