DuckDB Labs готує до релізу версію 2.0, заплановану на осінь 2026 року, і головна архітектурна зміна в ній — асинхронне читання Parquet та CSV-файлів. У власному бенчмарку компанії запит TPC-H Query 6 над 80,89-гігабайтним CSV-файлом на S3 прискорився з 877,6 до 45,3 секунди — майже у 19 разів. Для Parquet-файлу того ж розміру приріст скромніший, але теж вражаючий: з 8,23 до 2,84 секунди, тобто майже втричі.

Ще красномовніша цифра — не про швидкість запиту, а про те, що робив процесор увесь цей час. У тесті з чотирма одночасними запитами на 64-ядерній машині DuckDB v1.5.5 у середньому займала лише 5,9 ядра з 64 — решта 90% обчислювальної потужності просто чекала на відповідь від S3. Це не проблема продуктивності операторів бази даних. Це проблема того, що потік виконання блокується на мережевому виклику замість того, щоб робити хоч якусь корисну роботу, поки дані летять по дроту.

Річ у тім, що DuckDB довгі роки взагалі не мала цієї проблеми — бо не була розрахована на мережу. Це був движок для швидкого аналізу файлів на локальному SSD, де синхронне читання майже нічого не коштує. Але за останній рік DuckDB перетворилася на компонент хмарної інфраструктури: рушій для data lake, серверний протокол Quack, підтримка DuckLake. Файли, які раніше лежали поруч на диску, тепер лежать за океаном в S3-бакеті — і синхронний I/O, який ніхто не помічав десять років, раптом став головним вузьким місцем.

Саме тому ця, на перший погляд, суто інженерна зміна — не косметика для changelog. Якщо ви ганяєте DuckDB проти S3, Iceberg-таблиць чи DuckLake з EC2, Lambda чи будь-якого compute-шару в тому самому регіоні, асинхронний I/O напряму впливає на рахунок за хмару та на те, скільки інстансів вам реально потрібно для одного й того самого навантаження.

Звідки взялася потреба в асинхронному I/O для DuckDB?

DuckDB спроєктували як «швидкодіючий» аналітичний рушій для локальних даних — щось на кшталт SQLite для OLAP-навантажень. Основна оптимізаційна робота роками йшла не навколо читання файлів, а навколо push-down фільтрів і проєкцій: рушій намагався зчитати рівно стільки байтів, скільки потрібно для конкретного запиту, розбиваючи Parquet на row group, а CSV — на буфери фіксованого розміру. Локальний SSD віддає ці шматки з низькою латентністю й високою пропускною здатністю, тож вузьким місцем ставали джойни, агрегації та підзапити — не сам доступ до даних.

Ситуація змінилася, коли DuckDB стала популярним вибором для запитів по великих віддалених data lake — наприклад, DuckLake, форматі каталогу таблиць, який просуває сама DuckDB Labs. У травні 2026-го додалася ще одна деталь: DuckDB навчилася працювати як сервер через власний Quack-протокол, а не лише як вбудована бібліотека в процесі. Типовий сценарій розгортання відтоді змінився: дані лежать у блоб-сховищі на кшталт S3, а обчислення — на EC2-машині в тому ж регіоні.

У такій конфігурації латентність і пропускна здатність мережі важать набагато більше, ніж швидкість локального диска. Якщо система не тримає одночасно достатньо HTTP-запитів у польоті, вона просто не встигає вибрати доступну смугу пропускання — і продуктивність падає драматично, бо потоки виконання проводять більшу частину часу в очікуванні відповіді, а не за декодуванням даних. За даними HN 100+, саме це протиріччя між архітектурою, розрахованою на локальний диск, і фактичним використанням проти мережевого сховища й підштовхнуло команду DuckDB до переписування шляху читання даних.

Як асинхронний конвеєр читання працює під капотом?

