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

Проектирование схемы

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

Набор данных Stack Overflow

Для примеров в этом руководстве мы используем подмножество набора данных Stack Overflow. Он содержит все посты, голоса, пользователей, комментарии и значки, которые появились на Stack Overflow с 2008 года по апрель 2024 года. Эти данные доступны в формате Parquet по схемам, представленным ниже, в корзине S3 s3://datasets-documentation/stackoverflow/parquet/:

Первичные ключи и связи, указанные здесь, не применяются через ограничения (Parquet — это формат файла, а не таблицы) и только указывают, как данные связаны и какие уникальные ключи они имеют.

Схема Stack Overflow

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

Приведенная выше схема намеренно не является оптимальной для целей этого руководства.

Создание начальной схемы

Поскольку таблица posts будет целью большинства аналитических запросов, мы сосредоточимся на создании схемы для этой таблицы. Эти данные доступны в публичной корзине S3 s3://datasets-documentation/stackoverflow/parquet/posts/*.parquet с файлом для каждого года.

Загрузка данных из S3 в формате Parquet представляет собой наиболее распространенный и предпочтительный способ загрузки данных в ClickHouse. ClickHouse оптимизирован для обработки Parquet и потенциально может читать и вставлять десятки миллионов строк из S3 в секунду.

ClickHouse предоставляет возможность автоматического определения схемы для автоматической идентификации типов набора данных. Это поддерживается для всех форматов данных, включая Parquet. Мы можем использовать эту функцию для определения типов ClickHouse для данных через табличную функцию s3 и команду DESCRIBE. Обратите внимание, что ниже мы используем glob-шаблон *.parquet для чтения всех файлов в папке stackoverflow/parquet/posts.

