본문 바로가기
  • plotly로 바로쓰는 동적시각화 in R & 파이썬
EPL과 유튜브 데이터로 배우는 DuckDB

DuckDB 속도 비밀: 2026년을 위한 10가지 팁

by 아참형인간 2026. 5. 31.

https://aladin.kr/p/MSt8I

 

LUVIT♥ EPL과 유튜브 데이터로 배우는 DuckDB | 이기준

DuckDB를 활용한 SQL 기반 데이터 분석 입문서다. SQL 기초부터 고급 활용, 파이썬 연동, 데이터 시각화와 대시보드 제작까지 단계적으로 학습할 수 있다. EPL 데이터, 유튜브 트렌드, 「케이팝 데몬

www.aladin.co.kr

 

DuckDB를 날아오르게 만드는 실전 검증 튜닝 — 특수한 하드웨어 없이 쿼리 시간을 10–100배 단축합니다.

 

솔직히 말해 봅시다. DuckDB를 도입하는 이유는 클러스터를 관리하기 위해서가 아닙니다. 빠르게 답을 얻기 위해서입니다.

좋은 소식은 몇 가지 신중한 조정만으로도 — 그중 상당수는 한 줄짜리 설정에 불과하지만 — 이 작은 강자로부터 놀라운 성능을 끌어낼 수 있다는 것입니다.

다음은 2026년 최고의 데이터 엔지니어들이 실제로 사용하며 강력히 추천하는 실전 플레이북입니다.

1) 무엇보다 먼저 컬럼형 I/O를 활용하라: Parquet + Pushdown

필요한 컬럼만 읽는 것은 성능 향상을 위한 가장 큰 한 방입니다. 대규모 팩트 테이블은 Parquet 형식으로 저장하고, projection pushdown과 predicate pushdown을 적극 활용하세요.

-- 필요한 컬럼만, 필요한 위치에서만 읽기:
SELECT user_id, event_ts
FROM read_parquet('s3://prod/logs/dt=2026-09-*/*.parquet')
WHERE event_ts >= TIMESTAMP '2026-09-01'
  AND country = 'IN';

왜 빠를까요? DuckDB는 참조된 컬럼만 스캔하고, Parquet의 min/max 통계를 활용해 전체 row group을 건너뜁니다. I/O는 줄어들고, CPU 사용량은 감소하며, 대시보드는 더 쾌적해집니다.

빠른 팁

데이터를 반드시 테이블로 적재해야 하더라도, Parquet에서 먼저 스테이징하는 방식을 유지하세요. 그리고 크기가 큰 blob 데이터는 핵심 처리 경로(hot path) 밖에 두는 것이 좋습니다.

2) 정밀한 pruning을 위해 partition하고 sort하라

자주 사용하는 고가치 필터(예: dt, region)를 기준으로 디렉터리를 partition하고, 각 partition 안에서는 가장 자주 사용되는 predicate key(user_id, device_id)를 기준으로 정렬하세요. Export할 때는 row group 크기와 압축 방식도 제어하세요.

COPY (
  SELECT *
  FROM big_events
  WHERE dt BETWEEN '2026-09-01' AND '2026-09-30'
  ORDER BY user_id, event_ts
) TO 's3://prod/warehouse/big_events/dt={dt}/'
  (FORMAT PARQUET, PARTITION_BY (dt), ROW_GROUP_SIZE=128000000, COMPRESSION ZSTD);

왜 빠를까요? Partition pruning은 전체 디렉터리를 제외합니다. 정렬된 row group은 min/max 범위를 더 촘촘하게 만들어 pushdown 효과를 높입니다. 이는 가난한 사람의 Z-order이며, 실제로 효과가 있습니다.

3) 더 똑똑한 실행 계획을 위해 hot table에 ANALYZE를 실행하라

통계는 join 순서, filter selectivity, index 사용을 안내합니다. 대규모 ingest나 repartition 이후에는 통계를 갱신하세요.

ANALYZE big_events COLUMNS(user_id, event_ts, country, campaign_id);

“이런, 전부 스캔했네” 하는 순간은 줄어들고, 적절한 크기의 hash table을 더 자주 보게 될 것입니다.

4) join 전에 미리 aggregate하고 de-duplicate하라

일별 count만 필요하다면 raw clickstream(수십억 행)을 작은 dimension 테이블과 join하지 마세요. 먼저 줄인 다음 join하세요.

