dictGet() вместо JOIN
Словари загружаются в память и обеспечивают O(1) point-lookup по ключу. Функция dictGet() — основной интерфейс для доступа к словарю из SQL-запросов. Она заменяет JOIN с lookup-таблицей, устраняя необходимость build hash table на каждый запрос.
dictGet(): синтаксис и варианты
-- Базовый синтаксис: словарь, атрибут, ключ
SELECT dictGet('country_dict', 'country_name', country_code) AS country
FROM events;
-- Несколько атрибутов за один вызов
SELECT
dictGet('country_dict', 'country_name', country_code) AS country,
dictGet('country_dict', 'continent', country_code) AS continent
FROM events;
-- dictGetOrDefault: значение по умолчанию для отсутствующих ключей
SELECT dictGetOrDefault('country_dict', 'country_name', country_code, 'Unknown') AS country
FROM events;
-- Типизированные варианты (устаревшие, но встречаются в legacy-коде)
-- dictGetString, dictGetUInt64, dictGetFloat64 ...
-- Рекомендуется: dictGet с автоматическим приведением типа
Параметры dictGet:
- dict_name (String) — имя словаря (как в CREATE DICTIONARY)
- attr_name (String) — имя атрибута (столбца) для извлечения
- key — значение ключа (должно соответствовать PRIMARY KEY словаря)
dictGetOrDefault — безопасный вариант. Если ключ не найден в словаре, возвращает default-значение вместо нуля/пустой строки. Критично для LEFT JOIN семантики, где not-matched строки должны получить осмысленный fallback.
JOIN подход vs dictGet() подход
Классический JOIN
SELECT
e.event_id,
e.amount,
c.country_name,
c.continent
FROM events e
LEFT JOIN countries c ON e.country_code = c.code;
Что происходит: ClickHouse строит hash-таблицу из правой таблицы (countries) в памяти, затем для каждой строки левой таблицы (events) выполняет probe в hash-таблицу. Hash-таблица перестраивается на каждый запрос — даже если данные countries не менялись.
dictGet() подход
-- Предварительно: CREATE DICTIONARY country_dict ...
SELECT
event_id,
amount,
dictGet('country_dict', 'country_name', country_code) AS country_name,
dictGet('country_dict', 'continent', country_code) AS continent
FROM events;
Что происходит: Словарь country_dict уже загружен в память. Каждый вызов dictGet() — O(1) lookup в готовой структуре данных. Нет build hash table, нет probe, нет rebuild на каждый запрос.
Почему dictGet() быстрее JOIN
Ключевое различие:
- JOIN перестраивает hash-таблицу на каждый запрос. 100 запросов/сек к dashboard = 100 build hash table/сек.
- dictGet() использует одну pre-loaded структуру. 100 запросов/сек = 100 lookup серий, zero rebuild.
Direct Join: join_algorithm=‘direct’
ClickHouse позволяет использовать словарь как правую сторону JOIN напрямую, через настройку join_algorithm:
SET join_algorithm = 'direct';
SELECT
e.event_id,
e.amount,
c.country_name
FROM events e
LEFT JOIN country_dict c ON e.country_code = c.code;
Direct Join использует словарь вместо построения hash-таблицы. Результат: до 25x быстрее обычного hash join для dimension enrichment. Синтаксис остаётся стандартным SQL JOIN, но execution path идёт через dictionary lookup.
Direct Join — компромисс: SQL JOIN синтаксис (привычный, портируемый) + производительность dictGet() (pre-loaded lookup). Подходит для миграции существующих JOIN-запросов на словари без рефакторинга SQL.
Когда использовать dictGet()
dictGet() оптимален:
- Dimension enrichment: обогащение фактов атрибутами (user_id -> name, ip -> country)
- Маленькие lookup-таблицы (до 10M записей), помещающиеся в RAM
- Повторяющиеся запросы к одним и тем же справочникам (dashboards, reports)
- Высокая частота запросов (100+ QPS) — zero rebuild cost
JOIN остаётся предпочтительным:
- Ad-hoc запросы к разным таблицам (нет смысла создавать словарь для одноразового запроса)
- Большие правые таблицы (миллиарды строк), не помещающиеся в RAM
- Сложные join conditions (
ON a.x = b.x AND a.y > b.y) — dictGet() поддерживает только equality lookup - Multi-table JOIN chains (3+ таблиц) — dictGet() не поддерживает цепочки
Паттерн: от JOIN к dictGet()
Шаг 1: Идентифицировать lookup-таблицу (маленькая, стабильная, часто используемая в JOIN):
-- Было: JOIN каждый запрос
SELECT e.*, c.country_name
FROM events e
LEFT JOIN countries c ON e.country_code = c.code;
Шаг 2: Создать словарь:
CREATE DICTIONARY country_dict (
code String,
country_name String
)
PRIMARY KEY code
SOURCE(CLICKHOUSE(TABLE 'countries' DB 'default'))
LAYOUT(HASHED())
LIFETIME(MIN 300 MAX 600);
Шаг 3: Заменить JOIN на dictGet():
-- Стало: dictGet() lookup
SELECT
*,
dictGet('country_dict', 'country_name', country_code) AS country_name
FROM events;
Или использовать Direct Join для минимального рефакторинга:
SET join_algorithm = 'direct';
SELECT e.*, c.country_name
FROM events e
LEFT JOIN country_dict c ON e.country_code = c.code;
Ключевые выводы
- dictGet(‘dict’, ‘attr’, key) — O(1) lookup в pre-loaded словаре. Заменяет JOIN с lookup-таблицей для dimension enrichment.
- dictGetOrDefault — безопасный вариант с default-значением для missing keys (аналог LEFT JOIN с COALESCE).
- Direct Join (
join_algorithm='direct') — до 25x быстрее hash join. SQL JOIN синтаксис + dictionary execution path. - JOIN rebuild hash table на каждый запрос, dictGet() использует pre-loaded структуру. Критическая разница при высоком QPS.
- dictGet() для: dimension enrichment, маленькие справочники, повторяющиеся запросы. JOIN для: ad-hoc, большие таблицы, сложные conditions.