26 KiB
MergeTree
Движок MergeTree
, а также другие движки этого семейства (*MergeTree
) — это наиболее функциональные движки таблиц ClickHousе.
Основная идея, заложенная в основу движков семейства MergeTree
следующая. Когда у вас есть огромное количество данных, которые должны быть вставлены в таблицу, вы должны быстро записать их по частям, а затем объединить части по некоторым правилам в фоновом режиме. Этот метод намного эффективнее, чем постоянная перезапись данных в хранилище при вставке.
Основные возможности:
-
Хранит данные, отсортированные по первичному ключу.
Это позволяет создавать разреженный индекс небольшого объёма, который позволяет быстрее находить данные.
-
Позволяет оперировать партициями, если задан ключ партиционирования.
ClickHouse поддерживает отдельные операции с партициями, которые работают эффективнее, чем общие операции с этим же результатом над этими же данными. Также, ClickHouse автоматически отсекает данные по партициям там, где ключ партиционирования указан в запросе. Это также увеличивает эффективность выполнения запросов.
-
Поддерживает репликацию данных.
Для этого используется семейство таблиц
ReplicatedMergeTree
. Подробнее читайте в разделе Репликация данных. -
Поддерживает сэмплирование данных.
При необходимости можно задать способ сэмплирования данных в таблице.
!!! info
Движок Merge не относится к семейству *MergeTree
.
Создание таблицы
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
...
INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2
) ENGINE = MergeTree()
[PARTITION BY expr]
[ORDER BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]
Описание параметров запроса смотрите в описании запроса.
Секции запроса
-
ENGINE
— Имя и параметры движка.ENGINE = MergeTree()
.MergeTree
не имеет параметров. -
PARTITION BY
— ключ партиционирования.Для партиционирования по месяцам используйте выражение
toYYYYMM(date_column)
, гдеdate_column
— столбец с датой типа Date. В этом случае имена партиций имеют формат"YYYYMM"
. -
ORDER BY
— ключ сортировки.Кортеж столбцов или произвольных выражений. Пример:
ORDER BY (CounterID, EventDate)
. -
PRIMARY KEY
— первичный ключ, если он отличается от ключа сортировки.По умолчанию первичный ключ совпадает с ключом сортировки (который задаётся секцией
ORDER BY
.) Поэтому в большинстве случаев секциюPRIMARY KEY
отдельно указывать не нужно. -
SAMPLE BY
— выражение для сэмплирования.Если используется выражение для сэмплирования, то первичный ключ должен содержать его. Пример:
SAMPLE BY intHash32(UserID) ORDER BY (CounterID, EventDate, intHash32(UserID))
. -
SETTINGS
— дополнительные параметры, регулирующие поведениеMergeTree
:index_granularity
— гранулярность индекса. Число строк данных между «засечками» индекса. По умолчанию — 8192. Список всех доступных параметров можно посмотреть в MergeTreeSettings.h.min_merge_bytes_to_use_direct_io
— минимальный объем данных, необходимый для прямого (небуферизованного) чтения/записи (direct I/O) на диск. При слиянии частей данных ClickHouse вычисляет общий объем хранения всех данных, подлежащих слиянию. Если общий объем хранения всех данных для чтения превышаетmin_bytes_to_use_direct_io
байт, тогда ClickHouse использует флагO_DIRECT
при чтении данных с диска. Еслиmin_merge_bytes_to_use_direct_io = 0
, тогда прямой ввод-вывод отключен. Значение по умолчанию:10 * 1024 * 1024 * 1024
байт.
Пример задания секций
ENGINE MergeTree() PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate, intHash32(UserID)) SAMPLE BY intHash32(UserID) SETTINGS index_granularity=8192
В примере мы устанавливаем партиционирование по месяцам.
Также мы задаем выражение для сэмплирования в виде хэша по идентификатору посетителя. Это позволяет псевдослучайным образом перемешать данные в таблице для каждого CounterID
и EventDate
. Если при выборке данных задать секцию SAMPLE, то ClickHouse вернёт равномерно-псевдослучайную выборку данных для подмножества посетителей.
index_granularity
можно было не указывать, поскольку 8192 — это значение по умолчанию.
Устаревший способ создания таблицы
!!! attention Не используйте этот способ в новых проектах и по возможности переведите старые проекты на способ, описанный выше.
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
...
) ENGINE [=] MergeTree(date-column [, sampling_expression], (primary, key), index_granularity)
Параметры MergeTree()
date-column
— имя столбца с типом Date. На основе этого столбца ClickHouse автоматически создаёт партиции по месяцам. Имена партиций имеют формат"YYYYMM"
.sampling_expression
— выражение для сэмплирования.(primary, key)
— первичный ключ. Тип — [Tuple()](../../data_types/tuple.md-index_granularity
— гранулярность индекса. Число строк данных между «засечками» индекса. Для большинства задач подходит значение 8192.
Пример
MergeTree(EventDate, intHash32(UserID), (CounterID, EventDate, intHash32(UserID)), 8192)
Движок MergeTree
сконфигурирован таким же образом, как и в примере выше для основного способа конфигурирования движка.
Хранение данных
Таблица состоит из кусков данных (data parts), отсортированных по первичному ключу.
При вставке в таблицу создаются отдельные куски данных, каждый из которых лексикографически отсортирован по первичному ключу. Например, если первичный ключ — (CounterID, Date)
, то данные в куске будут лежать в порядке CounterID
, а для каждого CounterID
в порядке Date
.
Данные, относящиеся к разным партициям, разбиваются на разные куски. В фоновом режиме ClickHouse выполняет слияния (merge) кусков данных для более эффективного хранения. Куски, относящиеся к разным партициям не объединяются. Механизм слияния не гарантирует, что все строки с одинаковым первичным ключом окажутся в одном куске.
Для каждого куска данных ClickHouse создаёт индексный файл, который содержит значение первичного ключа для каждой индексной строки («засечка»). Номера строк индекса определяются как n * index_granularity
. Максимальное значение n
равно целой части деления общего числа строк на index_granularity
. Для каждого столбца "засечки" также записываются для тех же строк индекса, что и первичный ключ. Эти "засечки" позволяют находить данные непосредственно в столбцах.
Вы можете использовать одну большую таблицу, постоянно добавляя в неё данные пачками, именно для этого предназначен движок MergeTree
.
Первичные ключи и индексы в запросах
Рассмотрим первичный ключ — (CounterID, Date)
. В этом случае сортировку и индекс можно проиллюстрировать следующим образом:
Whole data: [-------------------------------------------------------------------------]
CounterID: [aaaaaaaaaaaaaaaaaabbbbcdeeeeeeeeeeeeefgggggggghhhhhhhhhiiiiiiiiikllllllll]
Date: [1111111222222233331233211111222222333211111112122222223111112223311122333]
Marks: | | | | | | | | | | |
a,1 a,2 a,3 b,3 e,2 e,3 g,1 h,2 i,1 i,3 l,3
Marks numbers: 0 1 2 3 4 5 6 7 8 9 10
Если в запросе к данным указать:
CounterID IN ('a', 'h')
, то сервер читает данные в диапазонах засечек[0, 3)
и[6, 8)
.CounterID IN ('a', 'h') AND Date = 3
, то сервер читает данные в диапазонах засечек[1, 3)
и[7, 8)
.Date = 3
, то сервер читает данные в диапазоне засечек[1, 10]
.
Примеры выше показывают, что использование индекса всегда эффективнее, чем full scan.
Разреженный индекс допускает чтение лишних строк. При чтении одного диапазона первичного ключа, может быть прочитано до index_granularity * 2
лишних строк в каждом блоке данных. В большинстве случаев ClickHouse не теряет производительности при index_granularity = 8192
.
Разреженность индекса позволяет работать даже с очень большим количеством строк в таблицах, поскольку такой индекс всегда помещается в оперативную память компьютера.
ClickHouse не требует уникального первичного ключа. Можно вставить много строк с одинаковым первичным ключом.
Выбор первичного ключа
Количество столбцов в первичном ключе не ограничено явным образом. В зависимости от структуры данных в первичный ключ можно включать больше или меньше столбцов. Это может:
-
Увеличить эффективность индекса.
Пусть первичный ключ —
(a, b)
, тогда добавление ещё одного столбцаc
повысит эффективность, если выполнены условия:- Есть запросы с условием на столбец
c
. - Часто встречаются достаточно длинные (в несколько раз больше
index_granularity
) диапазоны данных с одинаковыми значениями(a, b)
. Иначе говоря, когда добавление ещё одного столбца позволит пропускать достаточно длинные диапазоны данных.
- Есть запросы с условием на столбец
-
Улучшить сжатие данных.
ClickHouse сортирует данные по первичному ключу, поэтому чем выше однородность, тем лучше сжатие.
-
Обеспечить дополнительную логику при слиянии кусков данных в движках CollapsingMergeTree и SummingMergeTree.
В этом случае имеет смысл указать отдельный ключ сортировки, отличающийся от первичного ключа.
Длинный первичный ключ будет негативно влиять на производительность вставки и потребление памяти, однако на производительность ClickHouse при запросах SELECT
лишние столбцы в первичном ключе не влияют.
Первичный ключ, отличный от ключа сортировки
Существует возможность задать первичный ключ (выражение, значения которого будут записаны в индексный файл для каждой засечки), отличный от ключа сортировки (выражение, по которому будут упорядочены строки в кусках данных). Кортеж выражения первичного ключа при этом должен быть префиксом кортежа выражения ключа сортировки.
Данная возможность особенно полезна при использовании движков SummingMergeTree
и AggregatingMergeTree. В типичном сценарии использования этих движков таблица
содержит столбцы двух типов: измерения (dimensions) и меры (measures). Типичные запросы агрегируют
значения столбцов-мер с произвольной группировкой и фильтрацией по измерениям. Так как SummingMergeTree
и AggregatingMergeTree
производят фоновую агрегацию строк с одинаковым значением ключа сортировки, приходится
добавлять в него все столбцы-измерения. В результате выражение ключа содержит большой список столбцов,
который приходится постоянно расширять при добавлении новых измерений.
В этом сценарии имеет смысл оставить в первичном ключе всего несколько столбцов, которые обеспечат эффективную фильтрацию по индексу, а остальные столбцы-измерения добавить в выражение ключа сортировки.
ALTER ключа сортировки — лёгкая операция, так как при одновременном добавлении нового столбца в таблицу и ключ сортировки не нужно изменять данные кусков (они остаются упорядоченными и по новому выражению ключа).
Использование индексов и партиций в запросах
Для запросов SELECT
ClickHouse анализирует возможность использования индекса. Индекс может использоваться, если в секции WHERE/PREWHERE
, в качестве одного из элементов конъюнкции, или целиком, есть выражение, представляющее операции сравнения на равенства, неравенства, а также IN
или LIKE
с фиксированным префиксом, над столбцами или выражениями, входящими в первичный ключ или ключ партиционирования, либо над некоторыми частично монотонными функциями от этих столбцов, а также логические связки над такими выражениями.
Таким образом, обеспечивается возможность быстро выполнять запросы по одному или многим диапазонам первичного ключа. Например, в указанном примере будут быстро работать запросы для конкретного счётчика; для конкретного счётчика и диапазона дат; для конкретного счётчика и даты, для нескольких счётчиков и диапазона дат и т. п.
Рассмотрим движок сконфигурированный следующим образом:
ENGINE MergeTree() PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate) SETTINGS index_granularity=8192
В этом случае в запросах:
SELECT count() FROM table WHERE EventDate = toDate(now()) AND CounterID = 34
SELECT count() FROM table WHERE EventDate = toDate(now()) AND (CounterID = 34 OR CounterID = 42)
SELECT count() FROM table WHERE ((EventDate >= toDate('2014-01-01') AND EventDate <= toDate('2014-01-31')) OR EventDate = toDate('2014-05-01')) AND CounterID IN (101500, 731962, 160656) AND (CounterID = 101500 OR EventDate != toDate('2014-05-01'))
ClickHouse будет использовать индекс по первичному ключу для отсечения не подходящих данных, а также ключ партиционирования по месяцам для отсечения партиций, которые находятся в не подходящих диапазонах дат.
Запросы выше показывают, что индекс используется даже для сложных выражений. Чтение из таблицы организовано так, что использование индекса не может быть медленнее, чем full scan.
В примере ниже индекс не может использоваться.
SELECT count() FROM table WHERE CounterID = 34 OR URL LIKE '%upyachka%'
Чтобы проверить, сможет ли ClickHouse использовать индекс при выполнении запроса, используйте настройки force_index_by_date и force_primary_key.
Ключ партиционирования по месяцам обеспечивает чтение только тех блоков данных, которые содержат даты из нужного диапазона. При этом блок данных может содержать данные за многие даты (до целого месяца). В пределах одного блока данные упорядочены по первичному ключу, который может не содержать дату в качестве первого столбца. В связи с этим, при использовании запроса с указанием условия только на дату, но не на префикс первичного ключа, будет читаться данных больше, чем за одну дату.
Дополнительные индексы (Экспериментальная функциональность)
Для использования требуется установить настройку allow_experimental_data_skipping_indices
в 1. (запустить SET allow_experimental_data_skipping_indices = 1
).
Объявление индексов при определении столбцов в запросе CREATE
.
INDEX index_name expr TYPE type(...) GRANULARITY granularity_value
Для таблиц семейства *MergeTree
можно задать дополнительные индексы в секции столбцов.
Индексы агрегируют для заданного выражения некоторые данные, а потом при SELECT
запросе используют для пропуска блоков данных (пропускаемый блок состоит из гранул данных в количестве равном гранулярности данного индекса), на которых секция WHERE
не может быть выполнена, тем самым уменьшая объем данных читаемых с диска.
Пример
CREATE TABLE table_name
(
u64 UInt64,
i32 Int32,
s String,
...
INDEX a (u64 * i32, s) TYPE minmax GRANULARITY 3,
INDEX b (u64 * length(s)) TYPE set(1000) GRANULARITY 4
) ENGINE = MergeTree()
...
Эти индексы смогут использоваться для оптимизации следующих запросов
SELECT count() FROM table WHERE s < 'z'
SELECT count() FROM table WHERE u64 * i32 == 10 AND u64 * length(s) >= 1234
Доступные индексы
-
minmax
Хранит минимум и максимум выражения (если выражение -tuple
, то для каждого элементаtuple
), используя их для пропуска блоков аналогично первичному ключу. -
set(max_rows)
Хранит уникальные значения выражения на блоке в количестве не болееmax_rows
(еслиmax_rows = 0
, то ограничений нет), используя их для пропуска блоков, оценивая выполнимостьWHERE
выражения на хранимых данных.
Примеры
INDEX b (u64 * length(str), i32 + f64 * 100, date, str) TYPE minmax GRANULARITY 4
INDEX b (u64 * length(str), i32 + f64 * 100, date, str) TYPE set(100) GRANULARITY 4
Конкурентный доступ к данным
Для конкурентного доступа к таблице используется мультиверсионность. То есть, при одновременном чтении и обновлении таблицы, данные будут читаться из набора кусочков, актуального на момент запроса. Длинных блокировок нет. Вставки никак не мешают чтениям.
Чтения из таблицы автоматически распараллеливаются.