Как данные физически хранятся в таблице MergeTree() в ClickHouse?

Contents

Введение

В этой статье я опишу, как данные хранятся в таблице MergeTree при наличии партиций и их отсутствии.

Эта статья — углубленный разбор хранения данных одного движка MergeTree.

Чтобы познакомиться подробнее с Архитектурой ClickHouse — рекомендую другую свою статью «Заметки про ClickHouse. Tutorial 101 — Большая подборка информации».

Краткий обзор терминов Parts и Partitions для таблицы ClickHouse с Engine MergeTree()

Партиция – это набор записей в таблице, объединенных по какому-либо критерию. Например, партиция может быть по месяцу, по дню или по типу события. Данные для разных партиций хранятся отдельно. Это позволяет оптимизировать работу с данными, так как при обработке запросов будет использоваться только необходимое подмножество из всевозможных данных.


Parts — части таблицы, в которой хранятся строки. Одна часть = одна папка со столбцами.

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

Select не важно знать о partitions.

Select не знает о ключах партицирования (partitioning keys).

ПОТОМУ ЧТО каждая part имеет специальные файлы minmax_{PARTITIONING_KEY_COLUMN}.idx. Эти файлы содержат минимальные и максимальные значения этих столбцов в этой part. Кроме того, значения minmax_ хранятся в памяти в виде списка parts (вектор c++).

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

Основная идея механизма хранения MergeTree состоит в том, чтобы записывать новые данные в неизменяемые блоки, называемые parts (кусками или частями), а затем асинхронно объединять эти небольшие parts данных в более крупные parts до определенного размера. Запись небольших блоков хороша для записи, но плоха для чтения, поэтому фоновое слияние создает более крупные parts для оптимизации производительности чтения.

Part — это автономное подмножество данных и индексов таблицы, включающее различные файлы. Данные хранятся в столбчатом формате: либо в одном файле, либо в одном файле на столбец. Директория отдельной part содержит (пример):

  • Column data files (Файлы данных столбца). Части могут находиться в компактном режиме, когда все данные столбцов хранятся в одном файле .bin, или в расширенном режиме, по одному файлу .bin на каждый столбец.
  • Primary index (Первичный индекс) состоит из файла idx и файла меток.
    Необязательный индекс partitioning-key MinMax index
  • Необязательные вторичные индексы или secondary indexes (с idx и файлом меток для каждого вторичного индекса).
  • Файлы метаданных.
  • Projections. Аналогично концепции материализованного представления, но хранится в виде скрытой таблицы в подкаталоге со всеми теми же типами файлов, что и родительская таблица (bin, idx и т.д.).

  • Каждая таблица ClickHouse имеет первичный ключ (primary key) и ключ сортировки (sorting key).
  • Если пользователь опускает первичный ключ в инструкции CREATE TABLE, ключ сортировки действует как первичный ключ. Данные организованы на диске внутри части (part) в соответствии с порядком ключей сортировки.
  • Первичный индекс является sparse index (разреженным индексом): он не включает в себя все строки, содержащиеся в части (part). Вместо этого он создает одну запись индекса для каждой N-й строки (по умолчанию это 8192 строки), а каждый блок из N строк называется гранулой. Index entries известны как метки (marks).
  • Granule (Гранула) — это наименьший блок данных, который механизм запросов ClickHouse считывает с диска. Подобно тому, как блочное устройство может читать блоки размером 4 КБ, ClickHouse считывает гранулы как наименьшую единицу данных (в векторизованном виде). Такое векторизованное чтение гранул происходит чрезвычайно быстро, поскольку стоимость десериализации невелика, поскольку представление в памяти и на диске во многих случаях одинаково. Гранулы также можно сжимать для уменьшения размера на диске.

Чтобы найти данные в файлах данных столбцов (файлах .bin), каждый индексный файл сопровождается файлом меток, который включает указатели от ключевых меток индекса на смещения в файлах данных столбцов.

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

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

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

Когда данные вставляются в ClickHouse, он создает один или несколько блоков (частей).

Движок MergeTree выбирает части для слияния на основе некоторых эвристик. Он должен сбалансировать некоторые конкурирующие проблемы, такие как:

  • Ограничение усиления записи (регулярное объединение больших частей или объединение больших частей с мелкими частями приведет к более высокому усилению записи).

  • Ограничение количества частей (большое количество частей отрицательно повлияет на производительность чтения).

Почему частые и мелкие по объему вставки данных вредны?

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

Он имеет два порога, которые действуют на уровне раздела:

  1. Как только первый порог, parts_to_delay_insert, достигнут, сервер начинает искусственно замедлять вставки, давая фоновым слияниям шанс наверстать упущенное.
    По умолчанию 1000.

  2. Как только достигается второй порог, parts_to_throw_insert, сервер активно отклоняет запросы на вставку и обновление.
    По умолчанию 3000.

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

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

Как посмотреть Parts и Partitions через sql в таблице system.parts?

Таблица семейства MergeTree в ClickHouse состоит из parts. Parts, partitions и другие параметры таблицы описаны в системной таблице system.parts.

