Группировка по строкам через целочисленные ключи

Группировка по строкам через целочисленные ключи

 

DuckDB предлагает переносить повторяющиеся длинные строки в отдельную таблицу измерений с узкими целочисленными ключами. Агрегация выполняется только по этим ключам, а строки подставляются уже на финальном этапе. Такой приём снижает объём работы хеш-таблицы и уменьшает расход памяти при группировках с высокой кардинальностью. Перед выбором метода важно понимать, что таблица измерений — это справочник уникальных значений, а кардинальность — это количество уникальных элементов в столбце. Если кардинальность низкая, выигрыш от замены строк на числа будет небольшим, а если высокая — подготовка данных может оказаться слишком трудоёмкой.

Подход особенно заметен на данных, где одни и те же названия, метки или идентификаторы встречаются тысячи раз. Вместо повторного хеширования и сравнения переменных строк процессор работает с фиксированными целыми числами, что ускоряет как CPU, так и доступ к кешу. При принятии решения стоит оценить, насколько часто выполняются именно такие группировки: если запросы с GROUP BY по строкам редки, то затраты на создание и поддержку справочника могут не окупиться.

 

Почему строки замедляют агрегацию

В DuckDB группировка строится на хеш-агрегации. Для каждого ключа вычисляется хеш, затем происходит поиск и сравнение в хеш-таблице. Хеш-агрегация — это метод, при котором значения ключей хешируются для быстрого размещения в таблице, но при длинных строках этот процесс становится тяжёлым. Короткие строки до 12 байт хранятся прямо в структуре, но большинство реальных меток длиннее. В этом случае DuckDB читает префикс, переходит по указателю и сравнивает полные значения. Сравнение строк требует посимвольного анализа и дополнительных обращений к памяти, что увеличивает время обработки.

Каждый раз при обработке строки выполняются те же операции: хеширование всех байтов, копирование при создании новой группы и сравнение при совпадении. Когда строк много и они длинные, хеш-таблица растёт быстрее, хуже помещается в кеш и раньше начинает сбрасывать данные на диск при превышении лимита памяти. Словарное кодирование на диске не помогает, потому что при чтении в агрегат строки снова становятся полноценными значениями. Целочисленные ключи лишены этих издержек. Их ширина фиксирована, сравнение занимает несколько инструкций, а при малом диапазоне значений оптимизатор может выбрать идеальную хеш-таблицу без хеширования вовсе. При решении о применении метода нужно учитывать объём доступной памяти: если сервер часто упирается в лимиты, переход на числа даёт заметное преимущество.

 

Построение таблицы измерений

Сначала оценивают кардинальность строкового столбца. Полученное число определяет минимальную ширину ключа: tinyint хватает до 255 значений, smallint — до 32 тысяч, integer — до двух миллиардов. Ключи присваивают после сортировки, чтобы сохранить детерминированный порядок. Выбор ширины ключа важен для экономии памяти: слишком широкие числа (например, bigint вместо smallint) увеличивают размер таблицы фактов без необходимости.

Затем создают таблицу измерений, где каждой уникальной строке соответствует свой ключ. Основная таблица фактов остаётся без изменений или получает дополнительный столбец с ключом. Агрегацию выполняют по этому столбцу, а строки присоединяют только в самом конце запроса. Такая схема повторяет классическую звёздную модель, но применяется здесь ради скорости, а не ради нормализации. Перед внедрением стоит взвесить, будет ли таблица измерений обновляться часто: если исходные строки меняются редко, поддержка справочника проста; при частых изменениях возникают дополнительные затраты на пересчёт ключей.

 

Применение в российских проектах

В отечественных системах часто встречаются логи, телеметрия транспорта, данные маркетплейсов и биллинга, где одни и те же названия станций, категорий товаров или статусов повторяются миллионы раз. Переход на узкие ключи позволяет обрабатывать такие таблицы на серверах с ограниченной памятью без частых сбросов на диск. DuckDB доступен как открытая библиотека и не требует внешних сервисов, поэтому схема работает в изолированных контурах. При этом важно заранее оценить кардинальность: если уникальных строк больше нескольких десятков тысяч, выигрыш от narrow keys снижается и приходится выбирать между integer и bigint. Дополнительный join на финальном этапе добавляет свою стоимость, поэтому подход оправдан при многократных агрегациях по одному и тому же столбцу. Решая, стоит ли применять метод, следует проверить, как часто один и тот же столбец используется в GROUP BY: при единичных запросах overhead от join может превысить выгоду.

 

Когда эффект заметен и какие ограничения

Прирост скорости наиболее очевиден на запросах с GROUP BY по строкам длиннее 12 байт и кардинальностью от сотен до десятков тысяч. В таких случаях сокращается как время хеширования, так и объём копируемых данных. Если же строки короткие или кардинальность низкая, накладные расходы на join могут нивелировать выгоду. Метод не меняет семантику запроса, но требует дополнительной подготовки данных и поддержки таблицы измерений. При обновлении исходных строк ключи нужно пересчитывать или использовать механизм инкрементального обновления. В DuckDB 1.1 и новее оптимизатор уже учитывает статистику по диапазону ключей, поэтому ручная настройка perfect_ht_threshold обычно не требуется. Ограничением является необходимость контролировать целостность: при добавлении новых уникальных строк таблица измерений должна обновляться синхронно, иначе ключи потеряют смысл.

Подробности и примеры запросов приведены в оригинальной публикации DuckDB Faster String Aggregations with Dimension Tables.

 

Источник

Это краткий разбор материала DuckDB. Полная версия с примерами кода, схемами и деталями реализации — в оригинале: Группировка по строкам через целочисленные ключи.

Курс по теме — Каталог курсов Школы Больших Данных

 

Архитектура Данных

Код курса
ARMG
Ближайшая дата курса
21 декабря, 2026
Продолжительность
24 ак.часов
Стоимость обучения
76 800