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

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

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

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

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

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


Распространенной техникой, популяризированной решениями NoSQL, является денормализация данных в отсутствие поддержки JOIN, эффективно сохраняя все статистики или связанные строки на родительской строке в виде колонок и вложенных объектов. Например, в схеме блога мы можем хранить все Комментарии в виде 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:

Обратите внимание на время выполнения. Мы смогли денормализовать 66m строк за около 2 минут. Как мы увидим позже, это операция, которую мы можем запланировать.

Обратите внимание на использование функции groupArray, чтобы свести PostLinks в массив для каждого PostId перед объединением. Этот массив затем фильтруется на два подсписка: LinkedPosts и DuplicatePosts, которые также исключают любые пустые результаты из внешнего соединения.

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

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

Пакет

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

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

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

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

Потоковая обработка

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