Этот движок:
- Позволяет быстро записывать состояния объектов, которые постоянно меняются.
- Удаляет старые состояния объектов в фоновом режиме. Это значительно сокращает объем хранимых данных.
Подробности см. в разделе схлопывание.
Движок наследуется от MergeTree и добавляет логику схлопывания строк в алгоритм слияния частей данных. VersionedCollapsingMergeTree служит той же цели, что и CollapsingMergeTree, но использует другой алгоритм схлопывания, который позволяет вставлять данные в любом порядке с использованием нескольких потоков. В частности, столбец Version помогает корректно схлопывать строки, даже если они вставлены в неправильном порядке. В отличие от него, CollapsingMergeTree допускает только строго последовательную вставку.
Создание таблицы
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
...
) ENGINE = VersionedCollapsingMergeTree(sign, version)
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]Описание параметров запроса см. в разделе описание запроса.
Параметры движка
VersionedCollapsingMergeTree(sign, version)| Параметр | Описание | Тип |
|---|---|---|
sign |
Имя столбца с типом строки: 1 — это строка состояния, -1 — это строка отмены. |
Int8 |
version |
Имя столбца с версией состояния объекта. | Int*, UInt*, Date, Date32, DateTime или DateTime64 |
Секции запроса
При создании таблицы VersionedCollapsingMergeTree требуются те же секции, что и при создании таблицы MergeTree.
Устаревший метод создания таблицы
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
...
) ENGINE [=] VersionedCollapsingMergeTree(date-column [, sampling_expression], (primary, key), index_granularity, sign, version)Все параметры, кроме sign и version, имеют то же значение, что и в MergeTree.
-
sign— имя столбца с типом строки:1— это строка состояния,-1— строка отмены.Тип данных столбца —
Int8. -
version— имя столбца с версией состояния объекта.Тип данных столбца должен быть
UInt*.
Схлопывание
Данные
Рассмотрим ситуацию, когда вам нужно сохранять непрерывно изменяющиеся данные некоторого объекта. Логично хранить для объекта одну строку и обновлять её при каждом изменении. Однако операция обновления затратна и медленна для СУБД, поскольку требует перезаписи данных в хранилище. Обновление не подходит, если данные нужно записывать быстро, но вместо этого можно последовательно записывать изменения объекта следующим образом.
При записи строки используйте столбец Sign. Если Sign = 1, это означает, что строка представляет состояние объекта (назовём её строкой состояния). Если Sign = -1, это указывает на отмену состояния объекта с теми же атрибутами (назовём её строкой отмены). Также используйте столбец Version, который должен идентифицировать каждое состояние объекта отдельным числом.
Например, мы хотим вычислить, сколько страниц пользователи посетили на некотором сайте и сколько времени они там провели. В некоторый момент времени мы записываем следующую строку, отражающую состояние активности пользователя:
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ 1 │ 1 |
└─────────────────────┴───────────┴──────────┴──────┴─────────┘Чуть позже мы фиксируем изменение активности пользователя и записываем его следующими двумя строками.
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ -1 │ 1 |
│ 4324182021466249494 │ 6 │ 185 │ 1 │ 2 |
└─────────────────────┴───────────┴──────────┴──────┴─────────┘Первая строка отменяет предыдущее состояние объекта (пользователя). Она должна копировать все поля отменяемого состояния, кроме Sign.
Вторая строка содержит текущее состояние.
Поскольку нам нужно только последнее состояние активности пользователя, строки
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ 1 │ 1 |
│ 4324182021466249494 │ 5 │ 146 │ -1 │ 1 |
└─────────────────────┴───────────┴──────────┴──────┴─────────┘может быть удалено, в результате чего недействительное (старое) состояние объекта схлопывается. VersionedCollapsingMergeTree делает это при слиянии частей данных.
Чтобы узнать, почему для каждого изменения нужны две строки, см. Алгоритм.
Примечания по использованию
- Программа, которая записывает данные, должна помнить состояние объекта, чтобы можно было его отменить. Строка "Cancel" должна содержать копии полей первичного ключа, версию строки "state" и противоположный
Sign. Это увеличивает начальный объем хранилища, но позволяет быстро записывать данные. - Длинные растущие массивы в столбцах снижают эффективность движка из-за нагрузки при записи. Чем проще данные, тем выше эффективность.
- Результаты
SELECTсильно зависят от согласованности истории изменений объекта. Будьте внимательны при подготовке данных для вставки. Несогласованные данные могут приводить к непредсказуемым результатам, например к отрицательным значениям для неотрицательных метрик, таких как глубина сеанса.
Алгоритм
Когда ClickHouse выполняет слияние частей данных, он удаляет каждую пару строк с одинаковыми первичным ключом и версией, но с разным Sign. Порядок строк не имеет значения.
При вставке данных ClickHouse упорядочивает строки по первичному ключу. Если столбец Version не входит в первичный ключ, ClickHouse неявно добавляет его в конец первичного ключа и использует для упорядочивания.
Выборка данных
ClickHouse не гарантирует, что все строки с одинаковым первичным ключом окажутся в одной и той же результирующей части данных или даже на одном и том же физическом сервере. Это верно как для записи данных, так и для последующего слияния частей данных. Кроме того, ClickHouse обрабатывает SELECT-запросы в несколько потоков и не может предсказать порядок строк в результате. Это означает, что, если вам нужно получить полностью «схлопнутые» данные из таблицы VersionedCollapsingMergeTree, необходима агрегация.
Чтобы завершить схлопывание, напишите запрос с предложением GROUP BY и агрегатными функциями, учитывающими знак. Например, чтобы вычислить количество, используйте sum(Sign) вместо count(). Чтобы вычислить сумму чего-либо, используйте sum(Sign * x) вместо sum(x) и добавьте HAVING sum(Sign) > 0.
Агрегатные функции count, sum и avg можно вычислить таким способом. Агрегатную функцию uniq можно вычислить, если у объекта есть хотя бы одно несхлопнутое состояние. Агрегатные функции min и max вычислить нельзя, потому что VersionedCollapsingMergeTree не сохраняет историю значений схлопнутых состояний.
Если вам нужно получить данные со «схлопыванием», но без агрегации (например, чтобы проверить, есть ли строки, у которых последние значения соответствуют определённым условиям), можно использовать модификатор FINAL в предложении FROM. Этот подход неэффективен и не должен использоваться с большими таблицами.
Пример использования
Пример данных:
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ 1 │ 1 |
│ 4324182021466249494 │ 5 │ 146 │ -1 │ 1 |
│ 4324182021466249494 │ 6 │ 185 │ 1 │ 2 |
└─────────────────────┴───────────┴──────────┴──────┴─────────┘Создание таблицы:
CREATE TABLE UAct
(
UserID UInt64,
PageViews UInt8,
Duration UInt8,
Sign Int8,
Version UInt8
)
ENGINE = VersionedCollapsingMergeTree(Sign, Version)
ORDER BY UserIDВставка данных:
INSERT INTO UAct VALUES (4324182021466249494, 5, 146, 1, 1)INSERT INTO UAct VALUES (4324182021466249494, 5, 146, -1, 1),(4324182021466249494, 6, 185, 1, 2)Мы выполняем два запроса INSERT, чтобы создать две разные части данных. Если вставить данные одним запросом, ClickHouse создаст одну часть данных и никогда не выполнит слияние.
Получение данных:
SELECT * FROM UAct┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ 1 │ 1 │
└─────────────────────┴───────────┴──────────┴──────┴─────────┘
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 5 │ 146 │ -1 │ 1 │
│ 4324182021466249494 │ 6 │ 185 │ 1 │ 2 │
└─────────────────────┴───────────┴──────────┴──────┴─────────┘Что мы здесь видим и где находятся схлопнутые части?
Мы создали две части данных с помощью двух запросов INSERT. Запрос SELECT выполнялся в двух потоках, поэтому в результате строки расположены в случайном порядке.
Схлопывания не произошло, потому что части данных ещё не были слиты. ClickHouse выполняет слияние частей данных в непредсказуемый момент времени, который мы не можем заранее определить.
Вот почему нам нужна агрегация:
SELECT
UserID,
sum(PageViews * Sign) AS PageViews,
sum(Duration * Sign) AS Duration,
Version
FROM UAct
GROUP BY UserID, Version
HAVING sum(Sign) > 0┌──────────────UserID─┬─PageViews─┬─Duration─┬─Version─┐
│ 4324182021466249494 │ 6 │ 185 │ 2 │
└─────────────────────┴───────────┴──────────┴─────────┘Если агрегация не нужна и требуется принудительно выполнить схлопывание, можно использовать модификатор FINAL в предложении FROM.
SELECT * FROM UAct FINAL┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┬─Version─┐
│ 4324182021466249494 │ 6 │ 185 │ 1 │ 2 │
└─────────────────────┴───────────┴──────────┴──────┴─────────┘Это очень неэффективный способ выборки данных. Не используйте его для таблиц большого размера.