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

CollapsingMergeTree

Описание

Двигатель CollapsingMergeTree наследуется от MergeTree и добавляет логику для коллапса строк в процессе слияния. Двигатель таблицы CollapsingMergeTree асинхронно удаляет (коллапсирует) пары строк, если все поля в ключе сортировки (ORDER BY) эквивалентны, за исключением специального поля Sign, которое может принимать значения 1 или -1. Строки без пары с противоположным значением Sign остаются.

Для получения более подробной информации смотрите раздел Collapsing документа.

примечание

Этот движок может значительно сократить объем хранения, что, в свою очередь, повысит эффективность запросов SELECT.

Параметры

Все параметры этого движка таблицы, за исключением параметра Sign, имеют то же значение, что и в MergeTree.

  • Sign — Имя столбца с типом строки, где 1 соответствует строке "состояния", а -1 соответствует строке "отмены". Тип: Int8.

Создание таблицы

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) 
ENGINE = CollapsingMergeTree(Sign)
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]
Устаревший метод создания таблицы
примечание

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

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) 
ENGINE [=] CollapsingMergeTree(date-column [, sampling_expression], (primary, key), index_granularity, Sign)

Sign — Имя столбца с типом строки, где 1 соответствует строке "состояния", а -1 соответствует строке "отмены". Int8.

  • Для описания параметров запросов смотрите описание запроса.
  • При создании таблицы CollapsingMergeTree требуются те же клаузы запроса, что и при создании таблицы MergeTree.

Коллапс

Данные

Рассмотрим ситуацию, когда необходимо сохранять постоянно изменяющиеся данные для некоего объекта. Может показаться логичным иметь одну строку на объект и обновлять ее каждый раз, когда что-то меняется, однако операции обновления дороги и медленны для СУБД, так как они требуют перезаписи данных в хранилище. Если мы хотим быстро записывать данные, выполнение большого количества обновлений не является приемлемым подходом, но мы всегда можем записывать изменения объекта последовательно. Для этого мы используем специальный столбец Sign.

  • Если Sign = 1, это означает, что строка является строкой "состояния": строка, содержащая поля, которые представляют текущее корректное состояние.
  • Если Sign = -1, это означает, что строка является строкой "отмены": строка, используемая для отмены состояния объекта с теми же атрибутами.

Например, мы хотим подсчитать, сколько страниц пользователи просмотрели на каком-то веб-сайте и как долго они находились на них. В определенный момент времени мы записываем следующую строку с состоянием активности пользователей:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │         5 │      146 │    1 │
└─────────────────────┴───────────┴──────────┴──────┘

В более поздний момент времени мы регистрируем изменение активности пользователя и записываем его с помощью следующих двух строк:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │         5 │      146 │   -1 │
│ 4324182021466249494 │         6 │      185 │    1 │
└─────────────────────┴───────────┴──────────┴──────┘

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

Поскольку нам нужно лишь последнее состояние активности пользователя, мы можем удалить оригинальную строку "состояния" и строку "отмены", как показано ниже, коллапсируя недействительное (старое) состояние объекта:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │         5 │      146 │    1 │ -- old "state" row can be deleted
│ 4324182021466249494 │         5 │      146 │   -1 │ -- "cancel" row can be deleted
│ 4324182021466249494 │         6 │      185 │    1 │ -- new "state" row remains
└─────────────────────┴───────────┴──────────┴──────┘

CollapsingMergeTree выполняет именно такое коллапсирование во время слияния частей данных.

примечание

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

Особенности такого подхода

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

Алгоритм

Когда ClickHouse объединяет части данных, каждая группа последовательных строк с одним и тем же ключом сортировки (ORDER BY) сокращается до не более чем двух строк: строка "состояния" с Sign = 1 и строка "отмены" с Sign = -1. Другими словами, в ClickHouse записи коллапсируются.

Для каждой полученной части данных ClickHouse сохраняет:

1.Первая строка "отмены" и последняя строка "состояния", если количество строк "состояния" и "отмены" совпадает и последняя строка является строкой "состояния".
2.Последняя строка "состояния", если строк "состояния" больше, чем строк "отмены".
3.Первая строка "отмены", если строк "отмены" больше, чем строк "состояния".
4.Ни одной из строк, в остальных случаях.

Кроме того, когда количество строк "состояния" на две и более превышает количество строк "отмены", или наоборот, слияние продолжается. Однако ClickHouse рассматривает эту ситуацию как логическую ошибку и записывает ее в журнал сервера. Эта ошибка может возникнуть, если одни и те же данные вставляются более одного раза. Таким образом, коллапсирование не должно изменять результаты расчета статистики. Изменения постепенно коллапсируются, так что в конце остается только последнее состояние почти каждого объекта.

Столбец Sign обязателен, потому что алгоритм слияния не гарантирует, что все строки с одним и тем же ключом сортировки будут находиться в одной и той же результирующей части данных и даже на одном физическом сервере. ClickHouse обрабатывает запросы SELECT с использованием нескольких потоков, и он не может предсказать порядок строк в результате.

