Перейти к основному содержимому
Перейти к основному содержимому

Денормализация данных

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

Сравнение нормализованных и денормализованных схем

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

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


Распространенной техникой, популяризированной решениями NoSQL, является денормализация данных в отсутствие поддержки JOIN, фактически храня все статистические данные или связанные строки на родительской строке в виде столбцов и вложенных объектов. Например, в примере схемы блога мы можем хранить все Comments как Array объектов на соответствующих постах.

Когда использовать денормализацию

В общем, мы рекомендуем денормализацию в следующих случаях:

  • Денормализуйте таблицы, которые меняются нечасто или для которых допустима задержка перед тем, как данные станут доступны для аналитических запросов, т.е. данные могут быть полностью перезагружены в пакетном режиме.
  • Избегайте денормализации отношений многие-ко-многим. Это может привести к необходимости обновления многих строк, если меняется одна исходная строка.
  • Избегайте денормализации отношений с высокой кардинальностью. Если в каждой строке таблицы есть тысячи связанных записей в другой таблице, их необходимо представить в виде Array — либо примитивного типа, либо кортежей. Обычно, массивы с более чем 1000 кортежей не рекомендуется использовать.
  • Вместо того чтобы денормализовывать все столбцы как вложенные объекты, рассмотрите возможность денормализации только статистики с помощью материализованных представлений (см. ниже).

Не вся информация должна быть денормализована — только ключевая информация, к которой необходимо получать доступ часто.

Работы по денормализации могут выполняться как в ClickHouse, так и upstream, например, с использованием Apache Flink.

Избегайте денормализации для часто обновляемых данных

Для ClickHouse денормализация является одним из нескольких вариантов, которые пользователи могут использовать для оптимизации производительности запросов, но ее следует использовать осторожно. Если данные обновляются часто и необходимо обновление в почти реальном времени, этот подход следует избегать. Используйте это, если основная таблица в основном добавляет записи или может периодически перезагружаться в пакетном режиме, например, ежедневно.

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

Достижение этого в реальном времени часто нереалистично и требует значительных усилий со стороны разработчиков из-за двух проблем:

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

Процесс пакетного обновления становится более распространенным, когда все денормализованные объекты периодически перезагружаются.

Практические случаи использования денормализации

Рассмотрим несколько практических примеров, где денормализация может иметь смысл, и другие случаи, где более желательно использовать альтернативные подходы.

Предположим, у нас есть таблица Posts, которая уже была денормализована со статистикой, такой как AnswerCount и CommentCount — исходные данные предоставлены в этой форме. На самом деле, возможно, мы захотим нормализовать эту информацию, так как она, вероятно, будет часто меняться. Многие из этих столбцов также доступны через другие таблицы, например, комментарии к посту доступны через столбец PostId и таблицу Comments. Для целей примера предположим, что посты перезагружаются в пакетном процессе.

Мы также рассматриваем только денормализацию других таблиц в таблицу Posts, поскольку считаем ее нашей основной таблицей для аналитики. Денормализация в другом направлении также была бы уместной для некоторых запросов, при этом те же самые вышеуказанные соображения применимы.

Для каждого из следующих примеров предположим, что существует запрос, который требует использования обеих таблиц в соединении.

Посты и голоса

Голоса для постов представлены в виде отдельных таблиц. Оптимизированная схема для этого представлена ниже, а также команда вставки для загрузки данных:

На первый взгляд, это могут быть кандидаты для денормализации в таблицу постов. Однако у данного подхода есть несколько проблем.

Голоса добавляются к постам часто. Хотя это может снизиться со временем, следующий запрос показывает, что у нас около 40k голосов в час на 30k постах.

Это можно решить пакетной обработкой, если задержка допустима, но это все равно требует обработки обновлений, если мы не хотим периодически перезагружать все посты (что вряд ли желательно).

Более проблематично то, что у некоторых постов очень много голосов:

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

Пользователи и награды

Теперь давайте рассмотрим наши Users и Badges:

Сначала вставляем данные следующей командой:

Хотя пользователи могут часто получать награды, вряд ли это набор данных, который необходимо обновлять более одного раза в день. Связь между наградами и пользователями — один-ко-многим. Возможно, мы можем просто денормализовать награды на пользователей в виде списка кортежей? Хотя это возможно, быстрый проверочный запрос, подтверждающий наибольшее количество наград на пользователя, предполагает, что это не идеально:

Вряд ли реалистично денормализовать 19k объектов на одну строку. Эта связь, вероятно, лучше оставить в виде отдельных таблиц или с добавленными статистическими данными.

Мы можем захотеть денормализовать статистику по наградам на пользователей, например, количество наград. Мы рассматриваем такой пример при использовании словарей для этого набора данных во время вставки.

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, которые также исключают любые пустые результаты из внешнего соединения.

Мы можем выбрать некоторые строки, чтобы увидеть нашу новую денормализованную структуру:

Оркестрация и планирование денормализации

Пакет

Эксплуатация денормализации требует процесса трансформации, в котором она может быть выполнена и оркестрирована.

Мы показали выше, как ClickHouse может использоваться для выполнения этой трансформации после загрузки данных через INSERT INTO SELECT. Это подходящее решение для периодических пакетных трансформаций.

У пользователей есть несколько вариантов для оркестрации этого в ClickHouse, предполагая, что периодический пакетный процесс загрузки приемлем:

  • Обновляемые материализованные представления — обновляемые материализованные представления можно использовать для периодического планирования запроса, результаты которого отправляются в целевую таблицу. При выполнении запроса представление обеспечивает атомарное обновление целевой таблицы. Это предоставляет нативное средство ClickHouse для планирования этой работы.
  • Внешние инструменты — использование таких инструментов, как dbt и Airflow, для периодического планирования трансформации. Интеграция ClickHouse для dbt обеспечивает выполнение этого атомарно с созданием новой версии целевой таблицы, которая затем атомарно обменита с версией, получающей запросы (через команду EXCHANGE).

Стриминг

Пользователи также могут захотеть выполнить это вне ClickHouse, до вставки, с использованием таких технологий стриминга, как Apache Flink. В качестве альтернативы можно использовать инкрементные материализованные представления для выполнения этого процесса по мере вставки данных.