Комбинаторы в ClickHouse

от автора

По мере работы приходилось часто сталкиваться с тем, что не все коллеги были знакомы с комбинаторами агрегатных функций в ClickHouse или же ограничивались использованием комбинатора -If. Это побудило меня написать статью. Надеюсь, она окажется для вас полезной.

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

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

Скрипт создания таблицы
CREATE TABLE default.uk_price_paid (     `price` UInt32,     `date` Date,     `postcode1` LowCardinality(String),     `postcode2` LowCardinality(String),     `type` Enum8('other' = 0, 'terraced' = 1, 'semi-detached' = 2, 'detached' = 3, 'flat' = 4),     `is_new` UInt8,     `duration` Enum8('unknown' = 0, 'freehold' = 1, 'leasehold' = 2),     `addr1` String,     `addr2` String,     `street` LowCardinality(String),     `locality` LowCardinality(String),     `town` LowCardinality(String),     `district` LowCardinality(String),     `county` LowCardinality(String) ) ENGINE = MergeTree ORDER BY (postcode1, postcode2, addr1, addr2) SETTINGS index_granularity = 8192

-If комбинатор

Начнем с самых простых, но от этого не менее полезных комбинаторов в ClickHouse. Таким комбинатором является -If. В моей практике он — лидер по частоте использования. Он позволяет указывать дополнительное условие для агрегатной функции. В добавление к полю, по которому мы агрегируем, добавляем условие или можем использовать поле из таблицы, при условии, что она принимает значение 1/0.

К примеру, мы хотим узнать отношение средней цены продажи в определённом районе Лондона, пусть это будет Сити, к средней цене продаж во всем Лондоне с начала 2024 года.

SELECT avgIf(price, district = 'CITY OF LONDON') / avg(price) AS ratio_ FROM uk_price_paid WHERE (date >= '2024-01-01') AND (town = 'LONDON')      ┌────────────ratio_─┐ 1. │ 9.245274507120163 │    └───────────────────┘

Этот комбинатор значительно упрощает написание SQL-запросов и позволяет избежать использования джоинов и подзапросов.

-Distinct

Комбинатор позволяет отбирать для агрегатной функции только уникальные значения.

Скрипт создания и наполнения таблицы
CREATE TABLE random (     `column1` UInt8 ) ENGINE = Memory
INSERT INTO    random   SELECT    *  FROM    generateRandom(     'column1  UInt8', -- нейминг столбца и требуемый тип данных     42 -- random seed   )  LIMIT 10000

Теперь мы имеем таблицы с 10 000 случайных значений, принадлежащих к диапазону от 0 до 255.

SELECT     sum(column1) AS sum_total,     sumDistinct(column1) AS sum_distinct,     sumDistinctIf(column1, column1 < 10) AS sum_distinct_cond FROM random       ┌─sum_total─┬─sum_distinct─┬─sum_distinct_cond─┐ 1. │   1266960 │        32640 │                45 │    └───────────┴──────────────┴───────────────────┘

Получили сумму арифметической прогрессии в случаях sum_distinct и sum_distinct_cond.

State и производные комбинаторы

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

Начнем с -State. Этот комбинатор позволяет сохранить агрегатную функцию в промежуточном состоянии, из которого в дальнейшем можно получить конечный результат. Сейчас, скорее всего, у вас повис вопрос, а зачем это может понадобиться? Есть несколько ситуаций, когда применяют этот комбинатор:

Внутри AggregatingMergeTree — это движок таблиц из семейства MergeTree, подробнее про который можно прочитать по ссылке. Если описать коротко, основой юзер-кейс этого движка — обновление расчёта метрик по мере появления новых данных. Схему можно посмотреть на рисунке ниже.

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

Другой кейс — это экономия места (предагрегированные функции в большинстве кейсов занимают значительно меньше места в сравнении с исходными данными) и возможность построения гибкой витрины данных. Метрики в предагрированном состоянии можно хранить и в обычной таблице из семейства MergeTree.

Перейдем к логике работы комбинатора State. Для каждой функции наполнение предагрегированного состояния отличается: если для функции avg это будет значение числителя и знаменателя, то для функции uniq это будет хэш-таблица.

Пример хранящихся значений для sum и avg

Пример хранящихся значений для sum и avg

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

SELECT     sumState(column1) AS sum_total,     sumDistinctState(column1) AS sum_distinct,     sumDistinctIfState(column1, column1 < 10) AS sum_distinct_cond FROM random

Если мы выполним это запрос, то не получим конечные цифры, а увидим только бинарные данные. Чтобы завершить агрегацию, нам пригодится комбинатор -Merge или функция finalizeAggregation, и мы получим результат, как считали раньше.

Запрос в двух вариантах
SELECT     sumMerge(sum_total) AS sum_total,     sumDistinctMerge(sum_distinct) AS sum_distinct,     sumDistinctIfMerge(sum_distinct_cond) AS sum_distinct_cond FROM (     SELECT         sumState(column1) AS sum_total,         sumDistinctState(column1) AS sum_distinct,         sumDistinctIfState(column1, column1 < 10) AS sum_distinct_cond     FROM random )      ┌─sum_total─┬─sum_distinct─┬─sum_distinct_cond─┐ 1. │   1266960 │        32640 │                45 │    └───────────┴──────────────┴───────────────────┘ ; -- или SELECT     finalizeAggregation(sum_total) AS sum_total,     finalizeAggregation(sum_distinct) AS sum_distinct,     finalizeAggregation(sum_distinct_cond) AS sum_distinct_cond FROM (     SELECT         sumState(column1) AS sum_total,         sumDistinctState(column1) AS sum_distinct,         sumDistinctIfState(column1, column1 < 10) AS sum_distinct_cond     FROM random )     ┌─sum_total─┬─sum_distinct─┬─sum_distinct_cond─┐ 1. │   1266960 │        32640 │                45 │    └───────────┴──────────────┴──────────────────

