최근 몇 년 동안 데이터 처리를 위해 DuckDB를 점점 더 자주 사용해 왔고, 이제는 거의 전적으로(대개 Python 안에서) DuckDB를 사용한다. 이 글에서는 다른 SQL 기반 도구들과 DuckDB를 차별화하는 내가 좋아하는 기능들을 정리한다.
URL: https://www.robinlinacre.com/recommend_duckdb/
지난 몇 년 동안 나는 데이터 처리를 위해 DuckDB를 점점 더 자주 사용하게 되었고, 이제는 거의 전적으로 DuckDB만 사용한다. 대개는 Python 안에서 사용한다.
우리는 대부분의 표 형태(tabular) 데이터를 하나의 큰 머신에서 처리할 수 있는 더 단순한 세계로 이동하고 있으며1, 가장 거대한 데이터셋을 제외하면 클러스터의 시대는 저물고 있다.2
이 글에서는 DuckDB가 다른 SQL 기반 도구와 비교해 두드러지는, 내가 특히 좋아하는 기능들을 소개한다. 한마디로 요약하면, 설치가 간단하고, 사용하기 편하며(인체공학적이고), 빠르고, 기능도 더 충실하다.
이전 글에서는 Polars, pandas 또는 dplyr 같은 다른 API보다 SQL을 더 선호하는 이유를 설명했다.
DuckDB는 분석(analytics) 쿼리에 최적화된 오픈 소스 인-프로세스(in-process) SQL 엔진이다.
분석에 최적화된 엔진(OLAP)과 트랜잭션에 최적화된 엔진(OLTP)의 성능 차이는 과소평가하면 안 된다. DuckDB에서 실행되는 쿼리는, 정확히 동일한 쿼리를 (예를 들어) SQLite나 Postgres에서 실행할 때보다 100배, 심지어 1,000배까지도 빠를 수 있다.
DuckDB의 핵심 사용 사례는, 디스크에 csv, parquet, json 같은 형식으로 저장된 하나 이상의 큰 데이터셋을 배치 처리(batch process)하고 싶을 때다. 정제(cleaning), 조인, 집계, 새 컬럼 도출 같은 작업을 수행하려는 상황을 말한다.
하지만 DuckDB는 명령줄에서 CSV 파일을 확인하는 것 같은 훨씬 단순한 작업에도 사용할 수 있다.
DuckDB는 데이터 처리 엔진 중 가장 빠른 축에 속하는 것으로 꾸준히 벤치마크된다. 내가 본 벤치마크들3에 따르면 현재 선두 오픈 소스 엔진들—지금으로서는 polars, DuckDB, DataFusion, Spark, Dask—사이의 격차는 크지 않다. Spark와 Dask는 큰 데이터에서는 경쟁력이 있지만, 작은 데이터에서는 느린 편이다.
DuckDB 자체는 단 하나의 사전 컴파일된 바이너리다. Python에서는 의존성 없이 pip install로 설치할 수 있다. Spark 같은 더 무거운 대안들에 비해 설치가 정말 즐겁다. uv와 결합하면, 아무것도 없는 상태에서 1초도 안 돼 새 DuckDB Python 환경을 세팅할 수 있다 — 여기를 참고.
DuckDB는 빠르고 시작 시간이 거의 0에 가깝기 때문에, 데이터 엔지니어링 파이프라인의 CI와 테스트에 이상적이다.
과거에는 이 작업이 성가셨고, 예를 들어 Apache Spark에서 큰 테스트 스위트를 실행하는 일은 시간이 많이 들고 답답했다. 이제는 테스트 환경을 훨씬 쉽게 구축할 수 있고, 테스트 환경과 프로덕션 파이프라인 사이의 차이로 인한 문제 여지도 줄어든다.
이 단순함과 속도는 새로운 SQL을 작성하고, 큰 데이터셋에서 돌리기 전에 문법을 맞춰보는 과정에도 그대로 적용된다. 나는 역사적으로 Spark 같은 엔진(로컬 모드로 Spark를 시작하는 데도 몇 초가 걸린다)에서는 이게 성가셨고, AWS Athena 같은 독점 도구에서 쿼리를 실행해야 할 때는 더 심했다.4
자동완성을 지원하는 DuckDB UI도 있다 — 여기.
DuckDB 팀은 SQL 방언(dialect)에 광범위한 혁신을 구현해 두었고, 이 덕분에 사용이 매우 즐겁다. 다음 블로그 글을 참고하라: 123456.
내가 특히 좋아하는 것들은 EXCLUDE 키워드와, 일부 컬럼을 선택한 뒤 정규식으로 치환(regex-replace)할 수 있게 해주는 COLUMNS 키워드다.5 또한 QUALIFY와 윈도 함수(window functions)의 집계 수정자(aggregate modifiers)도 마음에 든다. 여기를 보라.
또 다른 기능은 first_name.lower().trim()처럼 함수 체이닝(function chaining)이 가능하다는 점이다.
DuckDB는 s3나 웹 상의 파일을 포함해, 파일에서 직접 데이터를 쿼리할 수 있다.
예를 들어 parquet 파일들이 있는 폴더를 쿼리하려면:
sqlselect * from read_parquet('path/to/*.parquet')
또는 (CORS가 활성화된 파일이라면) 웹에서 직접 SQL을 실행할 수도 있다:
sqlselect * from read_parquet('https://raw.githubusercontent.com/plotly/datasets/master/2015_flights.parquet') limit 2;
DuckDB 웹 셸에서 이 쿼리를 직접 실행해 보려면 여기를 클릭하라.
데이터 파이프라인에서 문제가 생기는 가장 쉬운 방법 중 하나는, CSV 같은 비정형(타입이 명시되지 않은) 포맷에서 들어오는 데이터 타입을 엄격하게 다루지 않는 것이다. DuckDB는 이를 위한 다양한 옵션을 제공한다 — 여기를 보라.
많은 데이터 파이프라인은 사실상 긴 CTE 시퀀스로 요약된다:
sqlWITH input_data AS ( SELECT * FROM read_parquet('...') ), step_1 AS ( SELECT ... FROM input_data JOIN ... ), step_2 AS ( SELECT ... FROM step_1 ) SELECT ... FROM step_2;
이런 파이프라인을 개발할 때는 각 단계에서 무슨 일이 일어났는지 확인하고 싶을 때가 많다.
Python에서는 다음처럼 쓸 수 있다.
pythoninput_data = duckdb.sql("SELECT * FROM read_parquet('...')") step_1 = duckdb.sql("SELECT ... FROM input_data JOIN ...") step_2 = duckdb.sql("SELECT ... FROM step_1") final = duckdb.sql("SELECT ... FROM step_2;")
이렇게 하면 step_2에서 데이터가 어떻게 생겼는지 성능 손실 없이 쉽게 살펴볼 수 있다. 이 단계들은 모두 한 번에 실행될 때까지 지연 평가(lazily)되기 때문이다.
또한 각 단계를 독립적으로 테스트 가능한 함수로 만들 수 있어, CI에서 SQL을 더 쉽게 테스트할 수 있다.
DuckDB는 대량 데이터 작업에 대해 완전한 ACID 준수를 제공하며, 이는 다른 분석 데이터 시스템들과 구별되는 점이다 — 여기를 보라. 이에 대한 더 자세한 내용은 이 팟캐스트에서 들을 수 있고, 여기에 전사(transcription)가 있다.
이는 매우 흥미로운 새로운 발전으로, DuckDB가 중간 규모 데이터에 대해서는 Iceberg나 Delta Lake 같은 레이크하우스 포맷을 대체할 잠재력도 갖게 한다.
데이터 처리 엔진에서 오랫동안 해결하기 어려웠던 문제 중 하나는 고성능 사용자 정의 함수(UDF)를 작성하기가 어렵다는 점이다.
예를 들어 PySpark에서는 보통 Scala로 커스텀 코드를 작성하고 JAR로 컴파일한 다음 Spark에 등록해야 최고 성능을 얻는다. 하지만 이는 번거롭고, 실제로는 Spark 버전 호환성과 DataBricks 같은 환경의 보안 제한 때문에 많은 문제를 겪게 된다.
DuckDB에서는 C++로 고성능 커스텀 UDF를 작성할 수 있다. 물론 이런 함수를 작성하는 게 쉽지는 않지만, DuckDB의 커뮤니티 확장은 코드를 마찰 없이 배포할 수 있는 방법을 제공한다. 커뮤니티 확장은 예를 들어 지리공간 데이터의 계층적 육각 인덱싱을 설치하기 위해 INSTALL h3 FROM community 같은 단일 명령으로 거의 즉시 설치할 수 있다.
팀은 문서를 단일 마크다운 파일로 제공하므로 LLM에 쉽게 제공할 수 있다.
내 최고의 팁: 이 파일을 코드 에디터에서 열고 코드 폴딩을 사용하면, 필요한 문서 부분을 컨텍스트로 복사하기 쉽다.
이 블로그 글의 많은 내용은, 대규모 레코드 연결(record linkage)을 위한 오픈 소스 라이브러리인 Splink에서 여러 SQL 방언을 지원해 온 내 경험을 바탕으로 한다. 우리는 기본 백엔드 선택으로 DuckDB를 추천하는 쪽으로 전환하면서 라이브러리 채택이 늘었고, 사용자들이 겪는 문제도 크게 줄었다. 큰 링크 작업에서도 마찬가지였고, 동시에 워크로드 속도도 매우 크게 빨라졌다.
또한 새로운 기능을 개발하고 테스트하는 과정의 단순함과 속도도 엄청나게 향상되었다.
pg_duckdb를 사용하면 Postgres 안에 DuckDB 계산 엔진을 임베드할 수 있다.특히 후자는 매우 강력한 잠재력이 있어 보인다. Postgres가 분석 처리와 트랜잭션 처리 모두에 동시에 최적화될 수 있기 때문이다. Postgres 인덱스 사용을 활성화/최적화하는 부분과, 필터를 Postgres로 푸시다운(push down)하는 현재의 몇 가지 단점을 다듬고 나면, 광범위하게 채택될 가능성이 크다고 생각한다.
오랫동안 Spark를 써 온 사용자로서, Spark 튜닝을 위한 수많은 복잡한 설정 옵션을 알아야 하는 상황에서 벗어나게 되어 기쁘다 ↩
클라우드에서 이런 192코어 프로세서를 사용할 수 있고 가격도 약 15,000달러 수준이므로, 정말로 엄청난 데이터를 다루는 게 아니라면 클러스터의 복잡함을 피할 수 있다. 또한 실제로 DuckDB의 분산(distributed) 버전도 등장했는데, 여기를 보라. ↩
분명히 말하지만, Athena는 매우 강력하고 유용한 도구다. 다만 중간 정도 복잡도의 쿼리를 개발하고 빠르게 반복(iterate)하는 과정에서는 답답하다고 느낀다. DuckDB에서 더 쉬운 이유의 예로는 이런 종류의 재현 가능한 예제(reprex)가 있다. ↩
예를 들어 emp_ 접두사로 시작하는 모든 컬럼을 선택하고, 접두사를 제거하도록 이름을 바꾸려면 다음처럼 할 수 있다: SELECT COLUMNS('emp_(.*)') AS '\\1'↩