WITH daily AS (
  SELECT user_id, dt, count(*) AS clicks
  FROM read_parquet('s3://prod/logs/dt=2026-09-*/*.parquet')
  GROUP BY user_id, dt
)
SELECT d.user_id, u.tier, sum(d.clicks) AS mtd_clicks
FROM daily d
JOIN users u ON u.user_id = d.user_id
GROUP BY d.user_id, u.tier;

이 방식은 몇 분 걸리던 join을 눈 깜짝할 사이의 aggregation으로 바꾸는 경우가 많습니다.

5) 파이프라인을 나누기 위해 temporary table이나 materialized view를 사용하라

깊게 쌓인 CTE는 우아해 보이지만, 작업을 반복할 수 있습니다. 여러 단계나 세션에서 재사용되는 중간 결과는 materialize하세요.

CREATE TEMP TABLE hot_users AS
SELECT user_id
FROM big_events
WHERE dt >= current_date - 7
GROUP BY user_id
HAVING count(*) > 500;

-- 자유롭게 재사용:
SELECT *
FROM purchases p
JOIN hot_users h USING (user_id);

약간의 디스크 사용량을 감수하는 대신, 실행 시간의 안정성과 설명 가능성을 크게 얻을 수 있습니다.

6) 병렬성과 메모리를 제어하라, 추측하지 마라

DuckDB는 병렬 scan에 뛰어납니다. 노트북 팬이 반란을 일으키기 전까지는요. 하드웨어와 workload에 맞는 적절한 기본값을 설정하세요.

PRAGMA threads=8;          -- 8C/16T 머신에서는 여기서 시작하고, 위아래로 튜닝
PRAGMA memory_limit='8GB'; -- OS를 쾌적하게 유지하고 swap storm 방지

“스레드가 많을수록 더 빠르다”가 항상 맞는 것은 아닙니다. 작고 cache-hot한 쿼리에서는 스레드를 줄이면 overhead가 감소합니다. 양쪽 모두 측정하세요.

7) 원격 lake에는 object caching을 켜라

차가운 S3 read는 반복 작업 속도를 떨어뜨립니다. DuckDB의 object cache는 특히 notebook에서 중복 네트워크 왕복을 줄여줍니다.

PRAGMA enable_object_cache=true;
-- 이제 동일한 Parquet 조각을 반복해서 읽으면 local cache를 사용합니다.

Partition pruning과 함께 사용하면 반칙처럼 느껴질 수 있습니다.

8) 집요하게 profile하라: EXPLAIN ANALYZE + JSON profile

Profile하지 않는다면 추측하는 것입니다. DuckDB의 내장 profiler는 시간이 어디에 쓰이는지 정확히 보여줍니다.

PRAGMA enable_profiling='json';
PRAGMA profiling_output='prof.json';

EXPLAIN ANALYZE
SELECT u.country, count(*) 
FROM users u 
JOIN big_events e ON e.user_id = u.user_id 
WHERE e.dt >= '2026-09-01'
GROUP BY u.country;

prof.json을 선호하는 viewer에 불러오고, 거대한 hash table, pruning되지 않은 scan, row-by-row UDF 같은 위험 신호를 찾으세요. 첫 번째 위험 신호를 고치고, 다시 profile하고, 반복하세요. 이것은 게임입니다.

9) Point lookup과 selective filter에는 index를 사용하라. 단, 신중하게

DuckDB의 가벼운 secondary index는 selective predicate나 point lookup을 많이 수행할 때 빛을 발합니다. 예를 들어 “큰 테이블에서 소수의 user를 찾는” 경우입니다.

CREATE INDEX idx_events_user ON big_events(user_id);
-- 나중에:
SELECT * FROM big_events WHERE user_id = 123456 LIMIT 100;

주의할 점: workload가 대부분 full scan과 대규모 aggregation 중심이라면 index는 도움이 되지 않으며 write를 느리게 만들 수 있습니다. Index는 물처럼 마시지 말고 에스프레소 샷처럼 사용하세요.

10) DuckDB의 vectorized engine으로 작업을 밀어 넣고 row loop를 피하라

Python/R에서는 row를 반복 처리하고 싶은 충동을 참으세요. DataFrame이나 Arrow table을 등록하고 DuckDB가 무거운 작업을 맡게 하세요.

import duckdb, pandas as pd

con = duckdb.connect()
df = pd.read_parquet('/mnt/data/clicks_2026_09.parquet')

con.register('clicks', df)
res = con.execute("""
  SELECT user_id,
         count(*) AS n,
         approx_quantile(latency_ms, 0.95) AS p95
  FROM clicks
  WHERE country = 'IN' AND dt >= '2026-09-01'
  GROUP BY user_id
""").df()

Vectorized operator는 batch를 아침 식사처럼 먹어 치웁니다. 노트북 배터리가 고마워할 것입니다.

간단한 사고 모델 (ASCII 버전)

[S3/HDFS/로컬 Parquet]
        │   (dt/region 기준 partition; user_id 기준 정렬)
        ▼
   [DuckDB Scan]
    ├─ Projection pushdown: 필요한 컬럼만 선택
    ├─ Predicate pushdown: row group/디렉터리 pruning
    ├─ 병렬 스캔 + object cache
    ▼
 [Hash/Merge Join]  ← 통계 정보와 사전 집계가 join 순서와 크기를 결정
    ▼
 [Aggregation]
    ▼
 [Materialized Temp Table / View]
    ▼
 [BI / Notebook / COPY TO Parquet]

이 그림을 키보드 옆에 붙여두세요. 성능 향상의 80%는 여기서 나옵니다.

미니 사례 연구: “비용은 3배 절감, 속도는 40배 향상”

한 구독형 스타트업은 매월 약 2.1TB의 클릭스트림 데이터를 처리하고 있었습니다. 이들은 원시 CSV를 Parquet으로 전환하고, dt와 region 기준으로 partition했으며, 각 partition 내부를 user_id, event_ts 기준으로 정렬했습니다. 또한 ROW_GROUP_SIZE=128MB와 ZSTD 압축을 적용했습니다. ANALYZE를 실행한 후에는 users 테이블과 join하기 전에 세션 데이터를 먼저 집계했습니다. 캐시 경합(cache thrash)을 방지하기 위해 PRAGMA threads=12로 제한했고, S3 데이터 레이크에는 object cache를 활성화했습니다.

결과는 어땠을까요? 주간 코호트 분석 작업은 37분에서 약 55초로 단축되었습니다. 그것도 단일 개발자 워크스테이션에서 말입니다. 클라우드 비용은 감소했습니다. 왜냐고요? 애초에 클라우드를 사용하지 않았기 때문입니다.

흔히 겪는 함정들 (그리고 피하는 방법)

숨어 있는 row-by-row 작업을 조심하라

반복문 안에서 문자열을 파싱하고 있나요? 가능하면 regexp_extract나 json_extract 같은 집합 기반(set-based) 함수로 옮기세요. 정말로 Python UDF가 필요하다면 배치 단위로 처리하세요.

과도한 partitioning을 피하라

dt=yyyy/mm/dd/hh 형태는 깔끔해 보이지만 수천 개의 작은 파일을 만들어낼 수 있습니다. 시간 단위 필터가 실제로 자주 사용된다는 근거가 없다면 일(day) 단위 partition을 선호하세요.

구조가 바뀌면 통계를 갱신하라

새로운 달이 시작되면 데이터 분포도 달라집니다. ANALYZE는 “누군가 기억났을 때” 실행하는 작업이 아니라, 데이터 적재 파이프라인의 일부가 되어야 합니다.

결론

DuckDB의 초능력은 마법이 아닙니다. 그것은 컬럼형 파일, 똑똑한 pruning, 벡터화 실행, 그리고 적절한 힌트를 주었을 때 이를 활용하는 쿼리 플래너가 만들어내는 기계적 공감(mechanical sympathy)입니다.

이 열 가지 기법을 적용하면 더 이상 “쿼리를 실행한다”라고 생각하지 않게 됩니다. 대신 “답을 얻는다”라고 생각하게 될 것입니다.

이번 주에 이 글 덕분에 한 시간을 절약했다면, 여러분만의 작은 최적화 팁도 댓글로 공유해 주세요. 더 많은 실전 경험담을 원한다면 팔로우하시고, 쿼리 실행 계획을 함께 분석해 보고 싶다면 언제든 요청해 주세요. 기꺼이 도와드리겠습니다.

 

<출처: https://medium.com/@hadiyolworld007/duckdb-speed-secrets-10-tricks-for-2026-29c990a8701d>

 

댓글