В первой части я прошелся по основным понятиям по работе с материализованным представлением и ReplacingMergeTree в ClickHouse. Разобрал особенности, основные преимущества и недостатки. В этой части я покажу как это работает вместе.

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

Таблица-источник (максимальное время ответа по-секундно):

CREATE TABLE responses_time (
    time DateTime,
    app String,
    max_time UInt16
) engine=ReplacingMergeTree()
ORDER BY (time, app)

Материализованное представление:

CREATE MATERIALIZED VIEW responses_time_by_day_mat_view TO responses_time_by_day
(
    day DateTime,
    app String,
    avg_time UInt16
) AS
SELECT
    time AS day,
    app,
    avg(max_time) as avg_time
FROM responses_time
GROUP BY
    toDateTime((intDiv(toUInt32(time), 86400) * 86400)) as time,
    app

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

  • Создать отдельную агрегированную таблицу:

CREATE TABLE responses_time_by_day
(
    day DateTime,
    app String,
    avg_time UInt16
) engine=MergeTree()
ORDER BY (day, app)
  • Раз в день (допустим в 00:00) запускать запрос:

SELECT
    toDateTime((intDiv(toUInt32(time), 86400) * 86400)) AS day,
    app,
    avg(max_time) as avg_time
FROM responses_time
WHERE time > yesterday()
GROUP BY app
  • Результаты вставлять в агрегат.

А теперь более подходящий пример. Представим, что собираем не время ответа, а статусы. Данные по суммарному количеству каждого статуса для каждого приложения необходимо показывать с агрегацией по дням/неделям/месяцам за последние полгода/год. При сотнях или тысячах приложений таблица заполнится очень быстро, поэтому снова требуется агрегация и хочется чтобы она работала автоматически без лишних приседаний. И важное условие: данные попадают в таблицу по крону, который берет какой-то последний промежуток времени из другой системы и перекладывает данные в ClickHouse (суммарно по-секундно и это уже околореальный пример из жизни). Но в этом подходе сразу возникают нюансы, ведь что-то может пойти не так. Например, прошлая итерация упала и образовалась дыра в метриках. Причем для разных приложений последнее сохраненное время оказалось тоже разным. Чтобы об этом всем не беспокоиться можно использовать ReplacingMergeTree для таблицы источника и период синка брать побольше. Но самое интересное это что тогда мы увидим в агрегате, если данные в таблице-источнике повторились? В части первой я описывал, что ClickHouse не сразу удаляет дубликаты.

Итак, таблица-источник:

CREATE TABLE responses_status (
    time DateTime,
    app String,
    status UInt16,
    count UInt32
) engine=ReplacingMergeTree()
ORDER BY (time, app, status)

Агрегированная таблица:

CREATE TABLE responses_status_by_min
(
    time DateTime,
    app String,
    status UInt16,
    count UInt32
) engine=SummingMergeTree()
ORDER BY (time, app, status)

Здесь уже использован движок SummingMergeTree. Он отлично подходит для текущей задачи так как:

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

То есть будет суммировать поле count и уже не важно в одном запросе это было вставлено или нет.

Ниже показано само мат представление, которое суммирует данные поминутно (это уменьшает размер данных для построения отчетов в десятки и сотни раз).

CREATE MATERIALIZED VIEW responses_status_mat_view TO responses_status_by_min AS
SELECT 
    toDateTime((intDiv(toUInt32(time), 60) * 60)) as time,
    app,
    status,
    sum(count) as count
FROM responses_status
GROUP BY time, app, status

Вставка данных:

INSERT INTO responses_status (time,app,status,count) VALUES
('2021-10-11 12:00:00', 'search', 200, 3),
('2021-10-11 12:00:00', 'search', 500, 1),
('2021-10-11 12:00:01', 'search', 200, 3),
('2021-10-11 12:00:01', 'search', 500, 2),
('2021-10-11 12:00:00', 'api', 200, 1),
('2021-10-11 12:00:00', 'api', 500, 5),
('2021-10-11 12:00:01', 'api', 200, 1),
('2021-10-11 12:00:01', 'api', 500, 1)

Результат:

SELECT * FROM responses_status_by_min

