Открываем вкладку "Exec" выполняем команду "bash". Сохраняем все запросы, выполняемые при открытии дашборда, в файл queries.tsv
clickhouse-client --query="SELECT query FROM system.query_log WHERE NOT query LIKE '%query_log%' AND http_user_agent = 'DataLens' AND 'QueryFinish' = type ORDER BY event_time desc LIMIT 9" > queries.tsvclickhouse-benchmark -i 45 -c 1 < queries.tsvwith queries as (
SELECT *
FROM system.query_log
WHERE NOT query LIKE '%query_log%'
AND 'QueryFinish' = type
and client_name = 'ClickHouse benchmark'
ORDER BY event_time desc
limit 45
)
select
query,
avg(read_rows) AS read_rows,
avg(read_bytes) AS read_bytes,
count(*) as cnt,
avg(query_duration_ms) as avg,
min(query_duration_ms) as min,
max(query_duration_ms) as max,
any(partitions) as partitions,
any(projections) as projections
from
queries
group by
query
order by
query;с countd(str([person_id])) на countd([person_id_int]).
2.1 Обновляем дашборд, в докере выполняем комануд получения запросов и запускаем clickhouse-benchmark
3. Добавляем первичный ключ. Удаляем таблицу. И при создании вместо PRIMARY KEY(tuple() указываем PRIMARY KEY(person_id_int).
DROP TABLE IF EXISTS learn_db.mart_student_lesson;
CREATE TABLE learn_db.mart_student_lesson
(
`student_profile_id` Int32, -- Идентификатор профиля обучающегося
`person_id` String, -- GUID обучающегося
`person_id_int` Int32 CODEC(Delta, ZSTD),
`educational_organization_id` Int16, -- Идентификатор образовательной организации
`parallel_id` Int16,
`class_id` Int16, -- Идентификатор класса
`lesson_date` Date32, -- Дата урока
`lesson_month_digits` String,
`lesson_month_text` String,
`lesson_year` UInt16,
`load_date` Date, -- Дата загрузки данных
`t` Int16 CODEC(Delta, ZSTD),
`teacher_id` Int32 CODEC(Delta, ZSTD), -- Идентификатор учителя
`subject_id` Int16 CODEC(Delta, ZSTD), -- Идентификатор предмета
`subject_name` String,
`mark` Nullable(UInt8), -- Оценка
PRIMARY KEY(person_id_int)
) ENGINE = MergeTree()
AS SELECT
floor(randUniform(2, 1300000)) as student_profile_id,
cast(student_profile_id as String) as person_id,
cast(person_id as Int32) as person_id_int,
student_profile_id / 365000 as educational_organization_id,
student_profile_id / 73000 as parallel_id,
student_profile_id / 2000 as class_id,
cast(now() - randUniform(2, 60*60*24*365) as date) as lesson_date, -- Дата урока
formatDateTime(lesson_date, '%Y-%m') as lesson_month_digits,
formatDateTime(lesson_date, '%Y %M') AS lesson_month_text,
toYear(lesson_date) as lesson_year,
lesson_date + rand() % 3, -- Дата загрузки данных
floor(randUniform(2, 137)) as t,
educational_organization_id * 136 + t as teacher_id,
floor(t/9) as subject_id,
CASE subject_id
WHEN 1 THEN 'Математика'
WHEN 2 THEN 'Русский язык'
WHEN 3 THEN 'Литература'
WHEN 4 THEN 'Физика'
WHEN 5 THEN 'Химия'
WHEN 6 THEN 'География'
WHEN 7 THEN 'Биология'
WHEN 8 THEN 'Физическая культура'
ELSE 'Информатика'
END as subject_name,
CASE
WHEN randUniform(0, 2) > 1
THEN NULL
ELSE
CASE
WHEN ROUND(randUniform(0, 5)) + subject_id < 5 THEN ROUND(randUniform(4, 5))
WHEN ROUND(randUniform(0, 5)) + subject_id < 9 THEN ROUND(randUniform(3, 5))
ELSE ROUND(randUniform(2, 5))
END
END AS mark
FROM numbers(200000000);3.1 Обновляем дашборд, в докере выполняем команд получения запросов и запускаем clickhouse-benchmark
SELECT t1.lesson_month_text AS res_0, count(t1.mark) AS res_1, t1.mark AS res_2
FROM learn_db.mart_student_lesson AS t1
GROUP BY res_0, res_2
LIMIT 1000001
FORMAT JSONCompactи
SELECT t1.subject_name AS res_0, avg(t1.mark) AS res_1
FROM learn_db.mart_student_lesson AS t1
GROUP BY res_0
LIMIT 1000001
FORMAT JSONCompactВ ddl таблицы поле mark Nullable(UInt8) - может принимать пустые значения. В рекомендациях работы с CH указано что Nullable поля работают медленнее.
4.1 Изменим конструкцию case (значение null заменим на -1), изменим тип поля в ddl таблицы на int8, пересоздадим таблицу. А в дашборде откроем датасет и создадим вычисляемое поле mark_real.
DROP TABLE learn_db.mart_student_lesson;
CREATE TABLE learn_db.mart_student_lesson
(
`student_profile_id` Int32, -- Идентификатор профиля обучающегося
`person_id` String, -- GUID обучающегося
`person_id_int` Int32 CODEC(Delta, ZSTD),
`educational_organization_id` Int16, -- Идентификатор образовательной организации
`parallel_id` Int16,
`class_id` Int16, -- Идентификатор класса
`lesson_date` Date32, -- Дата урока
`lesson_month_digits` String,
`lesson_month_text` String,
`lesson_year` UInt16,
`load_date` Date, -- Дата загрузки данных
`t` Int16 CODEC(Delta, ZSTD),
`teacher_id` Int32 CODEC(Delta, ZSTD), -- Идентификатор учителя
`subject_id` Int16 CODEC(Delta, ZSTD), -- Идентификатор предмета
`subject_name` String,
`mark` Int8, -- Оценка
PRIMARY KEY(person_id_int)
) ENGINE = MergeTree()
AS SELECT
floor(randUniform(2, 1300000)) as student_profile_id,
cast(student_profile_id as String) as person_id,
cast(person_id as Int32) as person_id_int,
student_profile_id / 365000 as educational_organization_id,
student_profile_id / 73000 as parallel_id,
student_profile_id / 2000 as class_id,
cast(now() - randUniform(2, 60*60*24*365) as date) as lesson_date, -- Дата урока
formatDateTime(lesson_date, '%Y-%m') as lesson_month_digits,
formatDateTime(lesson_date, '%Y %M') AS lesson_month_text,
toYear(lesson_date) as lesson_year,
lesson_date + rand() % 3, -- Дата загрузки данных
floor(randUniform(2, 137)) as t,
educational_organization_id * 136 + t as teacher_id,
floor(t/9) as subject_id,
CASE subject_id
WHEN 1 THEN 'Математика'
WHEN 2 THEN 'Русский язык'
WHEN 3 THEN 'Литература'
WHEN 4 THEN 'Физика'
WHEN 5 THEN 'Химия'
WHEN 6 THEN 'География'
WHEN 7 THEN 'Биология'
WHEN 8 THEN 'Физическая культура'
ELSE 'Информатика'
END as subject_name,
CASE
WHEN randUniform(0, 2) > 1
THEN -1
ELSE
CASE
WHEN ROUND(randUniform(0, 5)) + subject_id < 5 THEN ROUND(randUniform(4, 5))
WHEN ROUND(randUniform(0, 5)) + subject_id < 9 THEN ROUND(randUniform(3, 5))
ELSE ROUND(randUniform(2, 5))
END
END AS mark
FROM numbers(200000000);4.2 Обновляем дашборд, в докере выполняем команд получения запросов и запускаем clickhouse-benchmark
SELECT t1.lesson_month_text AS res_0, count(t1.mark) AS res_1, if(t1.mark = -1, NULL, t1.mark) AS res_2
FROM learn_db.mart_student_lesson AS t1
GROUP BY res_0, res_2
LIMIT 1000001
FORMAT JSONCompactи
SELECT t1.subject_name AS res_0, avg(if(t1.mark = -1, NULL, t1.mark)) AS res_1
FROM learn_db.mart_student_lesson AS t1
GROUP BY res_0
LIMIT 1000001
FORMAT JSONCompactDROP TABLE learn_db.mart_student_lesson;
CREATE TABLE learn_db.mart_student_lesson
(
`student_profile_id` Int32, -- Идентификатор профиля обучающегося
`person_id` String, -- GUID обучающегося
`person_id_int` Int32 CODEC(Delta, ZSTD),
`educational_organization_id` Int16, -- Идентификатор образовательной организации
`parallel_id` Int16,
`class_id` Int16, -- Идентификатор класса
`lesson_date` Date32, -- Дата урока
`lesson_month_digits` String,
`lesson_month_text` String,
`lesson_year` UInt16,
`load_date` Date, -- Дата загрузки данных
`t` Int16 CODEC(Delta, ZSTD),
`teacher_id` Int32 CODEC(Delta, ZSTD), -- Идентификатор учителя
`subject_id` Int16 CODEC(Delta, ZSTD), -- Идентификатор предмета
`subject_name` String,
`mark` Int8, -- Оценка
PRIMARY KEY(person_id_int, mark)
) ENGINE = MergeTree()
AS SELECT
floor(randUniform(2, 1300000)) as student_profile_id,
cast(student_profile_id as String) as person_id,
cast(person_id as Int32) as person_id_int,
student_profile_id / 365000 as educational_organization_id,
student_profile_id / 73000 as parallel_id,
student_profile_id / 2000 as class_id,
cast(now() - randUniform(2, 60*60*24*365) as date) as lesson_date, -- Дата урока
formatDateTime(lesson_date, '%Y-%m') as lesson_month_digits,
formatDateTime(lesson_date, '%Y %M') AS lesson_month_text,
toYear(lesson_date) as lesson_year,
lesson_date + rand() % 3, -- Дата загрузки данных
floor(randUniform(2, 137)) as t,
educational_organization_id * 136 + t as teacher_id,
floor(t/9) as subject_id,
CASE subject_id
WHEN 1 THEN 'Математика'
WHEN 2 THEN 'Русский язык'
WHEN 3 THEN 'Литература'
WHEN 4 THEN 'Физика'
WHEN 5 THEN 'Химия'
WHEN 6 THEN 'География'
WHEN 7 THEN 'Биология'
WHEN 8 THEN 'Физическая культура'
ELSE 'Информатика'
END as subject_name,
CASE
WHEN randUniform(0, 2) > 1
THEN -1
ELSE
CASE
WHEN ROUND(randUniform(0, 5)) + subject_id < 5 THEN ROUND(randUniform(4, 5))
WHEN ROUND(randUniform(0, 5)) + subject_id < 9 THEN ROUND(randUniform(3, 5))
ELSE ROUND(randUniform(2, 5))
END
END AS mark
FROM numbers(200000000);5.2 Обновляем дашборд, в докере выполняем команд получения запросов и запускаем clickhouse-benchmark
Оценки за весь период пользователям вряд ли будут нужны. Т.е. "дата урока" будет самым часто применяемым фильтром. Взяли данные за месяц, но запросы не ускорились. Поместим поле lession_date в первичный индекс и поместить его на первое место в первичном индексе.
DROP TABLE if exists learn_db.mart_student_lesson;
CREATE TABLE learn_db.mart_student_lesson
(
`student_profile_id` Int32, -- Идентификатор профиля обучающегося
`person_id` String, -- GUID обучающегося
`person_id_int` Int32 CODEC(Delta, ZSTD),
`educational_organization_id` Int16, -- Идентификатор образовательной организации
`parallel_id` Int16,
`class_id` Int16, -- Идентификатор класса
`lesson_date` Date32, -- Дата урока
`lesson_month_digits` String,
`lesson_month_text` String,
`lesson_year` UInt16,
`load_date` Date, -- Дата загрузки данных
`t` Int16 CODEC(Delta, ZSTD),
`teacher_id` Int32 CODEC(Delta, ZSTD), -- Идентификатор учителя
`subject_id` Int16 CODEC(Delta, ZSTD), -- Идентификатор предмета
`subject_name` String,
`mark` Int8, -- Оценка
PRIMARY KEY(lesson_date, person_id_int, mark)
) ENGINE = MergeTree()
AS SELECT
floor(randUniform(2, 1300000)) as student_profile_id,
cast(student_profile_id as String) as person_id,
cast(person_id as Int32) as person_id_int,
student_profile_id / 365000 as educational_organization_id,
student_profile_id / 73000 as parallel_id,
student_profile_id / 2000 as class_id,
cast(now() - randUniform(2, 60*60*24*365) as date) as lesson_date, -- Дата урока
formatDateTime(lesson_date, '%Y-%m') as lesson_month_digits,
formatDateTime(lesson_date, '%Y %M') AS lesson_month_text,
toYear(lesson_date) as lesson_year,
lesson_date + rand() % 3, -- Дата загрузки данных
floor(randUniform(2, 137)) as t,
educational_organization_id * 136 + t as teacher_id,
floor(t/9) as subject_id,
CASE subject_id
WHEN 1 THEN 'Математика'
WHEN 2 THEN 'Русский язык'
WHEN 3 THEN 'Литература'
WHEN 4 THEN 'Физика'
WHEN 5 THEN 'Химия'
WHEN 6 THEN 'География'
WHEN 7 THEN 'Биология'
WHEN 8 THEN 'Физическая культура'
ELSE 'Информатика'
END as subject_name,
CASE
WHEN randUniform(0, 2) > 1
THEN -1
ELSE
CASE
WHEN ROUND(randUniform(0, 5)) + subject_id < 5 THEN ROUND(randUniform(4, 5))
WHEN ROUND(randUniform(0, 5)) + subject_id < 9 THEN ROUND(randUniform(3, 5))
ELSE ROUND(randUniform(2, 5))
END
END AS mark
FROM numbers(200000000);В этот раз ускорение выполнения запросов получилось существенным.
Проверим скорость открытия дашборда по одной школе.
Добавим партицирование по educational_organization_id.
DROP TABLE if exists learn_db.mart_student_lesson;
CREATE TABLE learn_db.mart_student_lesson
(
`student_profile_id` Int32, -- Идентификатор профиля обучающегося
`person_id` String, -- GUID обучающегося
`person_id_int` Int32 CODEC(Delta, ZSTD),
`educational_organization_id` Int16, -- Идентификатор образовательной организации
`parallel_id` Int16,
`class_id` Int16, -- Идентификатор класса
`lesson_date` Date32, -- Дата урока
`lesson_month_digits` String,
`lesson_month_text` String,
`lesson_year` UInt16,
`load_date` Date, -- Дата загрузки данных
`t` Int16 CODEC(Delta, ZSTD),
`teacher_id` Int32 CODEC(Delta, ZSTD), -- Идентификатор учителя
`subject_id` Int16 CODEC(Delta, ZSTD), -- Идентификатор предмета
`subject_name` String,
`mark` Int8, -- Оценка
PRIMARY KEY(lesson_date, person_id_int, mark)
) ENGINE = MergeTree()
PARTITION BY educational_organization_id
AS SELECT
floor(randUniform(2, 1300000)) as student_profile_id,
cast(student_profile_id as String) as person_id,
cast(person_id as Int32) as person_id_int,
student_profile_id / 365000 as educational_organization_id,
student_profile_id / 73000 as parallel_id,
student_profile_id / 2000 as class_id,
cast(now() - randUniform(2, 60*60*24*365) as date) as lesson_date, -- Дата урока
formatDateTime(lesson_date, '%Y-%m') as lesson_month_digits,
formatDateTime(lesson_date, '%Y %M') AS lesson_month_text,
toYear(lesson_date) as lesson_year,
lesson_date + rand() % 3, -- Дата загрузки данных
floor(randUniform(2, 137)) as t,
educational_organization_id * 136 + t as teacher_id,
floor(t/9) as subject_id,
CASE subject_id
WHEN 1 THEN 'Математика'
WHEN 2 THEN 'Русский язык'
WHEN 3 THEN 'Литература'
WHEN 4 THEN 'Физика'
WHEN 5 THEN 'Химия'
WHEN 6 THEN 'География'
WHEN 7 THEN 'Биология'
WHEN 8 THEN 'Физическая культура'
ELSE 'Информатика'
END as subject_name,
CASE
WHEN randUniform(0, 2) > 1
THEN -1
ELSE
CASE
WHEN ROUND(randUniform(0, 5)) + subject_id < 5 THEN ROUND(randUniform(4, 5))
WHEN ROUND(randUniform(0, 5)) + subject_id < 9 THEN ROUND(randUniform(3, 5))
ELSE ROUND(randUniform(2, 5))
END
END AS mark
FROM numbers(200000000);8.1. Добавляем проекцию по оценке для случая, если дашборд строится без применения фильтров по дате и школе
ALTER TABLE learn_db.mart_student_lesson
ADD PROJECTION prj_mark
(
SELECT *
ORDER BY mark, person_id_int
);
ALTER TABLE learn_db.mart_student_lesson MATERIALIZE PROJECTION prj_mark;Он хорошо применяется если у нас есть какое-то поле (в нашем случае lesson_date) и есть другие поля, которые весьма близки и их порядок сортировки похож на то поле, по которому у нас происходит сортировка. Если мы создаем индекс по полю lesson_mpnth_digits, то на каждые 2 гранулы (или на то количество, которое мы укажем) будут создаваться две доп.засечки: в одной будет указано минимальное значение lesson_month_digits (которое попадает в эти гранулы), а в другой будет указано максимальное значение. Поскольку наши строки отсортированы по lesson_date, то все значения двух гранул могут попасть в 1 месяц или 2 месяца и с помощью этих засечек мы сможем отфильтровать по месяцу.
Выбираем в фильтре по месяцу апрель 2025, сохраняем запросы и выполняем без пропуска тесты.
ALTER TABLE learn_db.mart_student_lesson ADD INDEX lesson_month_text_skipping_index lesson_month_text TYPE minmax GRANULARITY 4;
ALTER TABLE learn_db.mart_student_lesson ADD INDEX lesson_month_digits_skipping_index lesson_month_digits TYPE minmax GRANULARITY 4;
ALTER TABLE learn_db.mart_student_lesson ADD INDEX lesson_year_skipping_index lesson_year TYPE minmax GRANULARITY 4;
ALTER TABLE learn_db.mart_student_lesson ADD INDEX load_date_skipping_index load_date TYPE minmax GRANULARITY 4;
ALTER TABLE learn_db.mart_student_lesson MATERIALIZE INDEX lesson_month_text_skipping_index;
ALTER TABLE learn_db.mart_student_lesson MATERIALIZE INDEX lesson_month_digits_skipping_index;
ALTER TABLE learn_db.mart_student_lesson MATERIALIZE INDEX lesson_year_skipping_index;
ALTER TABLE learn_db.mart_student_lesson MATERIALIZE INDEX load_date_skipping_index;