Что значат некоторые поля в таблице system.parts:

  • partition (String) – имя партиции.
  • name (String) – имя part (куска).
  • part_type (String) — формат хранения данных в part. Формат хранения данных определяется настройками min_bytes_for_wide_part и min_rows_for_wide_part таблицы MergeTree. Возможные значения:
    • Wide — каждая колонка хранится в отдельном файле.
    • Compact — все колонки хранятся в одном файле.
  • active (UInt8) – признак активности. Если кусок активен, то он используется таблицей, в противном случает он будет удален. Неактивные куски остаются после слияний.
  • marks (UInt64) – количество засечек. Чтобы получить примерное количество строк в куске, умножьте marks на гранулированность индекса (обычно 8192).
  • rows (UInt64) – количество строк.
  • bytes_on_disk (UInt64) – общий размер всех файлов кусков данных в байтах.
  • data_compressed_bytes (UInt64) – общий размер сжатой информации в куске данных. Размер всех дополнительных файлов (например, файлов с засечками) не учитывается.
  • data_uncompressed_bytes (UInt64) – общий размер распакованной информации куска данных. Размер всех дополнительных файлов (например, файлов с засечками) не учитывается.
  • remove_time (DateTime) – время, когда кусок стал неактивным.
  • min_date (Date) – минимальное значение ключа даты в куске данных.
  • max_date (Date) – максимальное значение ключа даты в куске данных.
  • partition_id (String) – ID партиции.
  • level (UInt32) — глубина дерева слияний. Если слияний не было, то level=0.
  • database (String) – имя базы данных.
  • table (String) – имя таблицы.
  • engine (String) – имя движка таблицы, без параметров.
  • path (String) – абсолютный путь к папке с файлами кусков данных.
  • disk (String) – имя диска, на котором находится кусок данных.

Пример запроса к system.parts в ClickHouse:

Результат по таблице с неудаленными parts:

Пример хранения данных в таблице data_mart без партицирования

В рамках первого примера, я создам таблицу без ключа партицирования:

Будет создана таблица с Engine MergeTree с сортировкой данных по колонкам Date, City, Client, Product и первичным ключом по полям, которые указаны в ORDER BY разделе.

Код генерации данных в ClickHouse и их вставки через Google Colab

Ссылка на Google Colab «1. ClickHouse генерация dataframe и вставка 1.3 млн строк без партиций.ipynb»

Вставка данных и слежение за тем, что происходит в таблице с Parts

На gif хорошо видно, что сначала вставляются parts со статусом active = 1 и с типом part_type = Compact. Когда количество частей доходит до 6, все эти части мержатся в одну часть с типом part_type = Wide. У остальных частей active меняется со значения 1 на значение 0.

После того, как вставка завершится и пройдет некоторое время, неактивные части будут удалены.

В итоге останется следующая картинка:

Обратите внимание, что две части в формате Wide, а одна в формате Compact.

Итоговый результат и описание хранения данных ClickHouse MergeTree Table на диске

схема

Хранение данных на диске для таблицы MergeTree table ClickHouse без партицирования

Директория detached содержит parts (куски), отсоединенные от таблицы с помощью запроса DETACH. Поврежденные куски также попадают в эту директорию – они не удаляются с сервера.

Сервер не использует куски из директории detached. Вы можете в любое время добавлять, удалять, модифицировать данные в директории detached — сервер не будет об этом знать, пока вы не сделаете запрос ATTACH.

Полный набор директорий и файлов для таблицы data_mart без партиций выглядит следующим образом (после выполнения всех этапов merging parts):

Пример хранения данных в таблице data_mart с партицированием (Partition By)

В этом разделе будет создана таблица с ключом партицирования в формате YYYYMM (т.е. год месяц):

Код генерации данных в ClickHouse и их вставки через Google Colab с Partition By

Ссылка на Google Colab «2. ClickHouse генерация dataframe и вставка 1 млн строк с партициями.ipynb»

Вставка данных и слежение за тем, что происходит в таблице с Parts

При вставке данных в таблицу MergeTree с партициями появляется последовательно не 1 part, а столько, сколько в Insert данных есть партиций. Для наглядности я сделал всего 2 партиции в генерируемых данных (т.е. партиция 202401 и партиция 202402), поэтому на следующей gif видно появление частей парами:

По мере того, как появляются партиции, при достижении определенного числа партиций происходит объединение Compact частей в Wide части.

Итоговый результат и описание хранения данных ClickHouse MergeTree Table на диске

Схема хранения данных в Parts в случае, если используются партиции: почти все тоже самое, кроме одного — внутри директории одной части содержатся два дополнительных файла:

  • partition.dat
  • minmax_Date.idx

Сначала удалятся неактивные части и картинка приобретет вид:

Итоговый результат после того, как все parts будут смержены и неактивные parts будут удалены:

Полный набор всех файлов для таблицы data_mart_with_partition выглядит следующим образом (снимок файлов в директориях parts выполнен, когда не все части смержены, чтобы показать различие Compact и Wide parts):

Какие выводы можно сделать из проведенных экспериментов

  1. Использование партиций увеличивает число parts на диске.
  2. Partitions позволяют манипулировать данными в таблице через открепление, прикрепление партиций, а также обмен партиций между таблицами.
  3. Если ключ партицирования завязан не на LowCardinality и вставки данных будут частыми и мелкими порциями данных — велика вероятность, что весь процесс встанет.
  4. Использование партиций вероятно приведет к снижению скорости чтения таблицы. То есть Select будет работать медленнее. Опять же все зависит от целей каждой отдельной таблицы в хранилище. Витрины данных идеально делать с минимальным набором частей. То есть подумайте о стратегии обновлении данных и моделировании данных (точнее бизнес-витрин). Если данных немного, идеально партицирование не делать. Если данных много и нужны операции над партициями — думайте о Low Cardinality Dimensions.

Использованные материалы для подготовки статьи по ClickHouse

Подпишись на телеграм канал Data Engineering Инжиниринг данных
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x