DuckDB 2.0 в альфе читает Parquet из S3 в 2, 3 раза быстрее

DuckDB 2.0 в альфе читает Parquet из S3 в 2, 3 раза быстрее

DuckDB 2.0 ожидается этой осенью, альфа-версия уже доступна. Автор поста в блоге MotherDuck (имя в тексте не указано) прогнал главные новшества на ноутбуке M5 и домашнем интернете, чтобы показать, что меняется для тех, кто строит таблицы и конвейеры данных, а не сами СУБД. Его главный тезис: DuckDB 2.0 действительно быстрее, но чтобы получить прирост, нужно понимать, как устроены ваши данные, а иногда и как их моделировать. Все цифры в посте получены на одной машине, и автор прямо просит прогонять собственные замеры, прежде чем на них ссылаться.

Первое новшество, асинхронный ввод-вывод (async I/O) при чтении по S3. Тестовый запрос читает файл Parquet размером 2,2 ГБ (голоса Stack Overflow: 228 млн строк, 2268 групп строк по примерно 122 000 строк) и считает голоса по типам; из четырёх столбцов читается один, около 230 МБ. В версии 1.5.5 каждый из 18 рабочих потоков по очереди скачивал данные и декодировал их: пока поток ждал сеть, процессор простаивал, а одновременно шло не больше 18 загрузок. В 2.0 отдельный пул потоков только скачивает, держит в полёте десятки групп строк и складывает байты в буфер, а рабочие потоки только декодируют. Сеть и процессор заняты одновременно. Регулирует это параметр read_ahead_depth: он по умолчанию равен -1 (автоматически, исходя из числа потоков), то есть функция включена сразу, а значение 0 возвращает поведение 1.5. Итог по замерам автора: чтение по S3 быстрее в 2-3 раза без изменений в запросах. Абсолютное время запросов в тексте не приведено. Оговорка автора: на тысячах мелких файлов Parquet по 1 МБ заметной разницы нет, потому что время уходит на обращения за каждым файлом, и 2.0 такую раскладку не спасёт.

Второе, переписанный движок рекурсивных запросов (WITH RECURSIVE, CTE). По заявлению команды DuckDB, ускорение на достижимости в графе составляет до 40 раз (это утверждение команды, которое цитирует автор). Раньше на каждом раунде рекурсии движок заново перечитывал всю таблицу; в 2.0 таблица читается один раз, индекс по родительскому столбцу строится один раз, и стоимость зависит от реально затронутых строк, а не от числа раундов, умноженного на размер таблицы. Выигрыш виден на глубоких цепочках родитель, потомок: история git, происхождение данных (data lineage), ветки ответов, полная спецификация изделия. Для этого автор сгенерировал репозиторий на 20 000 коммитов и обошёл предков HEAD до корня, но числа времени в опубликованном тексте не приведены. На неглубоких иерархиях вроде оргструктуры, по словам автора, много выигрыша не будет. Совет: хранить иерархию одной таблицей родитель, потомок с целочисленными идентификаторами, а USING KEY применять, когда рекурсия несёт значение вроде глубины или стоимости.

Третье, тип VARIANT, который в 2.0 стал полноценным типом данных. Ключевое слово, shredding (разбиение на столбцы): при записи группы строк на диск DuckDB находит поля JSON, которые встречаются в большинстве строк с одним и тем же видом значения, и выносит их в отдельные настоящие столбцы; редкие поля и поля с разными типами значений остаются в двоичном остатке. Автор сохранил пять миллионов JSON-событий тремя способами (строка JSON, VARIANT, обычная таблица с типизированными столбцами) и прогнал три запроса. VARIANT получился в 2,7 раза меньше JSON-строки (в итоговой сводке автора, «треть хранилища»). На запросах, затрагивающих разбитые на столбцы поля (фильтр или сумма по числовому подполю), VARIANT примерно в 6 раз быстрее разбора текста JSON и в пределах 20 процентов от типизированных столбцов; относительно VARIANT из 1.5.5, в 78 раз быстрее, потому что тип там был, а разбиения на столбцы не было. Исключение, запросы со списками: приведение списка VARIANT к VARCHAR[] в этой альфе занимает две секунды и медленнее пути через JSON. Правило моделирования прежнее: то, что трогает каждый запрос, достойно настоящих столбцов. Ловушка: поле, которое в одной строке число (231), а в другой текст («231ms»), тоже попадает в остаток.

Из мелочей в посте перечислены триггеры (с таблицами переходов до и после изменения, например для истории цен), вложенные схемы (CREATE SCHEMA finance.reports), DML внутри CTE (DELETE ... RETURNING в WITH с последующей вставкой, перенос строк атомарно) и режим совместимости с диалектом Spark SQL (SET dialect_compatibility_mode = 'spark'). Параметр external_file_cache_spill = true сбрасывает вытесненные из памяти блоки удалённых файлов во временный каталог вместо повторной загрузки: при лимите памяти 300 МБ второе чтение файла Parquet в 854 МБ на S3 сократилось с 23,9 до 0,35 секунды. В командной строке появились форматировщик SQL и запрашиваемая история; read_json теперь определяет метки времени ISO-8601 со смещением как TIMESTAMPTZ вместо тихой потери смещения; CREATE SECRET ... IN CONNECTION ограничивает секрет одним соединением. Клиент-серверный протокол Quack в этом релизе выходит в версию 1.0. Альфу можно поставить одной строкой: curl https://install.duckdb.org | DUCKDB_VERSION=alpha bash. MotherDuck, по словам автора, будет поддерживать 2.0 ближе к выходу релиза.

