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

Один из распространенных методов, популяризированных решениями NoSQL, заключается в денормализации данных в отсутствие поддержки JOIN
, фактически храня все статистики или связанные строки в родительской строке в виде колонок и вложенных объектов. Например, в примере схемы блога мы можем хранить все Comments
в виде Array
объектов на соответствующих постах.
Когда использовать денормализацию
В общем, мы рекомендуем денормализовать в следующих случаях:
- Денормализовать таблицы, которые изменяются нечасто или для которых допустима задержка, прежде чем данные станут доступны для аналитических запросов, т.е. данные могут быть полностью перезагружены批量.
- Избегать денормализации отношений многие-ко-многим. Это может привести к необходимости обновления многих строк, если изменится одна исходная строка.
- Избегать денормализации отношений с высокой кардинальностью. Если каждая строка в таблице имеет тысячи связанных записей в другой таблице, их необходимо будет представлять в виде
Array
— либо примитивного типа, либо кортежей. Обычно не рекомендуется использовать массивы с более чем 1000 кортежами. - Вместо того чтобы денормализовать все колонки как вложенные объекты, рассмотрите возможность денормализации только одной статистики с использованием материализованных представлений (см. ниже).
Не вся информация должна быть денормализована — только ключевая информация, которая должна быть часто доступна.
Работы по денормализации могут выполняться как в ClickHouse, так и на стороне источника, например, с использованием Apache Flink.
Избегайте денормализации часто обновляемых данных
Для ClickHouse денормализация является одним из нескольких вариантов, которые пользователи могут использовать для оптимизации производительности запросов, но она должна использоваться осторожно. Если данные обновляются часто и должны обновляться в режиме близком к реальному времени, этот подход следует избегать. Используйте это, если основная таблица в значительной мере только добавляется или может периодически перезагружаться批量, например, ежедневно.
Как подход, он сталкивается с одной главной проблемой — производительностью записи и обновления данных. Более конкретно, денормализация фактически перемещает ответственность за соединение данных с времени выполнения запроса на время приема данных. Хотя это может значительно улучшить производительность запросов, это усложняет процесс вставки и означает, что конвейеры данных должны повторно вставлять строку в ClickHouse, если изменится любая из строк, которые использовались для ее составления. Это может означать, что изменение в одной исходной строке потенциально требует обновления многих строк в ClickHouse. В сложных схемах, где строки были составлены из сложных соединений, изменение одной строки во вложенном компоненте соединения может потенциально означать, что необходимо обновить миллионы строк.
Достичь этого в реальном времени часто нереалистично и требует значительных инженерных усилий из-за двух вызовов:
- Запуск правильных операторов соединения при изменении строки таблицы. Это должно, в идеале, не вызывать обновления всех объектов для соединения — а только тех, которые были затронуты. Модификация соединений с целью фильтрации на правильные строки эффективно и с высокой пропускной способностью требует внешних инструментов или инженерных решений.
- Обновление строк в ClickHouse необходимо тщательно управлять, что вносит дополнительные сложности.
Поэтому более распространен процесс пакетного обновления, при котором все денормализованные объекты периодически перезагружаются.
Практические случаи для денормализации
Рассмотрим несколько практических примеров, где денормализация может иметь смысл, и те случаи, где альтернативные подходы предпочтительнее.
Рассмотрим таблицу Posts
, которая уже была денормализована со статистикой, такой как AnswerCount
и CommentCount
— исходные данные предоставляются в этой форме. На практике мы можем захотеть действительно нормализовать эту информацию, так как она, вероятно, будет часто изменяться. Многие из этих колонок также доступны через другие таблицы, например, комментарии к посту доступны через колонку PostId
и таблицу Comments
. Для целей примера предположим, что посты перезагружаются в процессе пакетной загрузки.
Мы также рассматриваем только денормализацию других таблиц на Posts
, так как считаем это нашей основной таблицей для аналитики. Денормализация в другом направлении также была бы уместной для некоторых запросов, при этом применяются те же вышеуказанные соображения.
Для каждого из следующих примеров предположим, что существует запрос, который требует использования обеих таблиц в соединении.
Посты и Голоса
Голоса за посты представлены в качестве отдельных таблиц. Оптимизированная схема для этого представлена ниже, а также команда вставки для загрузки данных:
На первый взгляд, это могут быть кандидаты для денормализации в таблицу постов. Однако здесь есть несколько проблем с таким подходом.
Голоса часто добавляются к постам. Хотя это может уменьшаться на пост со временем, следующий запрос показывает, что у нас около 40k голосов в час на 30k постах.
Это можно решить пакетной загрузкой, если задержка допустима, но это все еще требует от нас обработки обновлений, если мы не будем периодически перезагружать все посты (что вряд ли будет желательно).
Еще более проблематично то, что некоторые посты имеют крайне высокое количество голосов:
Основное наблюдение здесь заключается в том, что агрегированные статистики голосов для каждого поста были бы достаточны для большинства анализов — нам не нужно денормализовать всю информацию о голосах. Например, текущая колонка Score
представляет собой такую статистику, т.е. всего голосов "за" минус голосов "против". В идеале мы просто хотели бы извлекать эту статистику во время запроса с помощью простого поиска (см. словарь).
Пользователи и Значки
Теперь рассмотрим наши Users
и Badges
:

Сначала вставим данные с помощью следующей команды:
Хотя пользователи могут часто получать значки, это вряд ли будет набором данных, который нужно обновлять чаще, чем раз в день. Связь между значками и пользователями является один-ко-многим. Возможно, нам просто стоит денормализовать значки на пользователей в виде списка кортежей? Хотя это возможно, быстрый анализ на количество значков на пользователя предполагает, что это не идеально:
Вероятно, нецелесообразно денормализовать 19k объектов на одну строку. Эту связь лучше оставить как отдельные таблицы или с добавленными статистиками.
Мы можем пожелать денормализовать статистику из значков на пользователей, т.е. количество значков. Мы рассматриваем такой пример при использовании словарей для этого набора данных на этапе вставки.
Посты и PostLinks
PostLinks
соединяет Posts
, которые пользователи считают связанными или дублированными. Следующий запрос показывает схему и команду загрузки:
Мы можем подтвердить, что у постов нет чрезмерного количества ссылок, что препятствует денормализации:
Аналогично, эти ссылки не являются событиями, которые происходят слишком часто:
Мы используем это как наш пример денормализации ниже.
Простой пример статистики
В большинстве случаев денормализация требует добавления одной колонки или статистики к родительской строке. Например, мы можем просто захотеть обогатить наши посты количеством дублирующих постов и нам просто нужно добавить колонку.
Для заполнения этой таблицы мы используем INSERT INTO SELECT
, соединяя нашу статистику дубликатов с нашими постами.
Использование сложных типов для отношений один-ко-многим
Для выполнения денормализации нам часто необходимо использовать сложные типы. Если денормализуется отношение один-к-одному, с небольшим количеством колонок, пользователи могут просто добавить их как строки с их оригинальными типами, как показано выше. Однако это часто нежелательно для более крупных объектов и невозможно для отношений один-ко-многим.
В случаях сложных объектов или отношений один-ко-многим пользователи могут использовать:
- Именованные кортежи — они позволяют представить связанную структуру в виде набора колонок.
- Array(Tuple) или Nested — массив именованных кортежей, также известных как вложенные, где каждый элемент представляет собой объект. Применимо к отношениям один-ко-многим.
В качестве примера мы демонстрируем денормализацию PostLinks
на Posts
ниже.
Каждый пост может содержать несколько ссылок на другие посты, как показано в схеме PostLinks
выше. В качестве вложенного типа мы можем представить эти связанные и дублированные посты следующим образом:
Обратите внимание на использование настройки
flatten_nested=0
. Мы рекомендуем отключить упрощение вложенных данных.
Мы можем выполнить эту денормализацию, используя INSERT INTO SELECT
с запросом OUTER JOIN
:
Обратите внимание на время выполнения. Нам удалось денормализовать 66 миллионов строк примерно за 2 минуты. Как мы увидим позже, это операция, которую мы можем планировать.
Обратите внимание на использование функций groupArray
, чтобы объединить PostLinks
в массив для каждого PostId
перед соединением. Этот массив затем фильтруется на два подсписка: LinkedPosts
и DuplicatePosts
, которые также исключают любые пустые результаты из внешнего соединения.
Мы можем выбрать несколько строк, чтобы увидеть нашу новую денормализованную структуру:
Орchestrating и планирование денормализации
Пакетный процесс
Использование денормализации требует процесса преобразования, в котором она может быть выполнена и организована.
Мы показали выше, как ClickHouse может использоваться для выполнения этого преобразования, как только данные были загружены через INSERT INTO SELECT
. Это подходит для периодических пакетных преобразований.
Пользователи имеют несколько вариантов для организации этого в ClickHouse, при условии, что периодический пакетный процесс загрузки приемлем:
- Обновляемые материализованные представления — Обновляемые материализованные представления могут использоваться для периодического планирования запроса с результатами, отправляемыми в целевую таблицу. При выполнении запроса представление гарантирует, что целевая таблица атомарно обновляется. Это предоставляет родной способ ClickHouse для планирования этой работы.
- Внешние инструменты — Использование таких инструментов, как dbt и Airflow, для периодического планирования преобразования. Интеграция ClickHouse для dbt обеспечивает, что это будет выполнено атомарно с созданием новой версии целевой таблицы, которая затем атомарно заменяется версией, получающей запросы (через команду EXCHANGE).
Стриминг
Пользователи могут также захотеть выполнить это вне ClickHouse, до вставки, используя стриминговые технологии, такие как Apache Flink. Альтернативно, инкрементные материализованные представления могут использоваться для выполнения этого процесса по мере вставки данных.