DuckDB.NET 1.5.6.1: Reading Data Up to 12× Faster
DuckDB.NET 1.5.6.1, 데이터 읽기 최대 12배 빨라져
DuckDB.NET 1.5.6.1은 DuckDB 엔진을 그대로 둔 채 .NET 데이터 리더의 형식 변환과 호출 경로를 개선했습니다. 벤치마크에서 읽기 작업이 최대 12배 빨라졌고, 정수 열을 double로 읽을 때 할당량은 366MB에서 18KB로 줄었습니다.
- 주제
에디터 노트
이 성능 향상은 데이터베이스 쿼리 자체보다 DuckDB와 .NET 사이에서 값을 옮기는 비용을 줄인 결과입니다. 같은 DuckDB 엔진을 사용하고 SQL 집계 대조군의 차이가 거의 없었다는 점, 정수 변환 경로의 박싱과 가상 호출을 줄인 코드 변경이 수치와 함께 제시됐다는 점이 주장을 뒷받침합니다. 따라서 최대 12배라는 수치는 모든 쿼리나 애플리케이션의 전체 실행 시간이 12배 빨라진다는 뜻이 아니라 특정 읽기 작업에서 나온 결과입니다. 댓글의 OLAP와 OLTP 구분은 DuckDB와 SQLite의 용도를 묻는 논의에 해당하며 이 벤치마크의 성능 결과를 반박하지 않습니다. AI 생성 여부를 둘러싼 댓글도 코드와 측정 결과를 검증하는 근거는 제시하지 않습니다. 반면 실제 사용자가 메타데이터를 읽는 지연을 피해 Parquet 파일 목록을 직접 만들었다는 경험은 이 글이 측정한 값 변환 비용과 다른 병목이 애플리케이션에 남을 수 있음을 보여줍니다. 전체 개선을 판단하려면 자신의 읽기 형식과 데이터 경로를 기준으로 확인해야 합니다.
AI 요약
DuckDB.NET 1.5.6.1의 데이터 읽기 성능 개선은 DuckDB 엔진이 아니라 .NET 클라이언트 코드에서 나왔습니다. 작성자는 DuckDB.NET 1.5.6과 1.5.6.1을 같은 DuckDB 1.5.6 네이티브 엔진에서 실행해 비교했습니다. 벤치마크 환경은 .NET 10, AMD EPYC 9V45 8코어 Azure F8as_v7 가상 머신이며 BenchmarkDotNet으로 각 사례를 4개 프로세스에서 실행했습니다. 같은 버전을 서로 비교한 결과 최대 약 15% 차이는 측정 잡음으로 봤고, SQL 집계처럼 DuckDB가 대부분의 일을 하는 대조군은 두 버전 간 차이가 3% 이내였습니다.
데이터 리더에서 바꾼 다섯 가지
DuckDB는 쿼리 결과를 열마다 벡터로 묶어 메모리에 반환하고, DuckDBDataReader는 그 메모리에서 값을 읽습니다. 기존 구현에서는 GetInt32에서 GetFieldValue<int>로, 다시 제네릭 가상 메서드로 이어지는 호출마다 런타임 조회와 간접 분기 비용이 들었습니다. JIT 어셈블리를 살펴보면 실제 정수 값을 가져오는 명령은 두 개뿐이었고 나머지는 가상 호출과 열 형식 확인에 쓰였습니다.
첫째, DuckDBDataReader를 sealed로 선언해 상속과 재정의 가능성을 없앴습니다. 이에 따라 JIT가 GetFieldValue<T> 호출을 직접 연결하고 인라인할 수 있었고, 이 변경만으로 정수 읽기가 약 1.5배 빨라졌습니다. 둘째, 벡터가 보관한 .NET 형식과 요청 형식이 같으면 하위 리더를 거치지 않고 메모리에서 바로 읽는 빠른 경로를 추가했습니다. INTEGER 열을 int로 읽는 사례는 이 단계에서 4배 빨라졌습니다.
셋째, 정수 열을 double로 변환할 때 발생하던 박싱을 없앴습니다. 기존 코드는 열의 저장 형식을 런타임 스위치로 판별하고 object를 반환하면서 값을 두 번 박싱했습니다. 800만 개 값에서 366MB가 할당됐습니다. 열 저장 형식별로 NumericVectorDataReader<TStorage>를 만들고 변환도 제네릭으로 처리해 스위치와 박싱을 제거했습니다. 그 결과 GetDouble 읽기는 이후 단계에서 6.7배 빨라졌고 할당량도 18KB로 줄었습니다.
넷째, ReadInt64, ReadDouble 같은 형식별 일반 가상 메서드를 도입했습니다. 제네릭 가상 메서드에서 발생하던 런타임 조회를 일반 가상 호출로 바꿔 JIT가 더 효율적으로 처리하게 했습니다. 다섯째, nullable 값 읽기에 쓰던 표현식 트리 컴파일 대리자를 일반 메서드로 바꿨습니다. 표현식 트리 대리자는 한 번 컴파일된 뒤 계층형 컴파일의 최적화를 받지 못했지만, 일반 메서드는 JIT가 실행 정보를 반영해 다시 최적화할 수 있습니다.
측정 결과와 적용 범위
다섯 변경을 합친 결과 GetInt32는 59.8ms에서 10.5ms로 5.69배, nullable int 읽기는 70.4ms에서 15.4ms로 4.57배 빨라졌습니다. 여러 정수 형식의 열을 GetInt64로 읽는 작업은 60.3ms에서 15.2ms로 3.96배, GetDouble로 읽는 작업은 215.3ms에서 17.8ms로 12.11배 빨라졌습니다. GetDouble 사례의 할당량은 366MB에서 18KB로 감소했습니다. 짧은 ASCII 문자열 읽기는 1.35배 빨라졌지만 긴 문자열은 디코딩과 문자열 할당 시간이 커 개선 효과가 나타나지 않았습니다.
본문 첫머리의 전체 기능 비교표에는 읽기 외 성능도 포함됩니다. 예를 들어 typed scalar function은 5~7.7배, table function은 2.5~6배 빨라졌고 DateTime을 100만 행 추가하는 작업은 772ms에서 29.4ms로 줄며 할당량이 2.8GB에서 93KB로 감소했습니다. 다만 이 글의 상세 분석은 DuckDBDataReader에 집중하며, 나머지 기능은 후속 글에서 다룬다고 밝혔습니다. 개선 사항은 DuckDB.NET 1.5.6.1과 2.0에 반영되며, 기본 패키지 설치 명령은 dotnet add package DuckDB.NET.Data.Full --version 1.5.6.1입니다. 네이티브 라이브러리를 직접 공급하는 경우에는 DuckDB.NET.Data를 사용합니다.
Reddit 반응
- @countkillalot — DuckDB를 지켜보고 있는데 괜찮은 SQLite 대체재처럼 보입니다. 하지만 대부분의 애플리케이션에는 SQLite면 충분합니다. 자체 분석 도구를 운영하는 경우 말고는 운영 환경에서 쓸 만한 사례를 설명하기 어렵습니다. 전문 검색용 Lucene 플러그인이 있으면 좋을지도 모르겠습니다.
- @longshot — DuckDB는 OLAP에 더 적합하고 SQLite는 OLTP에 더 적합합니다.
- @ShikadaSorel — 글이 잘 쓰였고 성능 향상도 훌륭합니다.
- @Darwin226 — 글이 100% AI로 생성된 것 같습니다.
- @ShikadaSorel — 제 내부 AI 탐지기는 반응하지 않았지만 그럴 수도 있겠네요. 그래도 기술 설명이 잘 쓰였고 정보가 밀도 있게 담겼습니다. 관련 코드와 디스어셈블리도 원하는 수준으로 보여주며 결과도 명확합니다. Microsoft가 매년 공개하는 성능 개선 글과 크게 다르지 않아 보입니다.
- @Hacnar — 요즘은 올라오는 글마다 AI가 썼다고 말하는 사람이 있습니다. 노력 없는 댓글이지만 사람들이 곧 그런 말을 밀어내기 바랍니다.
- @atrocia6 — 그 여부는 모르겠지만 작성자 소개에 Claude Code와 Codex 같은 AI 코딩 에이전트를 점점 더 많이 활용하면서도 직접 코드를 다룬다고 적혀 있습니다.
- @KeyboardG — 반가운 업데이트입니다. DuckDB.NET을 많이 쓰면서 저장소 지연, 특히 푸시다운 필터링을 적용할 메타데이터를 가져오는 지연을 피해 왔습니다. 결국 Parquet 파일 목록을 직접 만들었습니다. 이번 주 후반에 새 버전을 써보겠습니다.
- @mcnamaragio — 다른 개선 사항도 많습니다. 릴리스 노트를 확인해 보시고 한번 사용해 보세요.
- @cheezballs — 또 다른 데이터베이스 프로젝트가 하루에 하나씩 나오는군요.
- @deanrihpee — DuckDB는 나온 지 꽤 되지 않았나요?
원문: giorgi.dev / 번역·요약: Trawling