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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Публикации и Голоса

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

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

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

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

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

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

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

Теперь давайте рассмотрим наших Пользователей и Награды:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Именованные кортежи - они позволяют связанной структуре представляться в виде набора колонок.
  • Array(Tuple) или Nested - массив именованных кортежей, также известных как вложенные, где каждая запись представляет собой объект. Применимо к отношениям "один-ко-многим".

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

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

Обратите внимание на использование настройки flatten_nested=0. Мы рекомендуем отключить уплотнение вложенных данных.

Мы можем провести эту денормализацию, используя INSERT INTO SELECT с запросом OUTER JOIN:

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

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

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

Организация и планирование денормализации

Пакет

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

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

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

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

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

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