Learning Platform
Глоссарий Troubleshooting
Урок 06.06 · 20 мин
Средний
dictGetdictGetOrDefaultDirect JoinJOINdimension enrichmentjoin_algorithmDictionary

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 vs dictGet(): путь выполнения
Dimension Enrichment: два подходаДва пути обогащения фактов dimension-атрибутами: классический JOIN (build + probe на каждый запрос) и dictGet() (pre-loaded lookup в памяти)
JOIN: build hash tableJOIN: на каждый запрос ClickHouse читает правую таблицу с диска, строит hash-таблицу в памяти, затем probes каждую строку левой таблицы. Стоимость: O(R) build + O(L) probe на каждый запрос.
Probe: lookup per rowProbe: для каждой строки events ClickHouse ищет country_code в hash-таблице. При 100M events -- 100M lookups в freshly-built hash table.
Result (hash table discarded)Результат: обогащённые строки. Hash-таблица уничтожается после запроса. Следующий запрос rebuild с нуля.
dictGet(): pre-loaded in RAMdictGet(): словарь загружен в RAM при старте или по LIFETIME. Никакого build на запрос -- структура уже готова. Стоимость build: O(0) per query.
Lookup: O(1) per rowLookup: O(1) per row в готовой hash-таблице (или массиве для flat). Та же сложность, что probe в JOIN, но без overhead build.
Result (dict stays in RAM)Результат: обогащённые строки. Словарь остаётся в памяти для следующих запросов. Zero rebuild cost.

Ключевое различие:

  • 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.

TIP

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;

Ключевые выводы

  1. dictGet(‘dict’, ‘attr’, key) — O(1) lookup в pre-loaded словаре. Заменяет JOIN с lookup-таблицей для dimension enrichment.
  2. dictGetOrDefault — безопасный вариант с default-значением для missing keys (аналог LEFT JOIN с COALESCE).
  3. Direct Join (join_algorithm='direct') — до 25x быстрее hash join. SQL JOIN синтаксис + dictionary execution path.
  4. JOIN rebuild hash table на каждый запрос, dictGet() использует pre-loaded структуру. Критическая разница при высоком QPS.
  5. dictGet() для: dimension enrichment, маленькие справочники, повторяющиеся запросы. JOIN для: ad-hoc, большие таблицы, сложные conditions.
Dimension table: денормализованные описательные атрибуты Hash Join: build, probe и batching при нехватке памяти

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Dashboard выполняет 100 запросов/сек, каждый обогащает events названием страны через LEFT JOIN countries. Таблица countries -- 250 записей. Какой подход оптимален?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7