Общие сведения
SELECT.
В отличие от транзакционных баз данных, таких как Postgres, materialized view в ClickHouse — это всего лишь trigger, который выполняет запрос над blocks данных по мере их вставки в таблицу. Результат этого запроса вставляется во вторую, “целевую”, таблицу. При вставке новых строк результаты снова отправляются в целевую таблицу, где промежуточные результаты обновляются и объединяются. Этот объединённый результат эквивалентен выполнению запроса по всем исходным данным.
Основная причина использовать Materialized Views заключается в том, что результаты, вставляемые в целевую таблицу, представляют собой итог aggregation, фильтрации или преобразования строк. Эти результаты часто занимают меньше места, чем исходные данные (в случае aggregations — в виде частичного sketch). Это, наряду с тем, что итоговый запрос для чтения результатов из целевой таблицы остаётся простым, обеспечивает более быстрое выполнение запросов, чем если бы те же вычисления выполнялись на исходных данных, перенося вычисления (а значит, и latency запроса) с query time на insert time.
Materialized Views в ClickHouse обновляются в реальном времени по мере поступления данных в таблицу, на которой они основаны, и работают скорее как постоянно обновляемые indexes. Это отличается от других баз данных, где Materialized Views обычно представляют собой статические snapshots запроса, которые необходимо refresh (аналогично ClickHouse Refreshable Materialized Views).
Пример
toStartOfDay:
SELECT над данными, вставленными в votes, и записывает результаты в up_down_votes_per_day:
TO здесь является ключевой: она указывает, куда будут направлены результаты, то есть в up_down_votes_per_day.
Мы можем заново заполнить нашу таблицу votes с помощью ранее выполненной вставки:
up_down_votes_per_day — должна быть 1 строка на каждый день:
votes) до 5000, сохранив результат нашего запроса. Однако ключевой момент в том, что при вставке новых голосов в таблицу votes новые значения будут отправляться в up_down_votes_per_day для соответствующего дня, где они будут автоматически асинхронно сливаться в фоновом режиме, — так что для каждого дня будет сохраняться только одна строка. Таким образом, up_down_votes_per_day всегда будет оставаться небольшой и актуальной.
Поскольку слияние строк выполняется асинхронно, в момент запроса у пользователя для одного дня может оказаться больше одной записи о голосе. Чтобы гарантировать, что все ещё не слитые строки будут объединены во время выполнения запроса, у нас есть два варианта:
- Использовать модификатор
FINALв имени таблицы. Именно так мы поступили для запроса count выше. - Выполнить агрегацию по ключу упорядочивания, используемому в нашей итоговой таблице, то есть
CreationDate, и просуммировать метрики. Обычно это эффективнее и гибче (таблицу можно использовать и для других задач), но первый вариант может быть проще для некоторых запросов. Ниже мы покажем оба:
Более сложный пример
Score и среднее значение CommentCount. Запрос для этого может выглядеть так:
posts.
В качестве примера и чтобы не загружать данные постов из S3, мы создадим дубликат таблицы posts_null с той же схемой, что и у posts. Однако эта таблица не будет хранить данные и будет использоваться только materialized view при вставке строк. Чтобы данные не сохранялись, мы можем использовать тип движка таблицы Null.
/dev/null. Наша materialized view будет вычислять и сохранять сводную статистику, когда в таблицу posts_null при вставке поступают строки, — это лишь триггер. Однако исходные данные сохраняться не будут. Хотя в нашем случае мы, вероятно, всё же хотим хранить исходные посты, этот подход можно использовать для вычисления агрегатов, избегая накладных расходов на хранение исходных данных.
Таким образом, materialized view принимает следующий вид:
State в конец агрегатных функций. Это гарантирует, что будет возвращаться состояние агрегатной функции, а не итоговый результат. Оно содержит дополнительную информацию, которая позволяет этому промежуточному состоянию объединяться с другими состояниями. Например, в случае среднего значения это будут количество и сумма значений в столбце.
Промежуточные состояния агрегации необходимы для вычисления корректных результатов. Например, при вычислении среднего значения простое усреднение средних по поддиапазонам даёт некорректный результат.Теперь создадим целевую таблицу для этого представления
post_stats_per_day, в которой хранятся эти промежуточные состояния агрегации:
SummingMergeTree было достаточно для хранения счётчиков, для других функций нам нужен более продвинутый тип движка: AggregatingMergeTree.
Чтобы ClickHouse понимал, что будут храниться состояния агрегатных функций, мы задаём для Score_quantiles и AvgCommentCount тип AggregateFunction, указывая функцию, из которой формируются частичные состояния, и тип их исходных столбцов. Как и в SummingMergeTree, строки с одинаковым значением ключа ORDER BY будут объединены (Day в примере выше).
Чтобы заполнить post_stats_per_day через нашу materialized view, мы можем просто выполнить вставку всех строк из posts в posts_null:
В рабочей среде вы, вероятно, прикрепили бы materialized view к таблицеВ итоговом запросе нужно использовать суффиксposts. Здесь мы использовалиposts_null, чтобы продемонстрировать null-таблицу.
Merge для наших функций (поскольку в столбцах хранятся промежуточные состояния агрегации):
GROUP BY вместо FINAL.
Другие применения
Фильтрация и преобразование
posts_null, а запрос SELECT будет фильтровать строки перед их вставкой в таблицу posts. Например, предположим, что мы хотим преобразовать столбец Tags в таблице posts. Он содержит список имён тегов, разделённых символом вертикальной черты. Преобразовав его в массив, мы сможем проще выполнять агрегацию по отдельным значениям тегов.
Это преобразование можно выполнить при запускеНаша materialized view для этого преобразования показана ниже:INSERT INTO SELECT. Однако materialized view позволяет инкапсулировать эту логику в DDL ClickHouse и сохранитьINSERTпростым, при этом преобразование будет автоматически применяться ко всем новым строкам.
Таблица поиска
comments:
PostId.
Предположим, пользователь хочет отфильтровать данные по определённому UserId и вычислить среднее значение Score:
PostId для фильтрации по столбцу UserId. Затем эти значения можно использовать для эффективного поиска.
В этом примере наша materialized view может быть очень простой и выбирать только PostId и UserId из comments при вставке. Эти результаты, в свою очередь, отправляются в таблицу comments_posts_users, которая упорядочена по UserId. Ниже мы создаём версию таблицы Comments с движком Null и используем её для заполнения нашего представления и таблицы comments_posts_users:
Цепочки и каскады materialized views
Materialized views и JOINs
Refreshable Materialized ViewsНижеописанное относится только к Incremental Materialized Views. Refreshable Materialized Views периодически выполняют свой запрос по всему целевому набору данных и полностью поддерживают JOINs. Рассмотрите их использование для сложных JOINs, если допустимо некоторое снижение актуальности результатов.
JOIN, но с одним важным ограничением: materialized view срабатывает только при вставке в исходную таблицу (крайнюю левую таблицу в запросе). Таблицы в правой части JOIN не вызывают обновление, даже если их данные изменяются. Это особенно важно при создании Incremental Materialized Views, где данные агрегируются или преобразуются во время вставки.
Если Incremental materialized view определяется с использованием JOIN, источником выступает крайняя левая таблица в запросе SELECT. Когда в эту таблицу вставляются новые строки, ClickHouse выполняет запрос materialized view только для этих вновь вставленных строк. Таблицы в правой части JOIN при этом читаются целиком, но изменения только в них не запускают view.
Из-за этого JOINs в Materialized Views напоминают JOIN со снимком по статическим таблицам измерений.
Это хорошо подходит для обогащения данных справочными таблицами или таблицами измерений. Однако любые обновления таблиц в правой части (например, метаданных пользователей) не обновят materialized view задним числом. Чтобы увидеть обновленные данные, в исходную таблицу должны поступить новые вставки.
Пример
users.
Напомним, что схемы наших таблиц выглядят следующим образом:
users уже заполнена:
Согласование группировки и сортировкиВ предложении
GROUP BY в materialized view должны присутствовать DisplayName, UserId и Day, чтобы оно соответствовало ORDER BY в целевой таблице SummingMergeTree. Это гарантирует, что строки будут корректно агрегироваться и объединяться при слиянии. Если опустить любой из этих элементов, это может привести к неверным результатам или неэффективным слияниям.daily_badges_by_user.
Рекомендации по использованию JOIN в materialized view
-
Используйте крайнюю левую таблицу как триггер. Только таблица в левой части оператора
SELECTзапускает materialized view. Изменения в таблицах справа не вызовут обновления. - Сначала вставляйте данные для JOIN. Убедитесь, что данные в присоединяемых таблицах существуют до вставки строк в исходную таблицу. JOIN вычисляется во время вставки, поэтому отсутствие данных приведёт к несовпадающим строкам или значениям NULL.
- Ограничивайте число столбцов, получаемых через JOIN. Выбирайте из присоединяемых таблиц только необходимые столбцы, чтобы минимизировать использование памяти и снизить задержку при вставке (см. ниже).
- Оценивайте производительность вставки. JOIN увеличивает стоимость вставок, особенно при больших таблицах справа. Выполняйте бенчмарк скорости вставки на репрезентативных производственных данных.
- Для простых поисков предпочитайте словари. Используйте Dictionaries для поиска по модели ключ-значение (например, сопоставления ID пользователя с именем), чтобы избежать дорогостоящих операций JOIN.
-
Согласовывайте
GROUP BYиORDER BYдля эффективного слияния. При использованииSummingMergeTreeилиAggregatingMergeTreeубедитесь, чтоGROUP BYсоответствуетORDER BYв целевой таблице, чтобы обеспечить эффективное слияние строк. - Используйте явные псевдонимы столбцов. Если в таблицах есть совпадающие имена столбцов, используйте псевдонимы, чтобы избежать неоднозначности и обеспечить корректные результаты в целевой таблице.
- Учитывайте объём и частоту вставок. JOIN хорошо работает при умеренной нагрузке на вставку. Для высокопроизводительной ингестии рассмотрите использование staging-таблиц, предварительных JOIN или других подходов, таких как Dictionaries и Refreshable Materialized Views.
Использование исходной таблицы в фильтрах и JOIN
Пример сценария
Объяснение
mvw1 и mvw2, — которые выполняют схожие операции, но немного различаются тем, как в них задаётся ссылка на исходную таблицу t0.
В mvw1 таблица t0 указывается напрямую внутри подзапроса (SELECT * FROM t0) в правой части JOIN. Когда в t0 выполняется вставка данных, запрос materialized view исполняется так, что t0 заменяется вставленным блоком данных. Это означает, что операция JOIN выполняется только для вновь вставленных строк, а не для всей таблицы.
Во втором случае, при JOIN с vt0, представление читает все данные из t0. Благодаря этому операция JOIN учитывает все строки в t0, а не только вновь вставленный блок.
Ключевое различие заключается в том, как ClickHouse обрабатывает исходную таблицу в запросе materialized view. Когда materialized view срабатывает при вставке, исходная таблица (t0 в данном случае) заменяется вставленным блоком данных. Это поведение можно использовать для оптимизации запросов, но его также нужно учитывать, чтобы избежать неожиданных результатов.
Сценарии использования и предостережения
IN (SELECT id FROM t0), содержит только что вставленные строки, что помогает фильтровать по нему t1.
Пример со Stack Overflow
users.
badges, например:
users, используя идентификаторы пользователей из вставленных строк badge:
badge будут выполняться эффективно:
2936484 извлекается только одна строка. Эта выборка также оптимизирована благодаря ключу упорядочивания таблицы Id.
materialized views и объединения
UNION ALL обычно используются для объединения данных из нескольких исходных таблиц в один результирующий набор.
Хотя UNION ALL напрямую не поддерживается в incremental materialized views, того же результата можно добиться, создав отдельную materialized view для каждой ветви SELECT и записывая результаты в общую целевую таблицу.
В нашем примере мы будем использовать набор данных Stack Overflow. Рассмотрим приведённые ниже таблицы badges и comments, которые содержат сведения о значках, полученных пользователем, и комментариях, которые он оставляет к постам:
INSERT INTO:
badges или comments, наивный подход — попытаться создать materialized view на основе предыдущего union-запроса:
comments. Например:
badges не будут активировать представление, поэтому user_activity не будет обновляться:
comments:
badges отражаются в таблице user_activity:
Параллельная и последовательная обработка
parallel_view_processing.
По умолчанию эта настройка имеет значение 0 (false), то есть materialized view выполняются последовательно в порядке uuid.
Например, рассмотрим следующую таблицу source и 3 materialized view, каждая из которых отправляет строки в таблицу target:
target, а также добавляет своё имя и время вставки.
Вставка строки в таблицу source занимает ~3 секунды, при этом каждое представление выполняется последовательно:
SELECT подтвердить, что строки поступают из каждой строки:
uuid представлений:
parallel_view_processing=1. Когда этот параметр включен, представления выполняются параллельно, поэтому порядок, в котором строки поступают в целевую таблицу, не гарантируется:
Когда использовать параллельную обработку
parallel_view_processing=1 может значительно повысить пропускную способность вставки, как показано выше, особенно если к одной таблице подключено несколько Materialized Views. Однако важно понимать связанные с этим компромиссы:
- Повышенная нагрузка при вставке: все Materialized Views выполняются одновременно, что увеличивает использование CPU и памяти. Если каждое представление выполняет ресурсоёмкие вычисления или JOIN, это может перегрузить систему.
- Необходимость строгого порядка выполнения: в редких сценариях, где порядок выполнения представлений имеет значение (например, при цепочке зависимостей), параллельное выполнение может привести к несогласованному состоянию или race condition. Хотя такие конфигурации возможны, они хрупки и могут перестать работать в будущих версиях.
Исторические значения по умолчанию и стабильностьПоследовательное выполнение долгое время использовалось по умолчанию, отчасти из-за сложности обработки ошибок. Исторически сбой в одном materialized view мог помешать выполнению остальных. В более новых версиях это улучшили за счёт изоляции сбоев на уровне каждого блока, но последовательное выполнение по-прежнему даёт более понятную семантику ошибок.
parallel_view_processing=1, если:
- У вас есть несколько независимых Materialized Views
- Вы хотите добиться максимальной производительности вставки
- Вы понимаете, достаточно ли у системы ресурсов для параллельного выполнения представлений
- Materialized Views зависят друг от друга
- Вам нужен предсказуемый порядок выполнения
- Вы занимаетесь отладкой или аудитом поведения вставки и хотите детерминированного повторного воспроизведения
materialized views и общие табличные выражения (CTE)
Общие табличные выражения не материализуютсяClickHouse не материализует CTE; вместо этого он напрямую подставляет определение CTE в запрос, из-за чего одно и то же выражение может вычисляться несколько раз (если CTE используется более одного раза).
- Если ваш CTE ссылается на таблицу, отличную от исходной таблицы (то есть той, к которой привязано materialized view), и используется в
JOINилиIN, он будет вести себя как подзапрос или JOIN, а не как триггер. - Materialized view по-прежнему будет срабатывать только при вставках в основную исходную таблицу, но CTE будет выполняться заново при каждой вставке, что может создавать лишние накладные расходы, особенно если таблица, на которую он ссылается, большая.
users пересчитывается при каждой вставке в posts, а materialized view не будет обновляться при добавлении новых пользователей — только при вставке в posts.
Как правило, CTE стоит использовать для логики, которая работает с той же исходной таблицей, к которой привязана materialized view, либо нужно убедиться, что используемые таблицы небольшие и вряд ли создадут узкие места по производительности. В качестве альтернативы рассмотрите те же оптимизации, что и для JOIN в Materialized Views.