Ключевые факты

  • Асинхронный ввод-вывод в DuckDB 2.0 включён по умолчанию (read_ahead_depth = -1): отдельный пул скачивает группы строк Parquet заранее, и чтение по S3, по замерам автора на ноутбуке M5 и домашнем интернете, быстрее в 2, 3 раза без изменений запросов.
  • Движок рекурсивных запросов переписан: команда DuckDB заявляет ускорение до 40 раз на достижимости в графе; выигрыш ожидается на глубоких цепочках (история git, происхождение данных), на неглубоких иерархиях вроде оргструктуры он небольшой.
  • VARIANT с разбиением на столбцы (shredding) в тесте на пяти миллионах JSON-событий в 2,7 раза меньше JSON-строки, примерно в 6 раз быстрее разбора текста на запросах по разбитым полям и в 78 раз быстрее VARIANT из 1.5.5; запросы со списками пока медленнее (приведение занимает две секунды).
  • external_file_cache_spill = true при лимите памяти 300 МБ сократил второе чтение файла Parquet в 854 МБ на S3 с 23,9 до 0,35 секунды.
  • Релиз ожидается этой осенью, альфа уже доступна; в него входят триггеры, вложенные схемы, DML в CTE, режим совместимости со Spark SQL и Quack 1.0.

Почему это важно

DuckDB, встраиваемая аналитическая СУБД, и главный тезис поста в том, что 2.0 приносит ускорение не за счёт магии, а за счёт того, что движок лучше использует сеть и процессор одновременно и умнее хранит полуструктурированные данные. Для тех, кто читает озеро данных из S3, прирост приходит без правки запросов: по замерам автора, чтение по S3 в 2.0 быстрее в 2-3 раза, чем в 1.5.5. Для задач с глубокими иерархиями автор считает, что 2.0 превращает работу, которую раньше отправляли в графовую базу данных, в обычный запрос.

Кому это важно

Тем, кто строит таблицы и конвейеры данных на DuckDB: аналитикам и инженерам данных, читающим Parquet из S3, обрабатывающим события и логи в JSON, обходящим деревья и графы (история git, происхождение данных, ветки ответов, спецификации изделий). Тем, кто мигрирует конвейеры со Spark SQL, может пригодиться режим совместимости с диалектом Spark, а кому нужна история изменений на стороне базы, триггеры.

Как это применить

Альфу можно поставить для командной строки одной строкой: curl https://install.duckdb.org | DUCKDB_VERSION=alpha bash, затем проверить версию запросом SELECT version(); остальные клиенты, на странице установки DuckDB. Асинхронное чтение по S3 включено по умолчанию, отдельно настраивать его не нужно; параметр read_ahead_depth = 0 возвращает поведение 1.5, что удобно для сравнения. Для рекурсивных запросов держите иерархию в одной таблице родитель, потомок с целочисленными идентификаторами, а USING KEY используйте, когда нужно нести значение вроде глубины или стоимости. Для JSON храните события с согласованным набором полей и согласованными типами значений как VARIANT, поля, которые трогает каждый запрос, переносите в настоящие столбцы (два оператора: ALTER TABLE ... ADD COLUMN и UPDATE), а приведения списков в горячих запросах пока избегайте. Для лимита памяти пробуйте external_file_cache_spill = true.

Можно ли доверять

Это практический обзор в блоге MotherDuck: автор не указан, а сама MotherDuck, по его словам, будет поддерживать 2.0 ближе к выходу релиза. Все цифры получены на одном ноутбуке M5 и домашнем интернете, и автор сам просит прогонять свои замеры, прежде чем на них ссылаться, поэтому они ориентировочные. Абсолютное время запросов к S3 в тексте не приведено, только диапазон 2-3 раза; для рекурсивных запросов результаты замера анонсированы, но цифр в тексте нет, а 40 раз, это заявление команды DuckDB, а не собственный замер автора. Результатов на другом оборудовании и в облачных вычислениях нет. Работа идёт на альфа-версии.

Риски и подводные камни

Это альфа: автор не заявляет о готовности к промышленному использованию, а медленное приведение списков VARIANT он описывает как особенность именно этой альфы. Асинхронное чтение не помогает на россыпи тысяч мелких файлов Parquet: время уходит на обращения за каждым файлом, и 2.0 такую раскладку не спасёт. Рекурсивные запросы почти не выиграют на неглубоких иерархиях. Для VARIANT важна согласованность типов: поле, которое в одной строке число, а в другой строка, попадает в двоичный остаток и работает медленнее. Скорость по S3 зависит от сети: на домашнем канале автор замерял обе версии, и она замедляет обе примерно одинаково, а в облачных вычислениях ожидается быстрее.

«Все цифры ниже получены на одной машине (ноутбук M5) и моём домашнем интернете, который замедляет обе версии примерно одинаково. Прогоните свои замеры, прежде чем на них ссылаться ;)»

— автор поста в блоге MotherDuck