Концептуально ідея проста: запустити операцію вводу-виводу, не блокуючи потік, який її запросив. DuckDB реалізує це через два окремі пули потоків. Перший, REGULAR, — це звичні робочі потоки, по одному на CPU-ядро за замовчуванням: вони декодують дані, виконують джойни й агрегації, і лише за наявності вільного часу беруться за I/O. Другий, ASYNC, — пул, призначений виключно для потенційно блокуючих операцій вводу-виводу.

Оскільки потік, який чекає HTTP-відповіді від S3, майже не навантажує CPU, DuckDB заводить набагато більше ASYNC-потоків, ніж є фізичних ядер: за замовчуванням у чотири рази більше системних потоків, із жорсткою стелею у 256. Задача — тримати цей пул максимально зайнятим, а для цього рушій не чекає, поки робочий потік запросить дані, а завчасно планує читання наперед — так званий read-ahead.

Черга read-ahead і життєвий цикл job'а

Одиниця роботи в цій системі — job: для Parquet це один row group, для CSV — межа сканування, яка відповідає буферу фіксованого розміру. Job розбивається на fetch-таски — власне byte-range запити до сховища; для Parquet кількість і розмір таких тасків залежать від проєкцій запиту, push-down фільтрів і фізичного розташування колонок у файлі.

У системи немає окремого потоку-продюсера черги: будь-який REGULAR-потік, який шукає роботу для сканування, сам поповнює чергу до дозволеної межі — за кількістю слотів або за бюджетом пам'яті. Fetch-таски одразу йдуть в ASYNC-пул, а job чекає своєї черги. Усі fetch-таски одного job'а поділяють спільний лічильник, і той, що доводить його до нуля, завершує I/O для всього job'а.

Робочий потік бере найстаріший job із черги: якщо I/O вже завершене — одразу декодує дані; якщо ні — паркує сканування й береться за іншу роботу в конвеєрі, а прокидається воно, коли останній fetch-таск для нього закриється. Щойно job забрали з черги, звільнений слот одразу можна заповнити наступним — так цикл fetch → decode тримається безперервним, поки мережа й CPU не простоюють одночасно.

Як DuckDB стримує апетит до пам'яті

Читання наперед купує пропускну здатність ціною пам'яті: якщо декодування повільніше за мережу, буфери накопичуються і можуть довести процес до OOM. DuckDB вирішує це через параметр read_ahead_depth: значення -1 (за замовчуванням) означає необмежену глибину, обмежену лише пам'яттю; додатне число N фіксує максимум job'ів наперед без бюджету пам'яті; 0 вимикає read-ahead повністю, повертаючи поведінку, близьку до синхронної.

У режимі за замовчуванням бюджет узгоджується з тим самим тимчасовим менеджером пам'яті, який ділить ресурси між конкурентними джойнами, сортуваннями й віконними операціями. Це означає, що під час пікового навантаження від «важкого» оператора черга read-ahead може одномоментно втратити майже весь бюджет і почати поводитися практично синхронно — а щойно пам'ять звільниться, знову розганяється.

Чим асинхронний I/O в DuckDB відрізняється від Trino, ClickHouse чи Spark?

У розподілених рушіїв на кшталт Trino чи Spark латентність мережі традиційно ховають не всередині одного процесу, а масштабом кластера: сотні executor-ів чи worker-ів одночасно тримають у польоті багато запитів до S3, і сумарна конкурентність кластера компенсує повільність окремого потоку. Це працює, але коштує інфраструктури — більше JVM-процесів, більше оверхеду на координацію, більше вартості за просте очікування.

ClickHouse рухається в подібному напрямку вже давно: у нього є окремий пул для асинхронних читань і підтримка io_uring — інтерфейсу асинхронного вводу-виводу ядра Linux — для локальних файлів. DuckDB наразі свідомо не йде туди: команда прямо каже, що дослідження io_uring — це наступний крок, а не поточна реалізація, і ASYNC-пул із блокуючими викликами — це проміжний, а не фінальний архітектурний вибір.