Агрегация необходима, если требуется получить полностью "коллапсированные" данные из таблицы CollapsingMergeTree. Для завершения коллапсирования запишите запрос с клаузой GROUP BY и агрегатными функциями, которые учитывают знак. Например, чтобы подсчитать количество, используйте sum(Sign) вместо count(). Чтобы подсчитать сумму чего-либо, используйте sum(Sign * x) с HAVING sum(Sign) > 0 вместо sum(x) так, как показано в примере ниже.

Агрегаты count, sum и avg можно вычислить таким образом. Агрегат uniq можно вычислить, если у объекта есть хотя бы одно неколлапсированное состояние. Агрегаты min и max нельзя вычислить, так как CollapsingMergeTree не сохраняет историю коллапсированных состояний.

примечание

Если вам нужно извлечь данные без агрегации (например, чтобы проверить, есть ли строки, значения которых соответствуют определенным условиям), вы можете использовать модификатор FINAL для клаузи FROM. Он объединит данные перед возвратом результата. Для CollapsingMergeTree возвращается только последняя строка состояния для каждого ключа.

Примеры

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

При наличии следующих примерных данных:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │         5 │      146 │    1 │
│ 4324182021466249494 │         5 │      146 │   -1 │
│ 4324182021466249494 │         6 │      185 │    1 │
└─────────────────────┴───────────┴──────────┴──────┘

Создадим таблицу UAct, используя CollapsingMergeTree:

CREATE TABLE UAct
(
    UserID UInt64,
    PageViews UInt8,
    Duration UInt8,
    Sign Int8
)
ENGINE = CollapsingMergeTree(Sign)
ORDER BY UserID

Далее мы вставим некоторые данные:

INSERT INTO UAct VALUES (4324182021466249494, 5, 146, 1)
INSERT INTO UAct VALUES (4324182021466249494, 5, 146, -1),(4324182021466249494, 6, 185, 1)

Мы используем два запроса INSERT, чтобы создать две разные части данных.

примечание

Если мы вставим данные одним запросом, ClickHouse создаст только одну часть данных и не выполнит никаких слияний.

Мы можем выбрать данные с помощью:

SELECT * FROM UAct
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │         5 │      146 │   -1 │
│ 4324182021466249494 │         6 │      185 │    1 │
└─────────────────────┴───────────┴──────────┴──────┘
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │         5 │      146 │    1 │
└─────────────────────┴───────────┴──────────┴──────┘

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

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

SELECT
    UserID,
    sum(PageViews * Sign) AS PageViews,
    sum(Duration * Sign) AS Duration
FROM UAct
GROUP BY UserID
HAVING sum(Sign) > 0
┌──────────────UserID─┬─PageViews─┬─Duration─┐
│ 4324182021466249494 │         6 │      185 │
└─────────────────────┴───────────┴──────────┘

Если нам не нужна агрегация и мы хотим принудительное коллапсирование, мы также можем использовать модификатор FINAL для клаузи FROM.

SELECT * FROM UAct FINAL
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │         6 │      185 │    1 │
└─────────────────────┴───────────┴──────────┴──────┘
примечание

Этот способ выборки данных менее эффективен и не рекомендуется для использования с большими объемами сканируемых данных (миллионы строк).

Пример другого подхода

Идея этого подхода заключается в том, что слияния учитывают только ключевые поля. В строке "отмены" мы можем, следовательно, указывать отрицательные значения, которые уравнивают предыдущую версию строки при суммировании без использования столбца Sign.

Для этого примера мы воспользуемся приведенными ниже примерными данными:

┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │         5 │      146 │    1 │
│ 4324182021466249494 │        -5 │     -146 │   -1 │
│ 4324182021466249494 │         6 │      185 │    1 │
└─────────────────────┴───────────┴──────────┴──────┘

Для этого подхода необходимо изменить типы данных PageViews и Duration, чтобы хранить отрицательные значения. Поэтому мы изменяем значения этих столбцов с UInt8 на Int16, когда создаем нашу таблицу UAct, используя collapsingMergeTree:

CREATE TABLE UAct
(
    UserID UInt64,
    PageViews Int16,
    Duration Int16,
    Sign Int8
)
ENGINE = CollapsingMergeTree(Sign)
ORDER BY UserID

Давайте протестируем подход, вставив данные в нашу таблицу.

Для примеров или небольших таблиц это, однако, приемлемо:

INSERT INTO UAct VALUES(4324182021466249494,  5,  146,  1);
INSERT INTO UAct VALUES(4324182021466249494, -5, -146, -1);
INSERT INTO UAct VALUES(4324182021466249494,  6,  185,  1);

SELECT * FROM UAct FINAL;
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │         6 │      185 │    1 │
└─────────────────────┴───────────┴──────────┴──────┘
SELECT
    UserID,
    sum(PageViews) AS PageViews,
    sum(Duration) AS Duration
FROM UAct
GROUP BY UserID
┌──────────────UserID─┬─PageViews─┬─Duration─┐
│ 4324182021466249494 │         6 │      185 │
└─────────────────────┴───────────┴──────────┘
SELECT COUNT() FROM UAct
┌─count()─┐
│       3 │
└─────────┘
OPTIMIZE TABLE UAct FINAL;

SELECT * FROM UAct
┌──────────────UserID─┬─PageViews─┬─Duration─┬─Sign─┐
│ 4324182021466249494 │         6 │      185 │    1 │
└─────────────────────┴───────────┴──────────┴──────┘