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

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

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

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

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

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

Денормализация в ClickHouse

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Схема Пользователи и Награды

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

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

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

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

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

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

Подобным образом, эти ссылки не являются событиями, которые происходят слишком часто:

Мы используем это как наш пример денормализации ниже.

Простой пример статистики

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

Чтобы заполнить эту таблицу, мы используем INSERT INTO SELECT, соединяя нашу дублирующую статистику с нашими постами.

Использование сложных типов для отношений один-ко-многим

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

В случаях сложных объектов или отношений один-ко-многим пользователи могут использовать:

  • Именованные кортежи — это позволяет представлять связанную структуру в виде набора колонок.
  • Array(Tuple) или Nested — массив именованных кортежей, также известных как 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. В качестве альтернативы можно использовать инкрементальные материализованные представления для выполнения этого процесса, когда данные вставляются.