JOIN связывает строки двух наборов по условию. Его используют, например, чтобы дополнить события названием категории или владельцем источника. Dictionary готовит доступ к атрибутам по ключу. Оба механизма могут решать задачу обогащения, но имеют разную семантику и стоимость.
Кратность соединения
Логический тип JOIN определяет результат: INNER возвращает совпадения, LEFT и RIGHT сохраняют также несовпавшие строки выбранной стороны, FULL — обеих сторон. CROSS строит все пары строк. Физический алгоритм определяет, как найти этот результат; одинаковый тип JOIN может выполняться разными алгоритмами.
Пусть events содержит event_id и source, а source_dim — source и team:
SELECT e.event_id, e.source, d.team
FROM events AS e
LEFT ALL JOIN source_dim AS d ON e.source = d.source;ALL сохраняет все совпадения. Если справа две строки для одного source, каждая строка этого источника слева даст две строки результата. Последующая сумма может удвоиться. Сортировочный ключ справочника не гарантирует уникальность.
ANY оставляет одно совпадение, когда задача требует один атрибут на ключ. Он не обещает последнюю версию по времени. Версионный справочник сначала нужно привести к явно выбранному состоянию. SEMI проверяет наличие совпадения, ANTI — его отсутствие; они не добавляют все строки правой стороны.
Для LEFT JOIN без совпадения по умолчанию возможны значения по умолчанию, например пустая строка. join_use_nulls = 1 возвращает NULL для отсутствующей стороны. Условие WHERE team IS NULL имеет смысл только с учётом этого поведения.
NULL в ключе при обычном соединении по равенству не совпадает с NULL другого набора. join_use_nulls меняет заполнение отсутствующей стороны результата, но не превращает NULL = NULL в истину. Если задача требует считать неизвестные ключи совпадающими, это задают явно и проверяют кратность: много неизвестных ключей с обеих сторон может дать очень большой результат.
JOIN по неравенству
ClickHouse поддерживает условия по неравенству между таблицами. Например, события можно связать с интервалами одного объекта:
SELECT e.object_id, e.event_time, p.label
FROM events AS e
LEFT ALL JOIN periods AS p
ON e.object_id = p.object_id
AND e.event_time >= p.valid_from
AND e.event_time < p.valid_to;Здесь events содержит object_id и event_time, а periods — object_id, границы valid_from/valid_to и label. Полуоткрытый интервал исключает двойное совпадение на общей границе соседних периодов. Пересекающиеся интервалы всё ещё способны размножить строки.
В учебной версии 25.8 такие неравенства поддерживают hash и grace_hash; они несовместимы с join_use_nulls. Для другой версии проверяют поддержку выбранного алгоритма и настроек.
ASOF JOIN решает отдельную задачу: выбрать ближайшее совпадение по упорядоченному полю вместе с равенствами по другим ключам. Он не эквивалентен ALL JOIN со всеми подходящими интервалами.
Алгоритмы и ресурсы
Hash join строит хеш-таблицу одной стороны и ищет совпадения для другой. Хеш определяет позицию поиска, после чего сравниваются сами ключи. При хорошем распределении поиск в среднем близок к постоянному времени. Parallel hash распараллеливает построение, grace hash делит работу на порции с использованием диска.
Full sorting merge сортирует входы и соединяет упорядоченные последовательности. Если физический порядок совместим с ключом соединения, сортировку может удаваться пропустить. Partial merge использует другой компромисс сортировки, памяти и поддерживаемых типов JOIN. Direct join обращается к готовой структуре lookup, например словарю, при совместимой семантике.
Алгоритм выбирают с учётом поддерживаемого условия, размера сторон, числа ключей и кратности. Фильтры и необходимые колонки сокращают структуру hash join. Большая кратность увеличивает сам результат. Выбор join_algorithm проверяют по фактическому выполнению; для sorting merge важен совместимый порядок входов.
Dictionary: ключ и атрибуты
Словарь получает данные из источника и хранит структуру поиска. Например, для настроенного словаря source_dict со строковым ключом и атрибутом team:
SELECT dictGet('source_dict', 'team', tuple('api')) AS team;Для строковых составных ключей подходит complex-key layout. Hash-подобный layout загружает данные в память; другие layouts меняют способ хранения и получения значений. LIFETIME задаёт обновление, поэтому новое значение источника не обязательно видно сразу. При lookup на нескольких узлах словарь и необходимые ресурсы должны быть доступны на каждом из них.
Для отсутствующего ключа выбирают default, dictGetOrDefault или проверку dictHas. Словарь обычно отражает актуальный снимок и сам не сохраняет историю атрибутов. Если нужен владелец на момент события, его можно зафиксировать при загрузке или использовать исторический справочник.
В распределённом JOIN также важна доставка правой стороны. GLOBAL для LEFT/INNER собирает её у инициатора и отправляет участвующим узлам. Большой набор увеличивает сеть и память на узлах; локальное соединение требует, чтобы нужные данные были доступны там, где оно выполняется.