DuckDB v2.0 добавит асинхронный ввод-вывод: запросы к S3 ускорились в 3, 3,7 раза

Большую часть своей истории DuckDB проектировалась для локального использования: база быстро читала Parquet- и CSV-файлы прямо с SSD того же компьютера, а фильтры и проекции запроса заранее отбрасывали лишние данные, поэтому синхронное чтение работало хорошо, а узкими местами были джойны, агрегации и подзапросы, а не сам доступ к данным. Но ситуация изменилась: DuckDB всё чаще используют для запросов к удалённым дата-лейкам (например, DuckLake), а с мая 2026 года её можно развернуть и как сервер по протоколу Quack. Типичная сегодняшняя схема, данные лежат в объектном хранилище вроде S3, а обрабатывает их машина EC2 в том же регионе; в такой связке решающими становятся задержка и пропускная способность сети, если не держать в полёте достаточно параллельных запросов, рабочие потоки простаивают в ожидании ответа от сети вместо обработки данных.
При синхронном чтении, например при сканировании одного удалённого Parquet-файла командой вида FROM read_parquet('s3://bucket/file.parquet'), работа разбивается на группы строк (row group), каждая из которых порождает один или несколько запросов на диапазон байтов, а рабочий поток блокируется и просто ждёт ответа по сети, прежде чем начать декодирование. DuckDB реализовала для этого асинхронный конвейер ввода-вывода (async I/O) на двух отдельных пулах потоков. Пул REGULAR (по умолчанию один поток на каждый системный поток) выполняет собственно работу, декодирование, джойны, агрегации, но при простое может подхватывать и задачи ввода-вывода. Пул ASYNC целиком выделен под блокирующие операции ввода-вывода: поскольку такие потоки почти всё время проводят в ожидании сетевого ответа и почти не грузят CPU, их держат намного больше, чем системных потоков, по умолчанию 4 потока на каждый системный поток, с общим потолком в 256.
Ключевой приём, чтение с упреждением (read-ahead): вместо того чтобы запускать чтение точно в момент, когда данные понадобились рабочему потоку, DuckDB заранее ставит в очередь fetch-задачи для следующих заданий, пока текущий рабочий поток декодирует уже загруженные данные. Единица работы, задание (job): для Parquet это одна группа строк, для CSV, участок файла фиксированного размера (scan boundary); одно Parquet-задание может распадаться на несколько fetch-задач в зависимости от того, какие столбцы и фильтры запрошены и как физически расположены байтовые диапазоны. Очередь заданий пополняет любой рабочий поток, который ищет, что бы просканировать: он добавляет задания, пока не упрётся в лимит, либо заданное число слотов, либо бюджет памяти. Fetch-задачи сразу уходят в пул ASYNC; когда рабочий поток забирает самое старое задание из очереди и видит, что все его fetch-задачи выполнены, он декодирует данные, а если они ещё не готовы, откладывает эту задачу и берётся за другую, пока не поступит сигнал о завершении.
У чтения с упреждением есть цена, память: если сеть отдаёт данные быстрее, чем рабочие потоки успевают их декодировать, предзагруженные данные накапливаются и рискуют упереться в нехватку памяти. Поэтому в DuckDB добавили настройку read_ahead_depth с тремя режимами: -1 (по умолчанию), глубина не ограничена жёстко, но регулируется общим бюджетом памяти, которым распоряжается тот же временный менеджер памяти, что делит её между параллельными джойнами, сортировками и оконными операторами (при сильном давлении на память бюджет может схлопнуться до одного задания за раз, и скан начинает вести себя почти как синхронный); N больше нуля, жёсткий лимит в N заданий вперёд без учёта памяти; 0, упреждающее чтение выключено полностью, и каждое задание сканирования запрашивает ввод-вывод только для себя.
Асинхронный ввод-вывод сейчас реализован для Parquet и для несжатых CSV-файлов с произвольным доступом (seekable) в UTF-8; поддержка родного формата DuckDB и JSON пока не готова. Опробовать его уже можно в превью-сборках v2.0.0-dev, а по умолчанию для всех пользователей он включится с выходом версии 2.0 осенью 2026 года. Эффект команда измерила на TPC-H Query 6 при масштабном факторе SF100: таблица lineitem содержала 600 037 902 строки, данные лежали на S3, а обрабатывала их машина EC2 r7i.16xlarge (64 виртуальных CPU, 512 ГБ RAM) в том же регионе; кеширование файлов было отключено, поэтому каждый прогон читал данные заново, а каждый вариант настроек прогнали пять раз и взяли среднее время.
Для Parquet-файла размером около 22 ГБ (примерно 4880 групп строк по ~122 880 строк в каждой) среднее время запроса упало с 8,230 секунды на DuckDB v1.5.5 (синхронное чтение) до 2,844 секунды на v2.0.0-dev с настройками по умолчанию, почти втрое быстрее. После ручной настройки, глубина упреждения ограничена 64 заданиями в полёте, поднят async_threads=48 и добавлены более настойчивые повторы запроса (http_retries=8, http_retry_wait_ms=50, http_retry_backoff=2), время снизилось до 2,227 секунды: на 21,7% быстрее прогона с настройками по умолчанию и примерно в 3,7 раза быстрее DuckDB v1.5.5. По сетевому трафику разница ещё нагляднее: настроенная версия почти полностью выбирает доступный канал в 25 Гбит/с, тогда как v1.5.5 держится на уровне около 5 Гбит/с, синхронное чтение не успевает выставлять в полёт достаточно параллельных запросов. Команда также отмечает небольшую задержку перед стартом основной передачи данных: несколько сотен миллисекунд уходит на открытие соединения и TLS-рукопожатие, ещё несколько сотен, на загрузку и обработку блока метаданных (footer) в конце Parquet-файла, и называет это направлением для дальнейшей оптимизации до выхода v2.0.
Материал, технический пост в официальном блоге проекта DuckDB, написан от лица команды («мы»), но подписан именем Pedro Holanda; в обсуждении на Hacker News его разместил пользователь pdet, под тем же именем на Hacker News известен сам Pedro Holanda, то есть ссылку, по всей видимости, разместил автор поста. Захваченный для этого пересказа текст обрывается на середине раздела о сетевом трафике, до того как в посте приводятся результаты отдельного бенчмарка для CSV-файлов и итоговый вывод, эти цифры в пересказе поэтому не приводятся.
Ключевые факты
- DuckDB добавляет асинхронный ввод-вывод (async I/O) для чтения удалённых Parquet- и CSV-файлов: возможность войдёт в v2.0 осенью 2026 года и уже доступна в превью-сборках v2.0.0-dev; поддержка родного формата DuckDB и JSON пока не готова.
- Механизм строится на двух пулах потоков: REGULAR (по одному потоку на каждый системный поток, декодирование, джойны, агрегации) и ASYNC (по умолчанию 4 потока на каждый системный поток, с лимитом в 256), потоки ASYNC держат в полёте запросы к удалённому хранилищу, пока REGULAR обрабатывает уже загруженные данные.
- Чтение с упреждением (read-ahead) заранее запускает fetch-задачи для следующих групп строк (Parquet) или участков файла (CSV); глубину настраивает опция read_ahead_depth: -1, по бюджету памяти (по умолчанию), N, жёсткий лимит в N заданий, 0, упреждение выключено полностью.
- На тесте TPC-H Query 6 (SF100, ~22 ГБ Parquet, таблица lineitem на 600 037 902 строки, данные на S3, обработка на EC2 r7i.16xlarge) среднее время запроса упало с 8,230 с на DuckDB v1.5.5 до 2,844 с на v2.0.0-dev с настройками по умолчанию, почти втрое быстрее; после ручной настройки время снизилось до 2,227 с, на 21,7% быстрее прогона с настройками по умолчанию и примерно в 3,7 раза быстрее v1.5.5.
- На настроенном прогоне сеть почти полностью выбирает канал в 25 Гбит/с, тогда как DuckDB v1.5.5 с синхронным чтением держится на уровне около 5 Гбит/с, синхронные запросы не успевают выставлять в полёт достаточно параллельных обращений.
Почему это важно
DuckDB долгое время проектировалась для локального использования: она быстро читала Parquet- и CSV-файлы прямо с SSD того же компьютера, а фильтры и проекции запроса заранее отбрасывали лишнее, поэтому синхронное чтение работало хорошо, а узкими местами были джойны, агрегации и подзапросы, не сам доступ к данным. Но ситуация изменилась: DuckDB всё чаще применяют для запросов к удалённым дата-лейкам (например, DuckLake), а с мая 2026 года её можно развернуть и как сервер по протоколу Quack, то есть данные всё чаще лежат не на локальном диске, а в объектном хранилище вроде S3, пока обрабатывает их отдельная машина (например, EC2) в том же регионе. В такой схеме решающими становятся задержка и пропускная способность сети: если не держать в полёте достаточно параллельных запросов, чтение не успевает загрузить весь доступный канал, а рабочие потоки простаивают в ожидании ответа от сети вместо обработки данных. Асинхронный ввод-вывод, прямой ответ на этот сдвиг: по словам команды DuckDB, он даёт наибольший эффект именно там, где синхронные запросы не позволяют выбрать всю доступную удалённую пропускную способность.
Кому это важно
В первую очередь, инженерам данных и аналитикам, которые гоняют DuckDB не по локальным файлам, а поверх облачных хранилищ: строят конвейеры над data lake на S3 и похожих хранилищах (в том числе через DuckLake) или разворачивают DuckDB как сервер по протоколу Quack. Для привычного сценария «небольшой CSV или Parquet на локальном SSD» изменение почти не заметно, там синхронное чтение и так справлялось хорошо, а узкое место было в других частях движка.
Как это применить
Опробовать асинхронный ввод-вывод уже можно в превью-сборках v2.0.0-dev, а по умолчанию для всех он включится с выходом версии 2.0 осенью 2026 года. Поведение настраивается опцией read_ahead_depth: -1 (по умолчанию), глубина не ограничена жёстко, но регулируется общим бюджетом памяти, которым распоряжается тот же временный менеджер памяти, что делит её между параллельными джойнами, сортировками и оконными операторами; положительное число N, жёсткий лимит в N заданий вперёд без учёта памяти; 0, упреждающее чтение выключено полностью, и каждое задание сканирования запрашивает ввод-вывод только для себя. В своём бенчмарке команда DuckDB для лучшего результата дополнительно подняла число ASYNC-потоков до 48, сделала повторы HTTP-запросов более настойчивыми (http_retries=8, http_retry_wait_ms=50, http_retry_backoff=2) и ограничила глубину упреждения 64 заданиями в полёте, это дало ещё 21,7% ускорения сверх настроек по умолчанию. Пост не говорит, станут ли именно эти значения новыми настройками по умолчанию в релизе v2.0.
Можно ли доверять
Материал, официальный технический пост в блоге самого проекта DuckDB, написан от лица команды («мы»), но подписан именем Pedro Holanda; в обсуждении на Hacker News его разместил пользователь pdet, под тем же именем на Hacker News известен сам Pedro Holanda, то есть ссылку, по всей видимости, разместил автор поста. Цифры получены в контролируемом бенчмарке самих разработчиков (TPC-H Query 6, пять прогонов, усреднение, кеш файлов отключён), а не в независимом стороннем тесте, так что к сравнению стоит относиться как к результату производителя на выбранном им сценарии, сравнения с другими движками (Spark, Trino, ClickHouse и подобными) пост не приводит. Захваченный для этого пересказа текст обрывается на середине раздела о сетевом трафике, до результатов отдельного бенчмарка для CSV-файлов и итогового вывода поста, эти данные в пересказе не отражены.
Риски и подводные камни
Сама команда DuckDB предупреждает о компромиссе: чтение с упреждением тратит память, и если декодирование медленнее сети, предзагруженные данные могут накапливаться и приводить к нехватке памяти, отсюда и отдельный механизм асинхронного управления памятью с общим бюджетом для остальных операторов. При сильном давлении на память очередь на практике схлопывается до одного задания за раз, и скан начинает вести себя почти как синхронный, то есть в условиях нехватки памяти обещанного ускорения может не быть. Пока поддержаны только Parquet и несжатые CSV-файлы с произвольным доступом (seekable) в UTF-8; родной формат DuckDB и JSON, ещё нет, и сроков их появления пост не называет. Наконец, сама функциональность на момент публикации не вышла в стабильном релизе, она доступна только в dev-превью v2.0.0, а до финального v2.0 осенью 2026 года формат и настройки по умолчанию могут измениться.
«Асинхронный ввод-вывод должен давать наибольший эффект там, где задержка синхронных запросов не позволяет использовать всю доступную удалённую пропускную способность.»
— команда DuckDB (блог проекта)