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

Движок таблицы VersionedCollapsingMergeTree

Этот движок:

  • Обеспечивает быструю запись состояний объектов, которые постоянно изменяются.
  • Удаляет старые состояния объектов в фоновом режиме, что значительно сокращает объём занимаемого места.

См. раздел Collapsing для получения дополнительной информации.

Движок наследуется от 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 — запись «state», -1 — запись «cancel».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 [, samp#table_engines_versionedcollapsingmergetreeling_expression], (primary, key), index_granularity, sign, version)

Все параметры, кроме sign и version, имеют то же значение, что и в MergeTree.

  • sign — имя столбца с типом записи: 1 — запись «state», -1 — запись «cancel».

    Тип данных столбца — Int8.

  • version — имя столбца с версией состояния объекта.

    Тип данных столбца должен быть UInt*.

Коллапсирование

Данные

Рассмотрим ситуацию, когда нужно сохранять постоянно изменяющиеся данные для некоторого объекта. Разумно иметь одну строку на объект и обновлять эту строку при каждом изменении. Однако операция UPDATE для СУБД дорогая и медленная, поскольку требует перезаписи данных в хранилище. Обновление неприемлемо, если нужно быстро записывать данные, но вы можете последовательно записывать изменения объекта следующим образом.

При записи строки используйте столбец Sign. Если Sign = 1, это означает, что строка представляет состояние объекта (назовём её строкой «state»). Если Sign = -1, это указывает на отмену состояния объекта с теми же атрибутами (назовём её строкой «cancel»). Также используйте столбец 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 делает это при слиянии частей данных.

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

Примечания по использованию

  1. Программа, которая записывает данные, должна запоминать состояние объекта, чтобы иметь возможность его отменить. Строка "Cancel" должна содержать копии полей первичного ключа, версию строки "state" и противоположный Sign. Это увеличивает начальный размер хранилища, но позволяет быстро записывать данные.
  2. Длинные постоянно растущие массивы в столбцах снижают эффективность движка из‑за нагрузки на запись. Чем проще данные, тем выше эффективность.
  3. Результаты SELECT сильно зависят от согласованности истории изменений объекта. Будьте внимательны при подготовке данных для вставки. При несогласованных данных вы можете получить непредсказуемые результаты, например отрицательные значения для неотрицательных метрик, таких как глубина сессии.

Algorithm

Когда 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 │
└─────────────────────┴───────────┴──────────┴──────┴─────────┘

Это крайне неэффективный способ выборки данных. Не используйте его для больших таблиц.