DESCRIBE TABLE s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/*.parquet')
SETTINGS describe_compact_output = 1

┌─name──────────────────┬─type───────────────────────────┐
│ Id                    │ Nullable(Int64)               │
│ PostTypeId            │ Nullable(Int64)               │
│ AcceptedAnswerId      │ Nullable(Int64)               │
│ CreationDate          │ Nullable(DateTime64(3, 'UTC')) │
│ Score                 │ Nullable(Int64)               │
│ ViewCount             │ Nullable(Int64)               │
│ Body                  │ Nullable(String)              │
│ OwnerUserId           │ Nullable(Int64)               │
│ OwnerDisplayName      │ Nullable(String)              │
│ LastEditorUserId      │ Nullable(Int64)               │
│ LastEditorDisplayName │ Nullable(String)              │
│ LastEditDate          │ Nullable(DateTime64(3, 'UTC')) │
│ LastActivityDate      │ Nullable(DateTime64(3, 'UTC')) │
│ Title                 │ Nullable(String)              │
│ Tags                  │ Nullable(String)              │
│ AnswerCount           │ Nullable(Int64)               │
│ CommentCount          │ Nullable(Int64)               │
│ FavoriteCount         │ Nullable(Int64)               │
│ ContentLicense        │ Nullable(String)              │
│ ParentId              │ Nullable(String)              │
│ CommunityOwnedDate    │ Nullable(DateTime64(3, 'UTC')) │
│ ClosedDate            │ Nullable(DateTime64(3, 'UTC')) │
└───────────────────────┴────────────────────────────────┘

Табличная функция s3 позволяет запрашивать данные в S3 непосредственно из ClickHouse. Эта функция совместима со всеми форматами файлов, которые поддерживает ClickHouse.

Это дает нам начальную неоптимизированную схему. По умолчанию ClickHouse сопоставляет их с эквивалентными типами Nullable. Мы можем создать таблицу ClickHouse, используя эти типы, с помощью простой команды CREATE EMPTY AS SELECT.

CREATE TABLE posts
ENGINE = MergeTree
ORDER BY () EMPTY AS
SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/*.parquet')

Несколько важных моментов:

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

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

Чтобы подтвердить создание таблицы:

SHOW CREATE TABLE posts

CREATE TABLE posts
(
        `Id` Nullable(Int64),
        `PostTypeId` Nullable(Int64),
        `AcceptedAnswerId` Nullable(Int64),
        `CreationDate` Nullable(DateTime64(3, 'UTC')),
        `Score` Nullable(Int64),
        `ViewCount` Nullable(Int64),
        `Body` Nullable(String),
        `OwnerUserId` Nullable(Int64),
        `OwnerDisplayName` Nullable(String),
        `LastEditorUserId` Nullable(Int64),
        `LastEditorDisplayName` Nullable(String),
        `LastEditDate` Nullable(DateTime64(3, 'UTC')),
        `LastActivityDate` Nullable(DateTime64(3, 'UTC')),
        `Title` Nullable(String),
        `Tags` Nullable(String),
        `AnswerCount` Nullable(Int64),
        `CommentCount` Nullable(Int64),
        `FavoriteCount` Nullable(Int64),
        `ContentLicense` Nullable(String),
        `ParentId` Nullable(String),
        `CommunityOwnedDate` Nullable(DateTime64(3, 'UTC')),
        `ClosedDate` Nullable(DateTime64(3, 'UTC'))
)
ENGINE = MergeTree('/clickhouse/tables/{uuid}/{shard}', '{replica}')
ORDER BY tuple()

После определения начальной схемы мы можем заполнить данные с помощью INSERT INTO SELECT, считывая данные с помощью табличной функции s3. Следующая загрузка данных posts занимает около 2 минут на 8-ядерном экземпляре ClickHouse Cloud.

INSERT INTO posts SELECT * FROM s3('https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/*.parquet')

0 rows in set. Elapsed: 148.140 sec. Processed 59.82 million rows, 38.07 GB (403.80 thousand rows/s., 257.00 MB/s.)

Приведенный выше запрос загружает 60 миллионов строк. Хотя это небольшой объем для ClickHouse, пользователи с более медленным интернет-соединением могут захотеть загрузить подмножество данных. Этого можно достичь, просто указав годы, которые они хотят загрузить, через glob-шаблон, например https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/2008.parquet или https://datasets-documentation.s3.eu-west-3.amazonaws.com/stackoverflow/parquet/posts/{2008, 2009}.parquet. Смотрите здесь, как glob-шаблоны можно использовать для таргетирования подмножеств файлов.

Оптимизация типов

Один из секретов производительности запросов ClickHouse — это сжатие.

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

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

На сжатие в ClickHouse будут влиять 3 основных фактора: ключ сортировки, типы данных и любые используемые кодеки. Все это настраивается через схему.

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

  • Используйте строгие типы - Наша начальная схема использовала строки для многих столбцов, которые явно являются числовыми. Использование правильных типов обеспечит ожидаемую семантику при фильтрации и агрегировании. То же самое относится к типам дат, которые были правильно предоставлены в файлах Parquet.
  • Избегайте Nullable столбцов - По умолчанию приведенные выше столбцы предполагались Null. Тип Nullable позволяет запросам определять разницу между пустым значением и значением Null. Это создает отдельный столбец типа UInt8. Этот дополнительный столбец должен обрабатываться каждый раз, когда пользователь работает со столбцом nullable. Это приводит к дополнительному использованию пространства хранения и почти всегда негативно влияет на производительность запросов. Используйте Nullable только в том случае, если есть разница между пустым значением по умолчанию для типа и значением Null. Например, значение 0 для пустых значений в столбце ViewCount, вероятно, будет достаточным для большинства запросов и не повлияет на результаты. Если пустые значения должны обрабатываться по-другому, их также часто можно исключить из запросов с помощью фильтра.
  • Используйте минимальную точность для числовых типов - ClickHouse имеет ряд числовых типов, предназначенных для различных числовых диапазонов и точности. Всегда стремитесь минимизировать количество битов, используемых для представления столбца. Помимо целых чисел разного размера, например Int16, ClickHouse предлагает беззнаковые варианты, минимальное значение которых равно 0. Они могут позволить использовать меньше битов для столбца, например, UInt16 имеет максимальное значение 65535, что в два раза больше, чем у Int16. Предпочитайте эти типы более крупным знаковым вариантам, если это возможно.
  • Минимальная точность для типов дат - ClickHouse поддерживает ряд типов дат и дат-времени. Date и Date32 могут использоваться для хранения чистых дат, причем последний поддерживает больший диапазон дат за счет большего количества битов. DateTime и DateTime64 обеспечивают поддержку дат-времени. DateTime ограничен детализацией до секунды и использует 32 бита. DateTime64, как следует из названия, использует 64 бита, но обеспечивает поддержку с точностью до наносекунды. Как всегда, выбирайте более грубую версию, приемлемую для запросов, минимизируя количество необходимых битов.
  • Используйте LowCardinality - Столбцы с числами, строками, Date или DateTime с небольшим количеством уникальных значений потенциально могут быть закодированы с использованием типа LowCardinality. Это кодирует значения в словарь, уменьшая размер на диске. Рассмотрите это для столбцов с менее чем 10 тысячами уникальных значений.
  • FixedString для особых случаев - Строки с фиксированной длиной могут быть закодированы типом FixedString, например, коды языков и валют. Это эффективно, когда данные имеют длину ровно N байтов. Во всех остальных случаях это, вероятно, снизит эффективность, и предпочтительнее использовать LowCardinality.
  • Enum для проверки данных - Тип Enum можно использовать для эффективного кодирования перечисляемых типов. Enum может быть 8 или 16 бит в зависимости от количества уникальных значений, которые они должны хранить. Рассмотрите возможность использования этого, если вам нужна либо связанная проверка во время вставки (необъявленные значения будут отклонены), либо вы хотите выполнять запросы, использующие естественный порядок значений Enum, например, представьте столбец обратной связи, содержащий ответы пользователей Enum(':(' = 1, ':|' = 2, ':)' = 3).

Совет: Чтобы найти диапазон всех столбцов и количество различных значений, пользователи могут использовать простой запрос SELECT * APPLY min, * APPLY max, * APPLY uniq FROM table FORMAT Vertical. Мы рекомендуем выполнять это на меньшем подмножестве данных, так как это может быть дорогостоящим. Этот запрос требует, чтобы числовые значения были хотя бы определены как таковые для получения точного результата, то есть не как String.

Применяя эти простые правила к нашей таблице posts, мы можем определить оптимальный тип для каждого столбца:

СтолбецЧисловойМин, МаксУникальные значенияNullsКомментарийОптимизированный тип
PostTypeIdДа1, 88НетEnum('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8)
AcceptedAnswerIdДа0, 7828517012282094ДаРазличать Null со значением 0UInt32
CreationDateНет2008-07-31 21:42:52.667000000, 2024-03-31 23:59:17.697000000-НетДетализация миллисекунд не требуется, использовать DateTimeDateTime
ScoreДа-217, 349703236НетInt32
ViewCountДа2, 13962748170867НетUInt32
BodyНет--НетString
OwnerUserIdДа-1, 40569156256237ДаInt32
OwnerDisplayNameНет-181251ДаСчитать Null пустой строкойString
LastEditorUserIdДа-1, 99999931104694Да0 — неиспользуемое значение, может использоваться для NullsInt32
LastEditorDisplayNameНет-70952ДаСчитать Null пустой строкой. Тестировали LowCardinality без преимуществString
LastEditDateНет2008-08-01 13:24:35.051000000, 2024-04-06 21:01:22.697000000-НетДетализация миллисекунд не требуется, использовать DateTimeDateTime
LastActivityDateНет2008-08-01 12:19:17.417000000, 2024-04-06 21:01:22.697000000-НетДетализация миллисекунд не требуется, использовать DateTimeDateTime
TitleНет--НетСчитать Null пустой строкойString
TagsНет--НетСчитать Null пустой строкойString
AnswerCountДа0, 518216НетСчитать Null и 0 одинаковымиUInt16
CommentCountДа0, 135100НетСчитать Null и 0 одинаковымиUInt8
FavoriteCountДа0, 2256ДаСчитать Null и 0 одинаковымиUInt8
ContentLicenseНет-3НетLowCardinality превосходит FixedStringLowCardinality(String)
ParentIdНет-20696028ДаСчитать Null пустой строкойString
CommunityOwnedDateНет2008-08-12 04:59:35.017000000, 2024-04-01 05:36:41.380000000-ДаСчитать значением по умолчанию 1970-01-01 для Nulls. Детализация миллисекунд не требуется, использовать DateTimeDateTime
ClosedDateНет2008-09-04 20:56:44, 2024-04-06 18:49:25.393000000-ДаСчитать значением по умолчанию 1970-01-01 для Nulls. Детализация миллисекунд не требуется, использовать DateTimeDateTime

Приведенное выше дает нам следующую схему:

CREATE TABLE posts_v2
(
   `Id` Int32,
   `PostTypeId` Enum('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8),
   `AcceptedAnswerId` UInt32,
   `CreationDate` DateTime,
   `Score` Int32,
   `ViewCount` UInt32,
   `Body` String,
   `OwnerUserId` Int32,
   `OwnerDisplayName` String,
   `LastEditorUserId` Int32,
   `LastEditorDisplayName` String,
   `LastEditDate` DateTime,
   `LastActivityDate` DateTime,
   `Title` String,
   `Tags` String,
   `AnswerCount` UInt16,
   `CommentCount` UInt8,
   `FavoriteCount` UInt8,
   `ContentLicense`LowCardinality(String),
   `ParentId` String,
   `CommunityOwnedDate` DateTime,
   `ClosedDate` DateTime
)
ENGINE = MergeTree
ORDER BY tuple()
COMMENT 'Оптимизированные типы'

Мы можем заполнить это простым INSERT INTO SELECT, считывая данные из нашей предыдущей таблицы и вставляя их в эту:

INSERT INTO posts_v2 SELECT * FROM posts

0 rows in set. Elapsed: 146.471 sec. Processed 59.82 million rows, 83.82 GB (408.40 thousand rows/s., 572.25 MB/s.)

Мы не сохраняем никаких значений null в нашей новой схеме. Приведенная выше вставка неявно преобразует их в значения по умолчанию для соответствующих типов — 0 для целых чисел и пустое значение для строк. ClickHouse также автоматически преобразует любые числовые значения в их целевую точность. Первичные (сортировочные) ключи в ClickHouse Пользователи, пришедшие из баз данных OLTP, часто ищут эквивалентную концепцию в ClickHouse.

Выбор ключа сортировки

В масштабе, в котором часто используется ClickHouse, эффективность памяти и диска имеет первостепенное значение. Данные записываются в таблицы ClickHouse фрагментами, известными как части (parts), с правилами, применяемыми для слияния частей в фоновом режиме. В ClickHouse каждая часть имеет свой собственный первичный индекс. Когда части объединяются, первичные индексы объединенной части также объединяются. Первичный индекс для части имеет одну запись индекса на группу строк — этот метод называется разреженным индексированием.

Разреженное индексирование в ClickHouse

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

Все столбцы в таблице будут отсортированы на основе значения указанного ключа сортировки, независимо от того, включены ли они в сам ключ. Например, если CreationDate используется в качестве ключа, порядок значений во всех других столбцах будет соответствовать порядку значений в столбце CreationDate. Можно указать несколько ключей сортировки — это будет упорядочено с той же семантикой, что и предложение ORDER BY в запросе SELECT.

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

  • Выберите столбцы, которые соответствуют вашим общим фильтрам. Если столбец часто используется в предложениях WHERE, отдайте приоритет включению их в ваш ключ над теми, которые используются реже.
  • Предпочитайте столбцы, которые помогают исключить большой процент от общего количества строк при фильтрации, тем самым уменьшая объем данных, которые необходимо прочитать.
  • Предпочитайте столбцы, которые, вероятно, сильно коррелируют с другими столбцами в таблице. Это поможет обеспечить смежное хранение этих значений, улучшая сжатие.
  • Операции GROUP BY и ORDER BY для столбцов в ключе сортировки могут быть более эффективными с точки зрения памяти.

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

Пример

Применяя приведенные выше рекомендации к нашей таблице posts, предположим, что наши пользователи хотят выполнять аналитику с фильтрацией по дате и типу поста, например:

"Какие вопросы получили больше всего комментариев за последние 3 месяца".

Запрос для этого вопроса с использованием нашей предыдущей таблицы posts_v2 с оптимизированными типами, но без ключа сортировки:

SELECT
    Id,
    Title,
    CommentCount
FROM posts_v2
WHERE (CreationDate >= '2024-01-01') AND (PostTypeId = 'Question')
ORDER BY CommentCount DESC
LIMIT 3

┌───────Id─┬─Title─────────────────────────────────────────────────────────────┬─CommentCount─┐
│ 78203063 │ How to avoid default initialization of objects in std::vector?     │               74 │
│ 78183948 │ About memory barrier                                               │               52 │
│ 77900279 │ Speed Test for Buffer Alignment: IBM's PowerPC results vs. my CPU │        49 │
└──────────┴───────────────────────────────────────────────────────────────────┴──────────────

10 rows in set. Elapsed: 0.070 sec. Processed 59.82 million rows, 569.21 MB (852.55 million rows/s., 8.11 GB/s.)
Peak memory usage: 429.38 MiB.

Запрос здесь очень быстрый, даже несмотря на то, что все 60 миллионов строк были линейно просканированы — ClickHouse просто быстрый :) Вам придется довериться нам, что ключи сортировки стоят того в масштабе ТБ и ПБ!

Давайте выберем столбцы PostTypeId и CreationDate в качестве наших ключей сортировки.

Возможно, в нашем случае мы ожидаем, что пользователи всегда будут фильтровать по PostTypeId. У него кардинальность 8, и он представляет собой логический выбор для первой записи в нашем ключе сортировки. Признавая, что фильтрации с детализацией до даты, вероятно, будет достаточно (это все равно принесет пользу фильтрам datetime), мы используем toDate(CreationDate) в качестве 2-го компонента нашего ключа. Это также создаст меньший индекс, так как дата может быть представлена 16 битами, ускоряя фильтрацию. Наша последняя запись ключа — это CommentCount для помощи в поиске постов с наибольшим количеством комментариев (окончательная сортировка).

CREATE TABLE posts_v3
(
        `Id` Int32,
        `PostTypeId` Enum('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8),
        `AcceptedAnswerId` UInt32,
        `CreationDate` DateTime,
        `Score` Int32,
        `ViewCount` UInt32,
        `Body` String,
        `OwnerUserId` Int32,
        `OwnerDisplayName` String,
        `LastEditorUserId` Int32,
        `LastEditorDisplayName` String,
        `LastEditDate` DateTime,
        `LastActivityDate` DateTime,
        `Title` String,
        `Tags` String,
        `AnswerCount` UInt16,
        `CommentCount` UInt8,
        `FavoriteCount` UInt8,
        `ContentLicense` LowCardinality(String),
        `ParentId` String,
        `CommunityOwnedDate` DateTime,
        `ClosedDate` DateTime
)
ENGINE = MergeTree
ORDER BY (PostTypeId, toDate(CreationDate), CommentCount)
COMMENT 'Ключ сортировки'

--заполнение таблицы из существующей таблицы

INSERT INTO posts_v3 SELECT * FROM posts_v2

0 rows in set. Elapsed: 158.074 sec. Processed 59.82 million rows, 76.21 GB (378.42 thousand rows/s., 482.14 MB/s.)
Peak memory usage: 6.41 GiB.

Наш предыдущий запрос улучшает время отклика запроса более чем в 3 раза:

SELECT
    Id,
    Title,
    CommentCount
FROM posts_v3
WHERE (CreationDate >= '2024-01-01') AND (PostTypeId = 'Question')
ORDER BY CommentCount DESC
LIMIT 3

10 rows in set. Elapsed: 0.020 sec. Processed 290.09 thousand rows, 21.03 MB (14.65 million rows/s., 1.06 GB/s.)

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

Далее: Техники моделирования данных

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

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

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

Следующие подходы направлены на минимизацию необходимости использования JOIN для оптимизации чтения и повышения производительности запросов. Хотя JOIN полностью поддерживаются в ClickHouse, мы рекомендуем использовать их умеренно (2-3 таблицы в запросе JOIN — это нормально) для достижения оптимальной производительности.

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

Чтобы минимизировать использование Join во время выполнения запросов, у пользователей есть несколько инструментов/подходов:

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

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