┌────────────────time─┬─app────┬─status─┬─count─┐
│ 2021-10-11 12:00:00 │ api    │    200 │     2 │
│ 2021-10-11 12:00:00 │ api    │    500 │     6 │
│ 2021-10-11 12:00:00 │ search │    200 │     6 │
│ 2021-10-11 12:00:00 │ search │    500 │     3 │
└─────────────────────┴────────┴────────┴───────┘

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

INSERT INTO responses_status (time,app,status,count) VALUES
('2021-10-11 12:00:02', 'search', 200, 3),
('2021-10-11 12:00:02', 'search', 500, 1),
('2021-10-11 12:00:03', 'search', 200, 3),
('2021-10-11 12:00:03', 'search', 500, 2),
('2021-10-11 12:00:02', 'api', 200, 1),
('2021-10-11 12:00:02', 'api', 500, 5),
('2021-10-11 12:00:03', 'api', 200, 1),
('2021-10-11 12:00:03', 'api', 500, 1)

Результат:

SELECT * FROM responses_status_by_min

┌────────────────time─┬─app────┬─status─┬─count─┐
│ 2021-10-11 12:00:00 │ api    │    200 │     2 │
│ 2021-10-11 12:00:00 │ api    │    500 │     6 │
│ 2021-10-11 12:00:00 │ search │    200 │     6 │
│ 2021-10-11 12:00:00 │ search │    500 │     3 │
└─────────────────────┴────────┴────────┴───────┘
┌────────────────time─┬─app────┬─status─┬─count─┐
│ 2021-10-11 12:00:00 │ api    │    200 │     2 │
│ 2021-10-11 12:00:00 │ api    │    500 │     6 │
│ 2021-10-11 12:00:00 │ search │    200 │     6 │
│ 2021-10-11 12:00:00 │ search │    500 │     3 │
└─────────────────────┴────────┴────────┴───────┘

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

SELECT * FROM responses_status_by_min FINAL

┌────────────────time─┬─app────┬─status─┬─count─┐
│ 2021-10-11 12:00:00 │ api    │    200 │     4 │
│ 2021-10-11 12:00:00 │ search │    500 │     6 │
└─────────────────────┴────────┴────────┴───────┘
┌────────────────time─┬─app────┬─status─┬─count─┐
│ 2021-10-11 12:00:00 │ api    │    500 │    12 │
│ 2021-10-11 12:00:00 │ search │    200 │    12 │
└─────────────────────┴────────┴────────┴───────┘

Все верно. А теперь посмотрим что будет при появлении дубликатов. Для чистоты эксперимента я предварительно сделал TRUNCATE обеих таблиц. После этого сделал две одинаковые вставки:

INSERT INTO responses_status (time,app,status,count) VALUES
('2021-10-11 12:00:00', 'search', 200, 3),
('2021-10-11 12:00:00', 'search', 500, 1),
('2021-10-11 12:00:01', 'search', 200, 3),
('2021-10-11 12:00:01', 'search', 500, 2),
('2021-10-11 12:00:00', 'api', 200, 1),
('2021-10-11 12:00:00', 'api', 500, 5),
('2021-10-11 12:00:01', 'api', 200, 1),
('2021-10-11 12:00:01', 'api', 500, 1)

INSERT INTO responses_status (time,app,status,count) VALUES
('2021-10-11 12:00:00', 'search', 200, 3),
('2021-10-11 12:00:00', 'search', 500, 1),
('2021-10-11 12:00:01', 'search', 200, 3),
('2021-10-11 12:00:01', 'search', 500, 2),
('2021-10-11 12:00:00', 'api', 200, 1),
('2021-10-11 12:00:00', 'api', 500, 5),
('2021-10-11 12:00:01', 'api', 200, 1),
('2021-10-11 12:00:01', 'api', 500, 1)

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

SELECT * FROM responses_status_by_min FINAL

┌────────────────time─┬─app────┬─status─┬─count─┐
│ 2021-10-11 12:00:00 │ api    │    200 │     4 │
│ 2021-10-11 12:00:00 │ search │    500 │     6 │
└─────────────────────┴────────┴────────┴───────┘
┌────────────────time─┬─app────┬─status─┬─count─┐
│ 2021-10-11 12:00:00 │ api    │    500 │    12 │
│ 2021-10-11 12:00:00 │ search │    200 │    12 │
└─────────────────────┴────────┴────────┴───────┘

Комментарии (0)