Показовий контраст — DataFusion, аналітичний рушій на Rust з екосистеми Apache Arrow: він асинхронний за задумом із самого початку, бо побудований на рантаймі Tokio, де кожна операція вводу-виводу — це async fn за замовчуванням. DuckDB, написана на C++ і історично синхронна в самій основі, натомість добудовує асинхронність окремим шаром зверху — двома пулами потоків і чергою read-ahead — не переписуючи весь рушій на event loop. Це прагматичніше й безпечніше для зрілого проєкту, але й менш елегантно, ніж рушій, асинхронний з нуля.

Окрема категорія — керовані хмарні сховища на кшталт Snowflake чи BigQuery: там шар вводу-виводу взагалі не бачить користувач, і порівнювати підходи до нього безглуздо. Але саме з ними DuckDB та DuckLake конкурують за той самий сценарій — дешева аналітика над даними в S3 без окремого керованого кластера, — і швидкість читання напряму впливає, наскільки цей self-hosted шлях виглядає економічно виправданим.

Що це дає тим, хто будує продукти на DuckDB просто зараз?

Перший практичний випадок — регулярні batch-запити над таблицями DuckLake чи Iceberg у S3, які виконуються з EC2 за розкладом. Бенчмарк на машині r7i.16xlarge (64 vCPU, 512 ГБ RAM, той самий регіон, що й S3-бакет) показав пришвидшення Parquet-запиту з 8,23 до 2,84 секунди без жодного тюнінгу, а з ручним налаштуванням кількості ASYNC-потоків і параметрів HTTP-ретраїв — до 2,23 секунди, тобто у 3,7 раза швидше за v1.5.5. Для команди, яка платить за EC2-години, це не лише швидший дашборд, а й потенційно менша інстанс-флота під те саме навантаження.

Другий сценарій — сирі CSV-дампи від партнерів чи з legacy-систем, які традиційно намагалися якнайшвидше сконвертувати в Parquet, бо синхронне читання CSV на віддаленому сховищі було нестерпно повільним. Приріст у 19 разів на CSV-запиті знімає значну частину цього тиску: ad hoc аналітика прямо по сирих CSV у S3 стає життєздатним варіантом для розвідувального аналізу й дебагу пайплайна, а не лише проміжним кроком перед конвертацією.

Третій — продукти з багатьма одночасними користувацькими запитами на одному інстансі DuckDB: вбудовані аналітичні бекенди, notebook-сервіси, self-serve BI. У тесті з чотирма паралельними TPC-H запитами сумарний час впав із 35,8 до 15,6 секунди, а середнє завантаження CPU зросло з 5,9 до 48,1 ядра з 64 доступних. Для будь-кого, хто впирався в стелю паралельності через I/O-голодування, а не через брак CPU, це прямий доступ до потужності, яка й так стояла в машині.

Що може піти не так із асинхронним I/O в DuckDB?

Найочевидніше обмеження — статус готовності. Асинхронний I/O доступний тільки в preview-збірках v2.0.0-dev, а стабільний реліз заплановано лише на осінь 2026 року. Команди, які хочуть скористатися прискоренням уже зараз, свідомо беруть на себе ризики пре-релізного програмного забезпечення у продакшені.

Друге — пам'ять як прихована плата за швидкість. Read-ahead купує пропускну здатність ціною буферів, і якщо в тому самому запиті працює важкий джойн чи сортування, тимчасовий менеджер пам'яті може практично забрати весь бюджет у черги read-ahead — і сканування непомітно скотиться до майже синхронної поведінки посеред виконання запиту. Це новий клас важкодіагностованого сповільнення, з яким операційним командам доведеться навчитися жити.