Также в случае проектирования таблиц нам пригодится такая функция, как toTypeName, которая подскажет, какой тип данных у наших предагрегированных функций (AggregateFunction), что поможет минимизировать вероятность ошибиться с типом данных.

SELECT     toTypeName(sumState(column1)) AS sum_total,     toTypeName(sumDistinctState(column1)) AS sum_distinct,     toTypeName(sumDistinctIfState(column1, column1 < 10)) AS sum_distinct_cond FROM random     ┌─sum_total─────────────────────┬─sum_distinct──────────────────────────┬─sum_distinct_cond──────────────────────────────┐ 1. │ AggregateFunction(sum, UInt8) │ AggregateFunction(sumDistinct, UInt8) │ AggregateFunction(sumDistinctIf, UInt8, UInt8) │    └───────────────────────────────┴───────────────────────────────────────┴────────────────────────────────────────────────┘

Также стоит помнить, что для алиасов, таких как median и другие, будет использоваться функция, отвечающая за расчёт. Например, для median это quantile. Мы это можем увидеть, выполнив следующий запрос:

SELECT toTypeName(medianState(column1)) FROM random      ┌─toTypeName(medianState(column1))───┐ 1. │ AggregateFunction(quantile, UInt8) │    └────────────────────────────────────┘

Если же говорить про комбинатор -MergeState, то он объединяет промежуточные состояния агрегатных функций в одно. К примеру, это может пригодиться при объединении State-функций типа uniq.

Демонстрация сравнения
SELECT uniqMerge(vs) FROM (     SELECT uniqMergeState(column1) AS vs     FROM     (         SELECT uniqState(number) AS column1         FROM numbers(1, 5)         UNION ALL         SELECT uniqState(number) AS column1         FROM numbers(4, 10)     ) )     ┌─uniqMerge(vs)─┐ 1. │            13 │    └───────────────┘

Получаем такой же результат, что и при простом вызове функции uniq.

SELECT uniq(number) AS vs FROM (     SELECT number     FROM numbers(1, 5)     UNION ALL     SELECT number     FROM numbers(4, 10) )    ┌─vs─┐    │ 13 │    └────┘

Рассмотрим ситуацию, когда нам может пригодиться -MergeState. Допустим, у нас есть две таблицы: в одной хранится предагрегированный таймспент пользователей при взаимодействии с видео, а в другой — предагрегированный таймспент пользователей при взаимодействии с аудио. Мы хотим получить общее предагрегированное состояние для пользователей. Можем сделать avgMergeState над предагригированным таймспентом и посчитать, сколько в среднем пользователи проводят времени в нашем продукте, учитывая данные и с аудио, и с видео.

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

SELECT     date,     town,     district,     avgState(price) FROM uk_price_paid GROUP BY     date,     town,     district
Определим нужный тип данных для столбцов таблицы

В этом нам поможет рассмотренная ранее функция toTypeName.

SELECT     toTypeName(date),     toTypeName(town),     toTypeName(district),     toTypeName(avgState)(price) FROM uk_price_paid GROUP BY     date,     town,     district limit 1

Теперь нужно создать подходящую таблицу для этих данных:

CREATE TABLE default.uk_price_paid_agg (     `date` Date,     `town` LowCardinality(String),     `district` LowCardinality(String),     `avg_price` AggregateFunction(avg, UInt32) ) ENGINE = MergeTree ORDER BY (town, district, date) SETTINGS index_granularity = 8192

Посмотрим, сколько заняла наша таблица:

SELECT     database,     `table`,     formatReadableSize(sum(data_compressed_bytes + data_uncompressed_bytes)) AS table_size,     sum(rows) AS rows FROM system.parts WHERE (active = 1) AND (database LIKE '%') AND (`table` LIKE '%') GROUP BY     database,     `table` ORDER BY table_size DESC      ┌─database─┬─table─────────────┬─table_size─┬─────rows─┐ 1. │ default  │ uk_price_paid_agg │ 129.76 MiB │  6469138 │ 2. │ default  │ uk_price_paid     │ 1.02 GiB   │ 29145919 │    └──────────┴───────────────────┴────────────┴──────────┘ 

Таблица занимает почти в 8 раз меньше места, чем источник. Благодаря этой витрине, мы можем гибко считать значение средней цены без обращения к данным в таблице-исходнике, что ускорит выполнение запроса и позволит построить гибкие дашборды, используя только агрегированную витрину. То есть, если мы будем фильтровать данные по городам или датам с районами, мы всегда будем получать корректное значение среднего. Это позволяет строить дашборды над источниками с большим количеством измерений для более удобной визуализации.


ссылка на оригинал статьи https://habr.com/ru/articles/825264/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *