Набор данных Stack Overflow
s3://datasets-documentation/stackoverflow/parquet/:
Указанные первичные ключи и связи не обеспечиваются ограничениями (Parquet — это файловый, а не табличный формат) и лишь показывают, как данные связаны между собой и какими уникальными ключами они обладают.
Набор данных 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.
Табличная функция S3 позволяет выполнять запросы к данным в S3 напрямую из ClickHouse. Эта функция совместима со всеми файловыми форматами, которые поддерживает ClickHouse.В результате мы получаем исходную неоптимизированную схему. По умолчанию ClickHouse преобразует их в эквивалентные типы Nullable. Мы можем создать таблицу ClickHouse с этими типами с помощью простой команды
CREATE EMPTY AS SELECT.
ORDER BY () означает, что у нас нет индекса, а точнее — никакого порядка в данных. Подробнее об этом позже. Пока достаточно знать, что все запросы будут требовать линейного сканирования.
Чтобы убедиться, что таблица создана:
INSERT INTO SELECT, читая их через табличную функцию S3. Ниже показано, как загрузить данные posts примерно за 2 минуты на 8-ядерном экземпляре ClickHouse Cloud.
Приведенный выше запрос загружает 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 так хорошо сжимает данные, рекомендуем эту статью. Кратко: ClickHouse как столбцовая база данных записывает значения по столбцам. Если эти значения отсортированы, одинаковые значения оказываются рядом друг с другом. Алгоритмы сжатия эффективно используют такие непрерывные шаблоны в данных. Кроме того, в ClickHouse есть кодеки и типы данных разной точности, которые позволяют дополнительно настраивать методы сжатия.На сжатие в ClickHouse влияют 3 основных фактора: ключ сортировки, типы данных и используемые кодеки. Все это настраивается через схему. Наибольшего начального прироста в сжатии и производительности запросов можно добиться простым процессом оптимизации типов. Для оптимизации схемы можно применить несколько простых правил:
- Используйте строгие типы - В нашей исходной схеме для многих столбцов, которые явно являются числовыми, использовался тип String. Использование корректных типов обеспечит ожидаемую семантику при фильтрации и агрегации. То же самое относится и к типам дат, которые уже корректно заданы в файлах 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, мы можем определить оптимальный тип для каждого столбца:
| Столбец | Числовой | Мин., макс. | Уникальные значения | NULL | Комментарий | Оптимизированный тип |
|---|---|---|---|---|---|---|
PostTypeId | Да | 1, 8 | 8 | Нет | Enum('Question' = 1, 'Answer' = 2, 'Wiki' = 3, 'TagWikiExcerpt' = 4, 'TagWiki' = 5, 'ModeratorNomination' = 6, 'WikiPlaceholder' = 7, 'PrivilegeWiki' = 8) | |
AcceptedAnswerId | Да | 0, 78285170 | 12282094 | Да | Различать NULL и значение 0 | UInt32 |
CreationDate | Нет | 2008-07-31 21:42:52.667000000, 2024-03-31 23:59:17.697000000 | * | Нет | Точность до миллисекунд не требуется, используйте DateTime | DateTime |
Score | Да | -217, 34970 | 3236 | Нет | Int32 | |
ViewCount | Да | 2, 13962748 | 170867 | Нет | UInt32 | |
Body | Нет | - | * | Нет | String | |
OwnerUserId | Да | -1, 4056915 | 6256237 | Да | Int32 | |
OwnerDisplayName | Нет | - | 181251 | Да | Считать NULL пустой строкой | String |
LastEditorUserId | Да | -1, 9999993 | 1104694 | Да | 0 — неиспользуемое значение, его можно использовать для NULL | Int32 |
LastEditorDisplayName | Нет | * | 70952 | Да | Считать NULL пустой строкой. LowCardinality протестирован, пользы нет | String |
LastEditDate | Нет | 2008-08-01 13:24:35.051000000, 2024-04-06 21:01:22.697000000 | - | Нет | Миллисекундная точность не требуется, используйте DateTime | DateTime |
LastActivityDate | Нет | 2008-08-01 12:19:17.417000000, 2024-04-06 21:01:22.697000000 | * | Нет | Точность до миллисекунд не требуется, используйте DateTime | DateTime |
Title | Нет | - | * | Нет | Считать NULL пустой строкой | String |
Tags | Нет | - | * | Нет | Считать NULL пустой строкой | String |
AnswerCount | Да | 0, 518 | 216 | Нет | Считать NULL и 0 равнозначными | UInt16 |
CommentCount | Да | 0, 135 | 100 | Нет | Считать NULL и 0 равнозначными | UInt8 |
FavoriteCount | Да | 0, 225 | 6 | Да | Считать NULL и 0 равнозначными | UInt8 |
ContentLicense | Нет | - | 3 | Нет | LowCardinality превосходит FixedString по производительности | LowCardinality(String) |
ParentId | Нет | * | 20696028 | Да | Считать NULL пустой строкой | String |
CommunityOwnedDate | Нет | 2008-08-12 04:59:35.017000000, 2024-04-01 05:36:41.380000000 | - | Да | Рекомендуется использовать значение по умолчанию 1970-01-01 для значений NULL. Миллисекундная точность не требуется, используйте DateTime | DateTime |
ClosedDate | Нет | 2008-09-04 20:56:44, 2024-04-06 18:49:25.393000000 | * | Да | Для NULL рассмотрите значение по умолчанию 1970-01-01. Миллисекундная точность не требуется, используйте DateTime | DateTime |
Получаем следующую схему:
INSERT INTO SELECT, прочитав данные из предыдущей таблицы и вставив их в эту:
Выбор ключа сортировки
Все столбцы в таблице будут отсортированы по значению указанного ключа сортировки, независимо от того, входят ли они в сам ключ. Например, если в качестве ключа используетсяПри выборе ключа сортировки можно руководствоваться несколькими простыми правилами. Иногда они могут противоречить друг другу, поэтому рассматривайте их по порядку. В результате этого процесса можно определить несколько ключей; обычно достаточно 4–5:CreationDate, порядок значений во всех остальных столбцах будет соответствовать порядку значений в столбцеCreationDate. Можно указать несколько ключей сортировки — в этом случае сортировка будет выполняться с той же семантикой, что и секцияORDER BYв запросеSELECT.
- Выбирайте столбцы, которые соответствуют вашим типичным фильтрам. Если столбец часто используется в секциях
WHERE, включать его в ключ следует в первую очередь по сравнению с теми, которые используются реже. Отдавайте предпочтение столбцам, которые при фильтрации позволяют исключить большую часть строк и тем самым уменьшить объем данных, которые нужно прочитать. - Отдавайте предпочтение столбцам, которые, вероятно, сильно коррелируют с другими столбцами таблицы. Это поможет обеспечить их последовательное хранение и улучшить сжатие.
Операции
GROUP BYиORDER BYдля столбцов, входящих в ключ сортировки, также можно сделать более эффективными с точки зрения использования памяти.
Пример
posts, предположим, что пользователи хотят выполнять аналитические запросы с фильтрацией по дате и типу поста, например:
“У каких вопросов было больше всего комментариев за последние 3 месяца”.
Запрос для этого вопроса с использованием нашей таблицы posts_v2, созданной ранее с оптимизированными типами, но без ключа сортировки:
Запрос здесь выполняется очень быстро, хотя все 60 млн строк были линейно просканированы — ClickHouse просто быстрый :) Вам придётся поверить нам на слово: на масштабах TB и PB ключи сортировки действительно того стоят!Давайте выберем столбцы
PostTypeId и CreationDate в качестве ключей сортировки.
Возможно, в нашем случае пользователи всегда будут фильтровать по PostTypeId. Этот столбец имеет мощность 8 и выглядит логичным выбором для первого элемента ключа сортировки. Поскольку фильтрации по дате, скорее всего, будет достаточно (хотя это всё равно поможет и с фильтрами по дате и времени), в качестве 2-го компонента ключа мы используем toDate(CreationDate). Это также позволит создать индекс меньшего размера, так как дату можно представить 16 битами, что ускоряет фильтрацию. Последний элемент нашего ключа — CommentCount, чтобы упростить поиск постов с наибольшим числом комментариев (финальную сортировку).
Далее: методы моделирования данных
Posts как центральную таблицу, через которую будет выполняться большинство аналитических запросов. Хотя остальные таблицы по-прежнему можно запрашивать отдельно, мы исходим из того, что основная часть аналитики будет выполняться в контексте posts.
В этом разделе мы используем оптимизированные варианты других таблиц. Хотя мы приводим их схемы, ради краткости не останавливаемся на принятых решениях. Они основаны на правилах, описанных ранее, и выводы о том, почему были выбраны именно такие решения, мы оставляем читателю.Все приведенные ниже подходы направлены на то, чтобы свести к минимуму необходимость использовать JOIN, оптимизировать чтение и повысить производительность запросов. Хотя JOIN в ClickHouse полностью поддерживаются, для достижения наилучшей производительности мы рекомендуем использовать их умеренно (JOIN-запросы с 2–3 таблицами — это нормально).
В ClickHouse нет понятия внешних ключей. Это не запрещает JOIN, но означает, что управление ссылочной целостностью остается на стороне пользователя и реализуется на уровне приложения. В OLAP-системах, таких как ClickHouse, целостность данных часто обеспечивается на уровне приложения или в процессе ингестии данных, а не самой базой данных, где это создавало бы существенные накладные расходы. Такой подход дает больше гибкости и ускоряет вставку данных. Это соответствует ориентации ClickHouse на скорость и масштабируемость запросов на чтение и вставку при работе с очень большими наборами данных.Чтобы свести к минимуму использование JOIN на этапе выполнения запроса, у пользователей есть несколько инструментов и подходов:
- Денормализация данных - Денормализуйте данные, объединяя таблицы и используя сложные типы для связей, отличных от 1:1. Это часто означает перенос JOIN с этапа выполнения запроса на этап вставки.
- Dictionaries - Специфичная для ClickHouse возможность для direct joins и поиска по ключу-значению.
- Incremental Materialized Views - Возможность ClickHouse, позволяющая перенести вычислительную нагрузку с этапа выполнения запроса на этап вставки, включая инкрементальное вычисление агрегированных значений.
- Refreshable Materialized Views - Подобно materialized view в других СУБД, позволяет периодически вычислять результаты запроса и кэшировать их.