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

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

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

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

Для примеров в этом гидe мы используем подмножество набора данных 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. Обратите внимание, что ниже мы используем глобальный шаблон *.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 table позволяет запрашивать данные в 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')

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

Наша таблица постов пуста после выполнения этой команды. Данные не были загружены. Мы указали MergeTree как движок нашей таблицы. MergeTree - это наиболее распространенный движок таблиц ClickHouse, который вы, вероятно, будете использовать. Он является многофункциональным инструментом в вашем арсенале ClickHouse, способным обрабатывать PB данных и обслуживать большинство аналитических случаев. Существуют и другие движки таблиц для использования в таких случаях, как 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 минуты на облачном экземпляре ClickHouse с 8 ядрами.

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

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

Одна из тайн производительности запросов ClickHouse заключается в сжатии.

Меньше данных на диске означает меньше I/O и, следовательно, более быстрые запросы и вставки. Нагрузка любого алгоритма сжатия по сравнению с CPU в большинстве случаев будет компенсирована сокращением I/O. Поэтому улучшение сжатия данных должно быть первым приоритетом при работе над обеспечением быстрой работы запросов 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. Этот словарь кодирует значения, уменьшая размер на диске. Рассмотрите это для колонок с менее чем 10k уникальными значениями.
  • FixedString для специальных случаев - Строки, которые имеют фиксированную длину, могут быть закодированы с помощью типа FixedString, например, коды языка и валюты. Это эффективно, когда данные имеют длину ровно N байт. В остальных случаях это, вероятно, снизит эффективность, и предпочтительнее использовать LowCardinality.
  • Enums для валидации данных - Тип Enum может быть использован для эффективного кодирования перечисляемых типов. Enums могут быть 8 или 16 бит, в зависимости от количества уникальных значений, которые они должны хранить. Рассмотрите использование этого, если вам нужна либо связанная валидация во время вставки (недекларированные значения будут отвергнуты), либо хотите выполнять запросы, которые используют естественный порядок в значениях Enum, например, представьте колонку обратной связи, содержащую ответы пользователей Enum(':(' = 1, ':|' = 2, ':)' = 3).

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

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

КолонкаЯвляется числовойМин, МаксУникальные значения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 и 0 значениемUInt32
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 - неиспользуемое значение, можно использовать для NullInt32
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 для Null. Миллисекундная гранулярность не требуется, используйте DateTimeDateTime
ClosedDateНет2008-09-04 20:56:44, 2024-04-06 18:49:25.393000000-ДаУчтите, что значение по умолчанию 1970-01-01 для Null. Миллисекундная гранулярность не требуется, используйте 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 'Optimized types'

Мы можем заполнить это с помощью простого 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 в частях, с правилами слияния частей в фоновом режиме. В 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 просто быстрый :) Вам придется доверять нам, что упорядочивающие ключи имеют значение на TB и PB масштабе!

Давайте выберем колонки PostTypeId и CreationDate в качестве наших упорядочивающих ключей.

Возможно, в нашем случае мы ожидаем, что пользователи всегда будут фильтровать по PostTypeId. У него кардинальность 8, и это логический выбор для первого элемента в нашем упорядочивающем ключе. Признавая, что фильтрация по гранулярности даты, вероятно, будет достаточной (она все равно будет полезна для фильтров даты и времени), мы используем toDate(CreationDate) в качестве второго компонента нашего ключа. Это также создаст меньший индекс, так как дату можно представить 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 'Ordering Key'

--populate table from existing table

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.

Our previous query improves the query response time by over 3x:

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.

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

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

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

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

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

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