121
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Как оптимизировать сложные запросы в Clickhouse

Время чтения 14 минут
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
121
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Приветствую специалистов по данным!

Я Павел Беляев, тимлид группы обработки данных в дата-офисе сервиса eLama и автор канала «Тимлидское об аналитике». Мы разрабатываем и поддерживаем витрины данных на языке SQL.

Известно, что Clickhouse — очень быстрая колоночная СУБД. Она может практически мгновенно отрабатывать простые запросы типа SELECT * FROM t WHERE x. Но дело сильно осложняется, когда добавляются объединения и сложные вычисления. В частности, запросы с джойнами больших таблиц исполняются значительно медленнее или вовсе могут упасть из-за недостатка памяти.

В этой статье я расскажу, какие лайфхаки мы открыли для себя, чтобы оптимизировать сложные запросы в Clickhouse.

Как оптимизировать сложные запросы в Clickhouse

Используйте более легкие таблицы

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

Например, у нас есть таблица transactions, содержащая транзакции всех пользователей, обогащенная датой активации (то есть датой первой транзакции), вычисляемой отдельно и хранимой в таблице date_activation. Число строк transactions равно числу транзакций всех активных пользователей, а количество строк в date_activation соответствует числу самих пользователей. Именно поэтому, если нас не интересуют транзакции, а нужна только дата активации, следует обращаться к date_activation, хотя она выглядит как промежуточная таблица для расчета полноценной витрины с деньгами transactions.

Чем меньше джойнов, тем лучше

Лучше обратиться к одной таблице и потом разделить результат по разным фильтрам, чем обращаться два раза к одной с разными условиями.

Например, можно использовать такой запрос:


SELECT ga.d, regs, regs_p
FROM
( -- события регистраций обычных пользователей в данных из Google Analytics
SELECT DATE(event_timestamp) AS d,
     COUNT(DISTINCT reg_user_id) regs
FROM datamart.ga4_hits
WHERE event_name = 'registration'
GROUP BY 1
) AS ga
 
LEFT JOIN
( -- события регистраций партнеров в данных из Google Analytics
SELECT DATE(event_timestamp) AS d,
     COUNT(DISTINCT reg_user_id) regs_p
FROM datamart.ga4_hits
WHERE event_name = 'registration_partner'
GROUP BY 1
) AS ga_p ON ga_p.d = ga.d
ORDER BY 1 DESC

Но эффективнее будет работать этот:


SELECT DATE(event_timestamp) AS d,
COUNT(DISTINCT IF(event_name='registration', reg_user_id, NULL)) regs,
COUNT(DISTINCT IF(event_name='registration_partner', reg_user_id, NULL)) regs_p
FROM datamart.ga4_hits_archive
WHERE event_name LIKE 'registration%'
GROUP BY 1
ORDER BY 1 DESC

В джойне ставьте более крупную таблицу первой

Clickhouse помещает правую (вторую) таблицу при джойне в память, поэтому для экономии памяти следует в запросе сначала обращаться к таблице помельче.

Если порядок подзапросов важен (LEFT JOIN, а не просто JOIN) и получается так, что к легкому подзапросу присоединяется очень тяжелый, всё равно меняйте порядок подзапросов местами, а LEFT JOIN замените на RIGHT JOIN.

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


SELECT *
FROM
(
SELECT user_id, login, date_registration, email, name, phone
FROM datamart.users -- таблица на 100 тыс. строк
) AS u
 
LEFT JOIN
(
SELECT user_id, price, date_paid, service_name
FROM datamart.transactions -- таблица на 10 млн строк
) AS t  ON u.user_id = t.user_id
 
WHERE t.user_id IS NOT NULL

 

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


SELECT *
FROM
(
SELECT user_id, price, date_paid, service_name
FROM datamart.transactions -- таблица на 10 млн строк
) AS t
 
RIGHT JOIN
(
SELECT user_id, login, date_registration, email, name, phone
FROM datamart.users -- таблица на 100 тыс. строк
) AS u ON u.user_id = t.user_id

 

Clickhouse разместит в памяти легкую users, а не тяжелую transactions, и запрос выполнится гораздо быстрее.

Фильтруйте на ранних этапах

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

Это касается и отсечения дубликатов строк. Например, у нас есть таблица с архивными данными из CRM и таблица свежих, обновляемых данных. Часть строк одной таблицы дублирует строки другой. Чтобы собрать данные из обеих таблиц, можно использовать запрос, в котором уникализация производится в конечном селекте:


SELECT DISTINCT *
FROM
(
SELECT created_at, crm_id, name, contact_data, user_id
FROM datamart.crm
UNION ALL
 
SELECT created_at, crm_id, name, contact_data, user_id
FROM datamart.crm_archive
)

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


SELECT *
FROM
(
SELECT created_at, crm_id, name, contact_data, user_id
FROM datamart.crm
UNION DISTINCT
 
SELECT created_at, crm_id, name, contact_data, user_id
FROM datamart.crm_archive
)

 

В итоге во внешний подзапрос попадает уже меньшее число уникальных строк.

Проверяйте скорость выполнения запроса

Напоследок совет о том, как выяснить, какой запрос шустрее. Запустите последовательно оба сравниваемых запроса с характерным комментарием вначале вроде
-- test variant 1и-- test variant 2соответственно. Затем обратитесь к системному логу запросов, чтобы узнать, как быстро запросы выполнены и сколько ресурсов они потребовали:


SELECT event_time,
query,
    formatReadableSize(memory_usage),
ProfileEvents.Values[indexOf(ProfileEvents.Names, 'UserTimeMicroseconds')] AS userCPU,
ProfileEvents.Values[indexOf(ProfileEvents.Names, 'SystemTimeMicroseconds')] AS systemCPU,
user,
*
FROM system.query_log
WHERE (event_date >= today() - 2) AND (event_time >= (now() - 3600*48))
AND query LIKE '%-- test variant%'
AND type='QueryFinish'
ORDER BY event_time DESC
LIMIT 2;

 

Резюме

Чтобы ускорить выполнение SQL-запросов в Clickhouse, следуйте таким рекомендациям по их оптимизации:

  • Используйте более легкие таблицы всегда, когда это возможно.
  • Сокращайте количество объединений типа JOIN.
  • В джойнах ставьте более крупную таблицу первой.
  • Применяйте фильтрацию и уникализацию как можно раньше.
  • Сравнивайте скорость выполнения запросов с помощью system.query_log.
Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники