TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 26 READS

DuckDB 2.0는 왜 빨라졌나: 데이터 모델링으로 끌어내는 속도

DuckDB 2.0는 왜 빨라졌나: 데이터 모델링으로 끌어내는 속도
SOURCE IMAGE · HACKER NEWS

올가을 정식 출시를 앞둔 DuckDB 2.0의 알파 버전이 공개됐다. MotherDuck이 공개한 벤치마크는 한 대의 노트북(M5)과 가정용 인터넷 회선에서 2.0 알파와 기존 1.5.5를 비교한 것으로, 재귀 CTE가 최대 90배, VARIANT가 JSON 텍스트 대비 6배, S3 비동기 I/O가 2.4배 빨라졌다는 수치가 핵심이다. 다만 작성자 스스로 밝혔듯 모든 숫자는 단일 장비와 한 가정의 네트워크 환경에서 나온 것이므로, 절대치로 인용하기보다 자기 환경에서 직접 돌려보고 판단하는 편이 맞다. 더 중요한 메시지는 따로 있다. 속도 향상의 상당 부분은 "데이터를 어떤 모양으로 저장하느냐"에 달려 있다는 점이다. 엔진이 빨라져도 데이터가 그 개선을 받아먹을 수 있는 형태가 아니면 체감 이득은 크지 않다.

쿼리를 바꾸지 않아도 되는 비동기 I/O

가장 손이 덜 가는 개선은 S3 비동기 I/O다. 2.2GB 규모의 Parquet 파일(2억 2800만 행, 2268개 row group)에서 한 컬럼만 읽어 집계하는 쿼리를 예로 들면, 쿼리 문법을 전혀 손대지 않고도 2.0에서 2~3배 빨라진다. 원리는 역할 분리다. 1.5.5에서는 18개 워커 각각이 다운로드와 디코딩을 번갈아 처리해, 네트워크를 기다리는 동안 CPU가 놀고 디코딩하는 동안에는 추가 다운로드가 멈췄다. 2.0은 다운로드만 전담하는 별도 스레드 풀을 두어 수십 개의 row group을 미리 받아 버퍼에 쌓아두고, 워커는 디코딩에만 집중한다. 네트워크와 CPU가 동시에 바빠지는 구조다.

이 동작을 제어하는 설정은 read_ahead_depth 하나로, 기본값 -1(스레드 수 기반 자동)이라 별도 설정 없이 켜져 있다. 0으로 두면 예전 동작으로 되돌릴 수 있다. 단 주의할 점이 있다. 수천 개의 1MB짜리 작은 파일로 데이터 레이크를 구성한 경우에는 의미 있는 개선이 없다. 작은 파일은 파일마다 푸터와 데이터를 왕복해서 읽는 라운드트립 비용이 지배적인데, 미리 읽기로는 이 왕복 자체를 없앨 수 없기 때문이다. 애초에 작은 파일을 잘게 쪼개 쌓는 방식은 권장되지 않으며, 2.0도 이 문제를 구해주지 않는다.

재귀 CTE: 그래프 DB로 넘기던 일을 일반 쿼리로

재귀 CTE는 쉽게 말해 테이블을 반복 순회하는 루프다. 조직도처럼 manager와 employee 두 컬럼만 있는 테이블에서 CEO부터 시작해 한 단계씩 하위 보고 관계를 찾아 내려가는 식이다. 조직도, 폴더 트리, 자재 명세서(BOM), 답글 스레드, 데이터 계보, git 히스토리가 모두 이 부모/자식 구조에 해당하며, 차이는 깊이뿐이다. 1.5의 문제는 매 라운드마다 다음 단계를 찾으려고 테이블 전체를 다시 읽었다는 데 있다. 8단계면 8번, 수천 단계면 같은 테이블을 수천 번 전체 스캔했다.

2.0은 테이블을 한 번만 읽어 부모 컬럼에 대한 조회 구조를 한 번 만들고, 각 라운드는 방금 찾은 소수의 행만 조회한다. 비용이 "라운드 수 × 테이블 크기"에서 "실제로 건드린 행 수"로 바뀐 셈이다. 그래서 효과는 깊이에 비례한다. 커밋마다 부모를 가리키는 2만 커밋짜리 git 히스토리처럼 깊은 체인에서는 극적으로 빨라져, 예전엔 그래프 데이터베이스로 넘기던 작업을 평범한 쿼리로 처리할 수 있다. 반대로 8단계 정도인 조직도처럼 얕은 계층에서는 체감이 크지 않다. 실무 지침은 분명하다. 계층은 정수 id를 쓰는 하나의 부모/자식 테이블로 유지하고, 재귀가 depth나 cost 같은 값을 함께 들고 다닐 때는 USING KEY를 활용하라.

VARIANT와 셰딩(shredding)

VARIANT가 정식 데이터 타입으로 승격됐고, 핵심 키워드는 셰딩이다. DuckDB가 row group을 디스크에 쓸 때 JSON 컬럼을 살펴, 대부분의 행에서 같은 종류의 값으로 나타나는 필드를 골라 실제 컬럼으로 떼어낸다. 구조화된 로그가 이상적인 사례다. level, service, latency_ms, trace_id처럼 매 줄에 같은 타입으로 들어오는 필드는 전부 셰딩되어 진짜 컬럼처럼 저장되고, 드물게 등장하거나 타입이 들쭉날쭉한 필드만 이진 블롭 형태의 '나머지'로 남는다. 나머지도 여전히 쿼리는 되지만 느리다. 함정은 latency_ms가 한 줄에서는 231, 다음 줄에서는 "231ms"처럼 값의 종류가 뒤섞이는 경우다. 이러면 해당 필드가 통째로 나머지로 떨어지므로 값의 타입을 일관되게 유지하는 것이 관건이다.

VARIANT는 속도만의 문제가 아니다. 같은 500만 건 이벤트를 JSON 텍스트와 VARIANT 등으로 저장해 비교했을 때, 일관된 필드 집합을 가진 데이터라면 저장 용량이 약 3분의 1 수준으로 줄고 필드 쿼리는 실제 컬럼처럼 동작한다. 결국 모델링의 황금률은 그대로다. 모든 쿼리가 건드리는 필드는 진짜 컬럼으로 승격하고(두 개의 문장이면 된다), 긴 꼬리의 희귀 필드는 VARIANT에 남겨두되, 당분간 핵심 쿼리에서 리스트 캐스트는 피하는 것이 좋다.

이번 릴리스에는 이 세 가지 외에도 커밋 로그에서 발견된 소소한 보강들이 있다. 테이블 변경 시 전후 상태를 전이 테이블로 보고 히스토리 테이블에 기록하는 등의 작업을 DB 쪽에서 직접 처리하는 트리거, finance.reports 같은 중첩 스키마, WITH 절 안에서 DELETE ... RETURNING 후 그 결과로 INSERT하는 CTE 내 DML(행을 원자적으로 옮기는 패턴) 등이다. 클라이언트-서버 프로토콜인 Quack도 함께 1.0이 된다. 정리하면, DuckDB 2.0의 속도는 공짜로 주어지는 부분(비동기 I/O)과 데이터를 올바르게 모델링해야 비로소 열리는 부분(재귀 CTE, VARIANT)으로 나뉜다. 비동기 I/O는 쿼리 수정 없이 켜져 있으니 그대로 누리고, 나머지 둘은 계층을 정수 id의 단일 부모/자식 테이블로 두고 일관된 타입의 필드를 VARIANT로 저장하는 식의 사전 설계가 전제되어야 한다는 점을 기억해둘 만하다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://motherduck.com/blog/why-duckdb-20-is-faster/
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기 →
처리 중...