В первой части я прошелся по основным понятиям по работе с материализованным представлением и 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 │ └─────────────────────┴────────┴────────┴───────┘
ссылка на оригинал статьи https://habr.com/ru/post/659413/
Добавить комментарий