Третє — розмір row group у Parquet-файлі раптом став параметром продуктивності, про який мало хто думав під час запису даних. У бенчмарку з тим самим датасетом, розбитим на різну кількість row group, оптимум виявився не в найбільшій і не в найменшій, а десь посередині: конфігурація з 306 групами відпрацювала за 2,11 секунди, тоді як варіант з єдиною group на весь 600-мільйонний файл — за 25,26 секунди, попри те що сам файл через кращу компресію був майже вдвічі меншим. Замало груп — і немає з чого нарізати паралельні мережеві запити.

Четверте — покриття форматів поки часткове: асинхронне читання зараз є лише для Parquet і незакодованого seekable UTF-8 CSV. JSON і власний формат DuckDB на черзі, а формати з зовнішніх (out-of-tree) розширень — поки що навіть не в планах. Реальний lakehouse-стек, який мішає формати, отримає вигоду лише частково.

П'яте — найкращі цифри з бенчмарків вимагають ручного тюнінгу. Стандартний режим із бюджетом пам'яті дає близько трикратного приросту, але щоб вичавити повні 3,7 раза й дійсно насититися 25-гігабітну мережу, знадобилося вручну виставити кількість ASYNC-потоків, кількість HTTP-ретраїв і backoff — параметри, підібрані під конкретну 64-ядерну машину. Без такої експертизи більшість команд отримають хороший, але не рекордний результат.

Часті запитання про асинхронний I/O в DuckDB

Коли асинхронний I/O стане доступним за замовчуванням?

З релізом DuckDB v2.0, запланованим на осінь 2026 року. Протестувати механізм можна вже зараз у preview-збірках v2.0.0-dev.

Чи потрібно переписувати SQL-запити, щоб отримати приріст?

Ні — асинхронне читання вмикається на рівні сканування Parquet і CSV прозоро для запиту. Тюнити можна опційно: параметр read_ahead_depth і кількість ASYNC-потоків, якщо хочеться вичавити максимум із конкретного заліза.

Чи допоможе це запитам над локальним диском?

Так, але значно скромніше — приблизно у 1,5 раза на холодних (некешованих) читаннях, за даними тесту на MacBook Pro з чипом Apple M4 Max. На вже прогрітому кеші різниці практично немає, бо диск узагалі не задіяний.

Висновок AiiN

Асинхронний I/O — це не окрема фіча, а визнання того, що DuckDB остаточно перестала бути тільки «швидким SQLite для аналітики» і стала компонентом хмарної lakehouse-інфраструктури поруч із DuckLake і Quack-протоколом. Ці три речі складаються в одну ставку: DuckDB Labs хоче, щоб пара «DuckDB + об'єктне сховище» була самодостатньою альтернативою важким розподіленим рушіям для більшості аналітичних навантажень середнього масштабу.

Наш прогноз на найближчі 6–12 місяців: перші, хто відчує ефект, — це продукти, вже побудовані на DuckLake й вбудованому DuckDB (включно з MotherDuck та подібними хмарними обгортками), для яких зміна відбудеться непомітно для користувача, але помітно для рахунку за компʼютинг. Далі варто чекати практичних кейс-стаді від команд, які ганяють DuckDB-based ETL на EC2, — з реальними цифрами економії на інстансах, а не лабораторними TPC-H. Наприкінці цього вікна ймовірна перша публічна оцінка io_uring від самої команди DuckDB та розширення асинхронного читання на JSON і нативний формат — це природне продовження вже описаної тут архітектури, а не гіпотеза з нуля.

Головне застереження для тих, хто читає ці бенчмарки й одразу планує міграцію: TPC-H Query 6 — це майже ідеальний I/O-bound сценарій, сканування з фільтром і агрегацією без важких джойнів. На CPU- або join-важких запитах приріст буде помітно скромнішим, а на вже добре прогрітих локальних кешах — і зовсім близьким до нуля. Асинхронний I/O розв'язує конкретну проблему — просто мережі, а не всі проблеми продуктивності аналітичних запитів одразу.