From fb0f37912d8134b530935baf548fb331b414b4e0 Mon Sep 17 00:00:00 2001 From: Dmitry Medvedev Date: Tue, 15 Sep 2026 15:34:46 +0500 Subject: [PATCH] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BF=D1=80=D0=B5=D0=B4=D1=8B=D0=B4=D1=83?= =?UTF-8?q?=D1=89=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8,=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D0=BD=D0=BE=D1=81=D1=82=D1=8C=D1=8E=20=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=BF=D0=BE=20=D0=BA=D0=BB=D0=B8=D0=BA=D1=85=D0=B0?= =?UTF-8?q?=D1=83=D1=81=20=D0=B8=20=D0=BF=D0=B5=D1=82-=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dmedvedev/2.1.theory_bd_indexes.sql | 2 +- dmedvedev/4.5.greenplum_pet_project.sql | 2 +- .../1.CH_family_megretree_types_of_dates.sql | 104 +++++++ .../2.CH_view_distribute_engine.sql | 102 +++++++ dmedvedev/clickhouse/3.CH_type_of_date.sql | 95 ++++++ dmedvedev/clickhouse/4.CH_syntaxis.sql | 230 ++++++++++++++ dmedvedev/clickhouse/5.CH_joins.sql | 167 +++++++++++ .../clickhouse/6.CH_indexes_partitions.sql | 111 +++++++ dmedvedev/clickhouse/7.CH_indexes.sql | 115 +++++++ dmedvedev/clickhouse/8.CH_compretion.sql | 26 ++ dmedvedev/clickhouse/9.CH_pet_project.sql | 283 ++++++++++++++++++ 11 files changed, 1235 insertions(+), 2 deletions(-) create mode 100644 dmedvedev/clickhouse/1.CH_family_megretree_types_of_dates.sql create mode 100644 dmedvedev/clickhouse/2.CH_view_distribute_engine.sql create mode 100644 dmedvedev/clickhouse/3.CH_type_of_date.sql create mode 100644 dmedvedev/clickhouse/4.CH_syntaxis.sql create mode 100644 dmedvedev/clickhouse/5.CH_joins.sql create mode 100644 dmedvedev/clickhouse/6.CH_indexes_partitions.sql create mode 100644 dmedvedev/clickhouse/7.CH_indexes.sql create mode 100644 dmedvedev/clickhouse/8.CH_compretion.sql create mode 100644 dmedvedev/clickhouse/9.CH_pet_project.sql diff --git a/dmedvedev/2.1.theory_bd_indexes.sql b/dmedvedev/2.1.theory_bd_indexes.sql index 80bd56b..fa83b9c 100644 --- a/dmedvedev/2.1.theory_bd_indexes.sql +++ b/dmedvedev/2.1.theory_bd_indexes.sql @@ -192,7 +192,7 @@ 1. SELECT * FROM users WHERE id = 100; нужен по полю id 2. SELECT * FROM users WHERE email = 'user@example.com'; нужен по полю email 3. SELECT * FROM products WHERE price BETWEEN 100 AND 500; нужен по price - 4. SELECT * FROM products WHERE name LIKE '%phone%'; нужен по name + 4. SELECT * FROM products WHERE name LIKE '%phone%'; не нужен по name 5. SELECT * FROM orders WHERE status = 'DELIVERED'; нужен по status 6. SELECT * FROM order_items WHERE order_id = 123; нужен по order_id diff --git a/dmedvedev/4.5.greenplum_pet_project.sql b/dmedvedev/4.5.greenplum_pet_project.sql index f746c5e..ee08a26 100644 --- a/dmedvedev/4.5.greenplum_pet_project.sql +++ b/dmedvedev/4.5.greenplum_pet_project.sql @@ -204,7 +204,7 @@ SELECT ) AS course_title, COUNT(DISTINCT dl.lesson_id) AS lessons_in_course, COUNT(DISTINCT flv.lesson_id) AS lessons_viewed, - ROUND(COUNT(DISTINCT flv.lesson_id) * 1.0 / NULLIF(COUNT(dl.lesson_id), 0), 2) AS completion_rate + ROUND(COUNT(DISTINCT flv.lesson_id) * 1.0 / NULLIF(COUNT(DISTINCT dl.lesson_id), 0), 2) AS completion_rate FROM core.dim_user du LEFT JOIN core.enrollments e ON du.user_id = e.user_id diff --git a/dmedvedev/clickhouse/1.CH_family_megretree_types_of_dates.sql b/dmedvedev/clickhouse/1.CH_family_megretree_types_of_dates.sql new file mode 100644 index 0000000..670ce89 --- /dev/null +++ b/dmedvedev/clickhouse/1.CH_family_megretree_types_of_dates.sql @@ -0,0 +1,104 @@ +### Задача 1: **Создай таблицу** page_views: + +- user_id UInt64 +- url String +- view_time DateTime + +**Условия:** + +- Движок: MergeTree +- ORDER BY (user_id, view_time) +- PARTITION BY toYYYYMM(view_time) + +CREATE TABLE page_views ( + user_id UInt64, + url String, + view_time DateTime +) +ENGINE = MergeTree +PARTITION BY toYYYYMM(view_time) +ORDER BY (user_id, view_time); + +### Задача 2: **Создай таблицу** users: + +- id UInt64 +- name String +- updated_at DateTime + +**Условия:** + +- Движок: ReplacingMergeTree(updated_at) +- ORDER BY id + +CREATE TABLE users ( + id UInt64, + name String, + updated_at DateTime +) +ENGINE = ReplacingMergeTree(updated_at) +ORDER BY id; + +### Задача 3: **Создай таблицу** region_sales: + +- region String +- date Date +- sales UInt64 + +**Условия:** + +- Движок: SummingMergeTree +- PARTITION BY toYYYYMM(date) +- ORDER BY (region, date) +- Вставь данные с повторами (region, date) + +CREATE TABLE region_sales ( + region String, + date Date, + sales UInt64 +) +ENGINE = SummingMergeTree +PARTITION BY toYYYYMM(date) +ORDER BY (region, date); + +INSERT INTO region_sales (region, date, sales) +VALUES + ('RU', '2026-08-11', 150), + ('FR', '2026-08-10', 100), + ('RU', '2026-08-10', 100), + ('FR', '2026-08-09', 75), + ('RU', '2025-07-05', 175); + +### Задача 4: **Создай таблицу** user_metrics: + +- region String +- avg_age_state AggregateFunction(avg, UInt8) +- active_users_state AggregateFunction(uniq, UInt64) + +**Условия:** + +- Движок: AggregatingMergeTree +- ORDER BY region +- Вставь данные +- Читай их через avgMerge() и uniqMerge() + +CREATE TABLE user_metrics ( + region String, + avg_age_state AggregateFunction(avg, UInt8), + active_users_state AggregateFunction(uniq, UInt64) +) +ENGINE = AggregatingMergeTree +ORDER BY region; + +INSERT INTO user_metrics (region, avg_age_state, active_users_state) +SELECT 'RU', avgState(toUInt8(22)), uniqState(toUInt64(73)) UNION ALL +SELECT 'FR', avgState(toUInt8(33)), uniqState(toUInt64(54)) UNION ALL +SELECT 'RU', avgState(toUInt8(44)), uniqState(toUInt64(10)) UNION ALL +SELECT 'FR', avgState(toUInt8(55)), uniqState(toUInt64(15)) UNION ALL +SELECT 'RU', avgState(toUInt8(22)), uniqState(toUInt64(17)); + +SELECT + region, + avgMerge(avg_age_state), + uniqMerge(active_users_state) +FROM user_metrics +GROUP BY region; \ No newline at end of file diff --git a/dmedvedev/clickhouse/2.CH_view_distribute_engine.sql b/dmedvedev/clickhouse/2.CH_view_distribute_engine.sql new file mode 100644 index 0000000..85b88e7 --- /dev/null +++ b/dmedvedev/clickhouse/2.CH_view_distribute_engine.sql @@ -0,0 +1,102 @@ +# Задачи на отработку: + +### **Задача 1.** + +На каждой ноде(узел) есть таблица logs, в которой пишутся события. + +CREATE TABLE logs ( + dt DateTime, + user_id UInt32, + event_type String +) ENGINE = MergeTree +ORDER BY dt; + +1. Создай распределённую таблицу dist_logs поверх logs с помощью ENGINE = Distributed. + +CREATE TABLE dist_logs ON CLUSTER clickhouse_cluster +ENGINE = Distributed( + 'clickhouse_cluster', + 'dmedvedev', + 'logs', + rand() +); + +2. Объясни, зачем нужен параметр rand() в Distributed(...). + + ЭТО КЛЮЧ ШАРДИРОВАНИЯ, ОН ПОЗВОЛЯЕТ РАСПРЕДЕЛИТЬ ДАННЫЕ РАНДОМНО И РАВНОМЕРНО ПО РАЗЛИЧНЫМ СЕРВЕРАМ. + + +### **Задача 2.** + +Есть таблица events(user_id, event_type, ts) + +CREATE TABLE events +( + user_id UInt32, + event_type String, + ts DateTime +) +ENGINE = MergeTree +ORDER BY (ts, user_id); + +1. Создай VIEW top_active_users, который показывает топ-100 пользователей по числу событий. + +CREATE VIEW top_active_users AS +SELECT + user_id, + COUNT(*) AS total_logs_user +FROM events +GROUP BY user_id +ORDER BY total_logs_user DESC +LIMIT 100; + +2. Объясни, почему VIEW не подходит, если нам нужно часто показывать это в дашборде с миллионами строк. + + ПОТОМУ ЧТО ПРЕДСТАВЛЕНИЮ VIEW НЕ ОБЛАДАЕТ СВОЙСТВОМ ХРАНЕНИЯ ИНФОРМАЦИИ, ПОСТОЯННО БУДУТ ПРОИСХОДИТЬ ТАКИЕ ПОВТОРНЫЕ ЗАПРОСЫЫ, + КОТОРЫЕ БУДУТ ЗАСОРЯТЬ ПАМЯТЬ, ДЛЯ ТАКИХ ЗАДАЧ МОЖЕТ БЫТЬ ИСПОЛЬЗОВАНО МАТЕРИАЛИЗОВАННОЕ ПРЕДСТАВЛЕНИЕ. + + +### **Задача 3.** + +Таблица raw_events содержит "сырые" клики по сайту. + +CREATE TABLE raw_events +( + event_time DateTime, + user_id UInt32, + event_type String +) +ENGINE = MergeTree +ORDER BY (event_time, user_id); + +1. Создай Materialized View mv_clicks_per_day, который считает число кликов по дням. + +CREATE MATERIALIZED VIEW mv_clicks_per_day +ENGINE = SummingMergeTree +ORDER BY = user_id +AS +SELECT + toDate(event_time) AS DAY, + count(*) AS count_of_enent +FROM raw_events; + +2. В движке используй SummingMergeTree, а исходник — raw_events. +3. Проверь, что при INSERT INTO raw_events данные появляются и в mv_clicks_per_day. + +INSERT INTO raw_events (event_time, user_id, event_type) +VALUES + ('2026-01-01', 1, 'abc'), + ('2026-02-01', 2, 'hdf'), + ('2026-02-03', 3, 'hhj'), + ('2026-03-04', 4, 'sdf'), + ('2026-01-01', 5, 'gf'); + +CREATE MATERIALIZED VIEW mv_clicks_per_day +ENGINE = SummingMergeTree +ORDER BY (DAY) +AS +SELECT + toDate(event_time) AS DAY, + toUInt64(1) AS count_of_event +FROM raw_events +GROUP BY DAY; diff --git a/dmedvedev/clickhouse/3.CH_type_of_date.sql b/dmedvedev/clickhouse/3.CH_type_of_date.sql new file mode 100644 index 0000000..01025a4 --- /dev/null +++ b/dmedvedev/clickhouse/3.CH_type_of_date.sql @@ -0,0 +1,95 @@ +# Задачи на отработку: + +### Задача 1. + +Есть "грязная" таблица с данными из внешней системы: + +CREATE TABLE raw_data ( + user_id String, + balance String, + created_at String +) ENGINE = MergeTree ORDER BY tuple(); + +**Подзадачи:** + +1. Преобразуй `balance` в `Decimal(10, 2)` — безопасно. +2. Преобразуй `created_at` в `DateTime64(3)`, даже если возможны ошибки. +3. Верни только строки, в которых оба преобразования прошли успешно. + SELECT + user_id, + toDecimal100rNull(balance, 2), + toDateTime640rNull(created_at, 3) + FROM raw_data + WHERE toDecimal640rNull(balance, 2) AND toDateTime640rNull(created_at, 3) IS NOT NULL; + +4. В чём разница между: + - CAST(balance AS Decimal(10, 2)) -- не поддерживает NULL вообще, при исполнении такой строки, если на входе не число, то запрос упадет в ошибку. + - toDecimal64OrNull(balance, 2) -- если на входе не число, то на выходе будет NULL + - toDecimal64OrZero(balance, 2) -- если на входе не число, то на выходе будет 0 + +### **Задача 2:** + +Есть таблица: + +CREATE TABLE events ( + event_id String, + event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid" + event_time String -- форматы: "12:30:45", "25:99:99" +); + +**Подзадачи:** + +1. Преобразуй **`event_date`** в **`Date`**, игнорируя некорректные значения. +2. Извлеки час из **`event_time`** (с обработкой ошибок). +3. Собери **`DateTime`** из валидных **`event_date`** и **`event_time`**. + + SELECT + event_id, + toDate320rNull(event_date), + toTime640rNull(event_time) + FROM events + WHERE toDate320rNull(event_date) AND toTime640rNull(event_time) IS NOT NULL; + +### **Задача 3:** + +Есть таблица с JSON-данными: + +CREATE TABLE user_activity ( + user_id UInt32, + actions String -- JSON: ["login", "purchase", "logout"] +); + +**Задания:** + +1. Преобразуй JSON-строку в массив (**`Array(String)`**). +2. Найди пользователей, совершивших действие "purchase". +3. Подсчитай количество действий у каждого пользователя + SELECT + user_id, + JSONExtract(actions, 'Array(String)'), + lenght(JSONExtract(actions, 'Array(String)')) + FROM user_activity + WHERE has(JSONExtract(actions, 'Array(String)'), 'purchase'); + +### **Задача 4:** + +Есть таблица: + +CREATE TABLE sensor_data ( + sensor_id UInt32, + value Float64, + status Enum('ok' = 1, 'error' = 2) +); + +**Задания:** + +1. Преобразуй **`status`** в строку (**`String`**). +2. Посчитай среднее значение **`value`** для каждого **`status`**. +3. Отфильтруй строки, где **`value`** нельзя преобразовать в **`Decimal(10,2)`**. + + SELECT + toString(status), + AVG(value) + FROM sensor_data + WHERE toDecimal640rNull(toString(value), 2) IS NOT NULL + GROUP BY toString(status); \ No newline at end of file diff --git a/dmedvedev/clickhouse/4.CH_syntaxis.sql b/dmedvedev/clickhouse/4.CH_syntaxis.sql new file mode 100644 index 0000000..0f42980 --- /dev/null +++ b/dmedvedev/clickhouse/4.CH_syntaxis.sql @@ -0,0 +1,230 @@ +# Задачи на отработку: + +### **Задача 1.** + +Есть таблица `users`: + +| user_id | name | age | country | is_active | +| --- | --- | --- | --- | --- | +| 1 | Ivan | 30 | RU | 1 | +| 2 | Anna | 25 | RU | 1 | +| 3 | Petr | 45 | RU | 0 | +| 4 | Olga | 35 | RU | 1 | +| 5 | John | 55 | US | 1 | +| 6 | Alice | 42 | US | 0 | +| 7 | Tom | 38 | US | 1 | +| 8 | Lucas | 28 | BR | 1 | +| 9 | Maria | 60 | BR | 1 | +| 10 | João | 27 | BR | 0 | +| 11 | Kenji | 33 | JP | 1 | +| 12 | Yuki | 29 | JP | 0 | +| 13 | Chen | 40 | CN | 1 | +| 14 | Li | 48 | CN | 1 | +| 15 | Fatima | 31 | AE | 1 | +| 16 | Ahmed | 36 | AE | 1 | +| 17 | George | 24 | US | 1 | +| 18 | Nina | 34 | RU | 1 | +| 19 | Max | 50 | DE | 1 | +| 20 | Sophie | 22 | DE | 0 | + +CREATE TABLE users ( + user_id UInt64, + name String, + age UInt32, + country String, + is_active UInt32 +) +ENGINE = MergeTree() +ORDER BY (age, country); + +1. Получи 10 самых старших **активных** пользователей. + SELECT + user_id, + name, + age + FROM users + WHERE is_active = 1 + ORDER BY age DESC + LIMIT 10; + +2. Выведи пользователей из `country = 'RU'`, отсортированных по имени. + SELECT + user_id, + name + FROM users + WHERE country = 'RU' + ORDER BY name; + +3. Что произойдёт, если написать `LIMIT 10` **без `ORDER BY`**? + В таком случае CH покажет 10 произвольных неотсортированных строк + +4. Посчитай количество пользователей по странам. + SELECT + country, + COUNT(*) + FROM users + GROUP BY country; + +5. Для каждой страны выведи минимальный и максимальный возраст. + SELECT + country, + MIN(age), + MAX(age) + FROM users + GROUP BY country; + +6. Отсортируй страны по убыванию количества пользователей. + SELECT + country, + COUNT(*) + FROM users + GROUP BY country + ORDER BY COUNT(*) DESC; + +📌 Дополнительно: + +- Используй `count()`, `min()`, `max()`, `sum()` +- Попробуй `uniqExact(country)` — что он делает? + uniqExact выводит количество уникальных значений, это тоже самое что и DISTINCT + +### **Задача 2.** + +Есть таблица `events`: + +| user_id | event_time | event_type | +| --- | --- | --- | +| 1 | 2025-07-17 09:00:00 | click | +| 1 | 2025-07-17 09:05:00 | view | +| 1 | 2025-07-17 09:10:00 | purchase | +| 2 | 2025-07-17 10:00:00 | click | +| 2 | 2025-07-17 10:05:00 | click | +| 2 | 2025-07-17 11:00:00 | view | +| 3 | 2025-07-16 12:00:00 | view | +| 3 | 2025-07-16 12:30:00 | click | +| 3 | 2025-07-17 08:00:00 | purchase | +| 4 | 2025-07-17 13:00:00 | click | +| 4 | 2025-07-17 14:00:00 | view | +| 4 | 2025-07-17 15:00:00 | view | +| 5 | 2025-07-16 20:00:00 | view | +| 5 | 2025-07-17 01:00:00 | click | +| 5 | 2025-07-17 06:00:00 | purchase | +| 6 | 2025-07-17 07:30:00 | view | +| 6 | 2025-07-17 08:45:00 | click | +| 7 | 2025-07-16 23:59:59 | click | +| 7 | 2025-07-17 00:01:00 | purchase | +| 7 | 2025-07-17 10:10:00 | view | + +CREATE TABLE events ( + user_id UInt64, + event_type String, + event_time DateTime64(3) +) +ENGINE = MergeTree() +ORDER BY (event_time, user_id); + +INSERT INTO dmedvedev.events (user_id, event_time, event_type) VALUES +(1, '2025-07-17 09:00:00', 'click'), +(1, '2025-07-17 09:05:00', 'view'), +(1, '2025-07-17 09:10:00', 'purchase'), +(2, '2025-07-17 10:00:00', 'click'), +(2, '2025-07-17 10:05:00', 'click'), +(2, '2025-07-17 11:00:00', 'view'), +(3, '2025-07-16 12:00:00', 'view'), +(3, '2025-07-16 12:30:00', 'click'), +(3, '2025-07-17 08:00:00', 'purchase'), +(4, '2025-07-17 13:00:00', 'click'), +(4, '2025-07-17 14:00:00', 'view'), +(4, '2025-07-17 15:00:00', 'view'), +(5, '2025-07-16 20:00:00', 'view'), +(5, '2025-07-17 01:00:00', 'click'), +(5, '2025-07-17 06:00:00', 'purchase'), +(6, '2025-07-17 07:30:00', 'view'), +(6, '2025-07-17 08:45:00', 'click'), +(7, '2025-07-16 23:59:59', 'click'), +(7, '2025-07-17 00:01:00', 'purchase'), +(7, '2025-07-17 10:10:00', 'view'); + + +1. Добавь колонку с кумулятивным количеством событий по `user_id` (используй `count() OVER ...`). + SELECT + user_id, + COUNT() OVER (PARTITION BY user_id ORDER BY user_id) + FROM events; + +2. Сделай аналог ранжирования событий внутри пользователя: `row_number() OVER (PARTITION BY user_id ORDER BY event_time)` + SELECT + user_id, + COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time) + FROM events; + +3. Что будет, если ты явно задашь `ROWS BETWEEN`? Поддерживается ли? + + Посчитается, поддерживается. + SELECT + user_id, + COUNT(*) OVER ( + PARTITION BY user_id ORDER BY event_time + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) + FROM events; + +4. Сделай `WITH recent_events AS (...)` — все события за последние 24 часа. + SELECT + user_id, + event_time + FROM events + WHERE event_time <= NOW() - INTERVAL 24 HOUR; + +5. Используй CTE, чтобы посчитать топ-5 пользователей по числу этих событий. + WITH + + recent_events AS ( + SELECT + user_id, + event_type, + event_time + FROM events + WHERE event_time >= NOW() - INTERVAL 24 HOUR + ) + + SELECT + user_id, + COUNT(*) + FROM recent_events + GROUP BY user_id + ORDER BY COUNT(*) DESC + LIMIT 5; + +6. Добавь второй `WITH` — для маппинга `event_type → category` и используй его в основном запросе. + WITH + + recent_events AS ( + SELECT + user_id, + event_type, + event_time + FROM events + WHERE event_time >= NOW() - INTERVAL 24 HOUR + ), + + top_users AS( + SELECT + user_id, + COUNT(*) + FROM recent_events + GROUP BY user_id + ORDER BY COUNT(*) DESC + LIMIT 5 + ) + + SELECT + user_id, + CASE + WHEN event_type IN ('click', 'view') THEN 'interaction' + WHEN event_type IN ('purchase') THEN 'conversion' + ELSE 'unknown' + END AS category + FROM recent_events re INNER JOIN top_users tu ON re.user_id = tu.user_id; + +📌 Вопрос: поддерживает ли ClickHouse **многоуровневые** или **рекурсивные** CTE? + МНОГОУРОВНЕВОСТЬ ЕСТЬ, А РЕКУРСИВНОСТИ НЕТ. \ No newline at end of file diff --git a/dmedvedev/clickhouse/5.CH_joins.sql b/dmedvedev/clickhouse/5.CH_joins.sql new file mode 100644 index 0000000..6c7f420 --- /dev/null +++ b/dmedvedev/clickhouse/5.CH_joins.sql @@ -0,0 +1,167 @@ +# Задачи на отработку: + +### **Задача 1.** + +Таблица `products` + +| product_id | name | +| --- | --- | +| 101 | iPhone 14 | +| 102 | Galaxy S22 | +| 103 | Pixel 7 | + +Таблица `prices` + +| product_id | price | region | +| --- | --- | --- | +| 101 | 999 | US | +| 101 | 1049 | EU | +| 102 | 850 | US | +| 102 | 899 | CA | +| 103 | 799 | JP | +1. Сделай `ANY LEFT JOIN`, чтобы получить цену **любой** из регионов. + SELECT + pr.product_id, + p.region, + pr.name, + p.price + FROM prices p + ANY LEFT JOIN products pr ON p.product_id = pr.product_id; + ЗДЕСЬ МЫ ПОЛУЧИМ НА ВЫХОДЕ 5 СТРОК. + А ЕСЛИ БЫ ДЖОЙНИЛИ ТАБЛИЦЫ НАОБОРОТ, ТО ПОЛУЧИЛИ БЫ 3 СТРОКИ. + +2. Что будет, если в таблице `prices` несколько цен на один `product_id`? + ВСЕ БУДЕТ ЗАВИСЕТЬ ОТ СОЕДИНЕНИЯ ТАБЛИЦ. ЕСЛИ МЫ ДЖОЙНИМ К ТАБЛИЦЕ С product_id И НА КАЖДОЫЙ ЕСТЬ НЕСКОЛЬКО ЦЕН, ТО В ТАКОМ СЛУЧАЕ КЛИКХАУС ВОЗЬМЕТ ПЕРВОЕ ЗНАЧЕНИЕ С ДИСКА И ПРИСВОИТ ЕГО К product_id, А ОСТАЛЬНЫЕ ЗНАЧЕНИЯ ВЫБРОСИТ. + +3. Как повлияет `ALL LEFT JOIN`? Почему в ClickHouse его избегают? + ИЗ-ЗА ПРИМЕНЕНИЯ ОЛЛ ЛЕФТ ДЖОЙН КЛИКХАУС НЕ СМОЖЕТ ВЫБРОСИТЬ НЕНУЖНЫЕ СТРОКИ И УЧТЕТ ВСЕ. В НАШЕМ ПРИМЕРЕ СТРОК СТАНЕТ 5. + ДАННЫЕ МОГУТ ПЕРЕГРУЖАТЬ ПАМЯТЬ И ТОРМОЗИТЬ СИСТЕМУ. МЕТРИКИ МОГУТ СЧИТАТЬСЯ ИСКАЖЕННЫМИ. + +### **Задача 2.** + +Таблица `ticks` + +| time | symbol | price | +| --- | --- | --- | +| 2025-07-17 10:00:00 | AAPL | 190.5 | +| 2025-07-17 10:01:00 | AAPL | 191.2 | +| 2025-07-17 10:02:30 | AAPL | 192.0 | +| 2025-07-17 10:05:00 | GOOG | 2850.0 | + +Таблица `orders` + +| time | symbol | size | +| --- | --- | --- | +| 2025-07-17 10:00:30 | AAPL | 50 | +| 2025-07-17 10:02:00 | AAPL | 75 | +| 2025-07-17 10:03:00 | AAPL | 20 | +| 2025-07-17 10:04:00 | GOOG | 15 | +1. Сделай `ASOF JOIN`, чтобы каждой сделке найти **предыдущую цену**. + SELECT + o.symbol, + o.size, + t.price, + o.time AS order_time, + t.time AS price_time + FROM orders o + ASOF LEFT JOIN ticks t ON o.symbol = t.symbol AND o.time >= t.time; + +2. Почему нельзя использовать обычный `JOIN` по `time = time`? + КАК МИНИМУМ ПОТОМУ ЧТО ВРЕМЯ ПО СДЕЛКЕ И ВРЕМЯ ПО ТИКЕТУ НЕ СОВПАДАЕТ. + +3. Сделай выборку только по `symbol = 'AAPL'`. + SELECT + o.symbol, + o.size, + t.price, + o.time AS order_time, + t.time AS price_time + FROM orders o + ASOF LEFT JOIN ticks t ON o.symbol = t.symbol AND o.time >= t.time + WHERE o.symbol = 'AAPL'; + +ИЛИ + + SELECT + o.symbol, + o.size, + t.price, + o.time AS order_time, + t.time AS price_time + FROM + ( + SELECT * FROM orders WHERE o.symbol = 'AAPL' + ) AS o + ASOF LEFT JOIN + ( + SELECT * FROM ticks WHERE o.symbol = 'AAPL' + ) AS t + ON o.symbol = t.symbol AND o.time >= t.time; + +### **Задача 3.** + +Таблица `users` + +| user_id | name | +| --- | --- | +| 1 | Alice | +| 2 | Bob | +| 3 | Charlie | +| 4 | Diana | + +Таблица `payments` + +| payment_id | user_id | amount | +| --- | --- | --- | +| 1001 | 1 | 500 | +| 1002 | 1 | 1500 | +| 1003 | 3 | 1200 | +| 1004 | 4 | 900 | +1. Верни всех пользователей, у которых **есть платежи** (через `WHERE user_id IN (...)`). + SELECT + u.user_id + FROM users u + WHERE user_id IN (SELECT user_id FROM payments); + +2. Добавь условие: сумма платежа больше 1000. + SELECT + u.user_id + FROM users u + WHERE user_id IN (SELECT user_id FROM payments WHERE amount > 1000); + +3. Почему это называют `SEMI JOIN`? Как это оптимизируется в ClickHouse? + Потому что это неполноценное соединение , а только проверяющее факт наличия того или иного условия. + Вообще по идее нужно было бы написать LEFT SEMI JOIN , в кликхаус видя конструкцию IN (SELECT...) уже сам понимает что это SEMI JOIN. + +### **Задача 4.** + +Таблица `big_logs` (упрощённый фрагмент) + +| user_id | event_time | event_type | +| --- | --- | --- | +| 1 | 2025-07-17 08:00:00 | login | +| 2 | 2025-07-17 08:05:00 | view | +| 3 | 2025-07-17 08:10:00 | purchase | +| 1 | 2025-07-17 08:20:00 | logout | +| 2 | 2025-07-17 08:30:00 | click | + +Таблица `users` + +| user_id | name | +| --- | --- | +| 1 | Alice | +| 2 | Bob | +| 3 | Charlie | +1. Сделай `JOIN`, чтобы добавить `name` к каждому логу. + SELECT + bl.user_id, + bl.event_time, + bl.event_type, + u.name + FROM big_logs bl ANY LEFT JOIN users u ON bl.user_id = u.user_id; + +2. Почему `users` нужно ставить **в правую часть JOIN**? + ПОТОМУ ЧТО ЭТА ТАБЛИЦА ЯВЛЯЕТСЯ МАЛЕНЬКОЙ И МОЖНО СКАЗАТЬ СПРАВОЧНОЙ, А ЛОГИ БУДУТ РАСТИ И РАСТИ. И ЕСЛИ ЕЕ ЗАГРУЗИТЬ В ОПЕРАТИВНУЮ ПАМЯТЬ, ТО МЫ МОЖЕМ РАНО ИЛИ ПОЗДНО ПОЛУЧИТЬ ОШИБКУ. + +3. Как выбрать `ANY` или `ALL` — если `user_id` уникален? + ЕСЛИ МЫ УВЕРЕНЫ НА 100% ЧТО ОН УНИКАЛЕН ТО НУЖНО ВАЫБИРАТЬ ANY , В ТАКОМ СЛУЧАЕ СОХРАНЯЮТСЯ РЕСУРСЫ ПАМЯТИ. ALL ЖЕ ВЫБИРАЕТСЯ КОГДА НЕОБХОДИМО ДЕКАРТОВО ПРОИЗВЕДЕНИЕ. \ No newline at end of file diff --git a/dmedvedev/clickhouse/6.CH_indexes_partitions.sql b/dmedvedev/clickhouse/6.CH_indexes_partitions.sql new file mode 100644 index 0000000..d3aa19b --- /dev/null +++ b/dmedvedev/clickhouse/6.CH_indexes_partitions.sql @@ -0,0 +1,111 @@ +# Задачи на отработку: + +### **Задача 1.** + +Таблица `sales` + +| id | region | sale_date | amount | +| --- | --- | --- | --- | +| 1 | RU | 2024-05-01 | 100.0 | +| 2 | RU | 2024-05-15 | 120.0 | +| 3 | US | 2024-06-01 | 90.0 | +| 4 | US | 2024-07-03 | 150.0 | +| 5 | DE | 2024-07-04 | 160.0 | +1. Создай таблицу с `PARTITION BY toYYYYMM(sale_date)`. + CREATE TABLE IF NOT EXISTS sales ( + id UInt8, + region String, + sale_sate DateTime64, + amount Float64 + ) + ENGINE = MergeTree() + PARTITION BY toYYYYMM(sale_date) + ORDER BY (sale_date, region); + + +2. Выполни `ALTER TABLE ... DROP PARTITION '202405'`. + УДАЛИЛАСЬ ПАРТИЦИЯ + +3. Что произойдет, если не указывать `PARTITION BY`? + ЕСЛИ МЫ ГОВОРИМ ПРО ПРОЦЕСС СОЗДАНИЯ, ТО В ТАКОМ СЛУЧАЕ ВСЕ ДАННЫЕ БУДУТ НАХОДИТЬСЯ ПО УМОЛЧАНИЮ В ОДНОЙ ПАРТИЦИИ. + +### **Задача 2.** + +Есть таблица `events`, и ты хочешь логически хранить данные **по типу события**. + +| id | event_type | event_date | +| --- | --- | --- | +| 1 | login | 2024-06-01 | +| 2 | click | 2024-06-01 | +| 3 | click | 2024-06-02 | +| 4 | purchase | 2024-06-03 | +| 5 | login | 2024-06-04 | +1. Создай таблицу с `PARTITION BY event_type`. + CREATE TABLE IF NOT EXISTS sales ( + id UInt8, + event_type String, + event_date Date + ) + ENGINE = MergeTree() + PARTITION BY event_type + ORDER BY (event_type, event_date); + +2. Удали все события `click` (подсказка: это `ALTER TABLE DROP PARTITION 'click'`). +3. Чем такой подход может быть опасен (подумай про кардинальность значений)? + СЕЙЧАС СОБЫТИЙ ПО ФАКТУ 3, ЭТО МАЛО И УДАЛЕНИЕ БУДЕТ ПРОИСХОДИТЬ БЫСТРО, А ЕСЛИ ПАРТИЦИЙ БУДЕТ 1000, ТО В ТАКОМ СЛУЧАЕ СИСТЕМА БУДЕТ ТОРМОЗИТЬ. + +### **Задача 3.** + +Ты хочешь разбить таблицу `logs` на 100 партиций по `user_id`, чтобы равномерно распределить объём данных. + +| id | user_id | log_time | +| --- | --- | --- | +| 1 | 123 | 2024-07-17 10:00:00 | +| 2 | 456 | 2024-07-17 10:01:00 | +| 3 | 789 | 2024-07-17 10:02:00 | +1. Создай таблицу с `PARTITION BY cityHash64(user_id) % 100`. + CREATE TABLE IF NOT EXISTS logs ( + id UInt32, + user_id UInt32, + log_time DateTime64 + ) + ENGINE = MergeTree() + PARTITION BY cityHash64(user_id) % 100 + ORDER BY (user_id, log_time); + +2. Что ты выиграешь с точки зрения баланса? + Я СБАЛАНСИРОВАННО РАЗДЕЛЮ ВСЕ ЛОГИ ПО ПАРТИЦИЯМ ПО УНИКАЛЬНОМУ ЮЗЕР АЙДИ + +3. Почему такой тип `PARTITION` не ускорит `SELECT * WHERE user_id = 456`? + ПОТОМУ ЧТО В ТАКОМ СЛУЧАЕ ПРИДЕТСЯ ВСЕ РАВНО СКАНИРОВАТЬ ВСЕ ПАРТИЦИИ, ТО ЕСТЬ ВСЮ БАЗУ ДАННЫХ, ВВИДУ ТОГО, ЧТО АЙДИШНИК 456 КЛИКХАУЦС НЕ СМОЖЕТ ПОДСТАВИТЬ В + cityHash64(user_id) ДЛЯ РАСЧЕТА В ХЭЩ ФУНКЦИИ. ПОЭТОМУ ОПТИМИЗАТОРУ НЕОБХОДИМО БУДЕТ ЗАГЛЯНУТЬ В 100 ФАЙЛОВ НА ДИСКЕ . + +### **Задача 4.** + +В таблице `visits` данные зависят от региона и времени. + +| id | region | visit_date | +| --- | --- | --- | +| 1 | RU | 2024-06-01 | +| 2 | RU | 2024-06-15 | +| 3 | US | 2024-06-03 | +| 4 | US | 2024-07-01 | +| 5 | RU | 2024-07-05 | +1. Создай таблицу с `PARTITION BY (region, toYYYYMM(visit_date))`. + CREATE TABLE IF NOT EXISTS visits ( + id UInt8, + region String, + visit_date Date + ) + ENGINE = MergeTree() + PARTITION BY (region, toYYYYMM(visit_date)) + ORDER BY (visit_date, id); + +2. Как будет выглядеть имя партиции? + "RU-202406" + "US-202406" + "US-202407" + "RU-202407" + +3. Сможешь ли ты эффективно удалить **все июльские данные по `RU`**? + НЕТ КОНЕЧНО, ТАК КАК ЭТИ ДАННЫЕ НАХОДЯТСЯ В ДВУХ ПАРТИЦИЯХ, ТО НЕОБХОДИМО БУДЕТ С ПОМОЩЬЮ УДАЛЯТЬ ЧЕРЕЗ "RU-202407". \ No newline at end of file diff --git a/dmedvedev/clickhouse/7.CH_indexes.sql b/dmedvedev/clickhouse/7.CH_indexes.sql new file mode 100644 index 0000000..809c46b --- /dev/null +++ b/dmedvedev/clickhouse/7.CH_indexes.sql @@ -0,0 +1,115 @@ +# Задачи на отработку: + +### **Задача 1.** + +**Таблица:** `logs(log_id UInt64, ts DateTime)` + +| log_id | ts | +| --- | --- | +| 1 | 2024-06-01 10:00:00 | +| 2 | 2024-06-01 11:00:00 | +| 3 | 2024-06-02 12:00:00 | +| 4 | 2024-06-03 13:00:00 | +| 5 | 2024-06-04 14:00:00 | +1. Создай `ORDER BY ts`, добавь `minmax`индекс: + + ALTER TABLE logs ADD INDEX idx_ts ts TYPE minmax GRANULARITY 2; + + ГОТОВО + +2. Сделай запрос: `SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03'` + ГОТОВО + +3. Посмотри, сколько блоков было пропущено (в `system.query_log`, `rows_read`, `rows`). + НЕТ ПРАВ ДОСТУПА + +4. Что будет, если `ORDER BY log_id`, а не `ts`? + ТОГДА ДАННЫЕ НА ДИСКЕ БУДУТ ОТСОРТИРОВАНЫ ПО log_id, а первичный и вторичный индексы по ts перестанут помогать в поиске данных. + + +### **Задача 2.** + +**Таблица:** `clicks(id UInt64, country String)` + +| id | country | +| --- | --- | +| 1 | RU | +| 2 | US | +| 3 | RU | +| 4 | DE | +| 5 | RU | +1. Создай таблицу с `ORDER BY id`, добавь индекс: + + ALTER TABLE clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2; + + ДОБАВИЛ + +2. Выполни запрос `SELECT * FROM clicks WHERE country = 'RU'`. + ВЫПОЛНИЛ + +3. Посмотри `EXPLAIN` — индекс используется? + + ИСПОЛЬЗУЕТСЯ + + Expression ((Project names + Projection)) + Expression ((WHERE + Change column names to column identifiers)) + ReadFromMergeTree (dmedvedev.clicks) + Indexes: + PrimaryKey + Condition: true + Parts: 1/1 + Granules: 1/1 + Skip + Name: idx_country + Description: set GRANULARITY 2 + Parts: 1/1 + Granules: 1/1 + Ranges: 1 + +4. Как будет вести себя индекс, если `country` имеет 10M уникальных значений? + ОН НЕ БУДЕТ РАБОТАЬ, ТАК КАК МЫ ИНДЕКСЕ set МЫ УСТАНОВИЛИ ПОРОГ УНИКАЛЬНЫХ ЗНАЧЕНИЙ В 1000 ШТУК. + В ТАКИХ СЛУЧАЯХ НУЖНО БЫЛО БЫ ВОСПОЛЬЗОВАТЬСЯ БЛУМ ФИЛЬТРОМ. + +### **Задача 3.** + +**Таблица:** `users(id UInt64, email String)` + +| id | email | +| --- | --- | +| 1 | alice@mail.com | +| 2 | bob@mail.com | +| 3 | charlie@mail.com | +| 4 | dave@mail.com | +| 5 | eve@example.com | +1. Добавь индекс: + + ALTER TABLE users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2; + ДОБАВИЛ + +2. Проверь поиск по точному email: + SELECT * FROM users WHERE email = 'alice@mail.com'; + + РАБОТАЕТ + 1 alice@mail.com + + Expression ((Project names + Projection)) + Expression ((WHERE + Change column names to column identifiers)) + ReadFromMergeTree (dmedvedev.users) + Indexes: + PrimaryKey + Condition: true + Parts: 1/1 + Granules: 1/1 + Skip + Name: idx_email + Description: bloom_filter GRANULARITY 2 + Parts: 1/1 + Granules: 1/1 + Ranges: 1 + +3. Почему этот индекс **не ускоряет** поиск по `LIKE '%mail%'`? + ПОТОМУ ЧТО ФИЛЬТР БЛУМА НЕ МОЖЕТ ИСКАТЬ ДАННЫЕ КОТОРЫЕ СОДЕРЖАТЬСЯ ВНУТРИ КАКИХ-ТО СТРОК, ЕМУ НУЖНО ПОНИМАТЬ КАКОЕ БУДЕТ НАЧАЛО, А ЗДЕСЬ ОНО НЕ УКАЗАНО. + ДЛЯ ТАКОГО ПОИСКА НУЖНО ПРИМЕНЯТЬ МОДЕРНИЗИРОВАННЫЙ ФИЛЬТР БЛУМА ПО ТОКЕНАМ. + +4. Что произойдёт при `LIKE 'alice%'`? + В ТАКОМ СЛУЧАЕ ОБЫКНОВЕННЫЙ ФИЛЬТР БЛУМА СРАБОТАЕТ. \ No newline at end of file diff --git a/dmedvedev/clickhouse/8.CH_compretion.sql b/dmedvedev/clickhouse/8.CH_compretion.sql new file mode 100644 index 0000000..c1ab8fe --- /dev/null +++ b/dmedvedev/clickhouse/8.CH_compretion.sql @@ -0,0 +1,26 @@ +### **Задача 1.** + +У тебя таблица `logs`с длинными строками — сообщения занимают много места. + +| id | level | message | created_at | +| --- | --- | --- | --- | +| 1 | INFO | "User logged in" | 2024-07-01 10:00:00 | +| 2 | WARN | "Request took too long" | 2024-07-01 10:05:00 | +| 3 | ERROR | "Unable to connect to server" | 2024-07-01 10:10:00 | +1. Создай таблицу `logs` с колонкой `message CODEC(ZSTD(3))`. +2. Добавь `created_at CODEC(Delta, ZSTD)` — это улучшит хранение по времени. +3. Посмотри статистику сжатия: + + SELECT + column, + codec_expression, + compression_codec + FROM system.columns + WHERE table = 'logs'; + + ОТВЕТ: + name type compression_codec + id UInt64 - + LEVEL String - + message String CODEC(ZSTD(3)) + created_at DateTime CODEC(Delta(4), ZSTD(1)) \ No newline at end of file diff --git a/dmedvedev/clickhouse/9.CH_pet_project.sql b/dmedvedev/clickhouse/9.CH_pet_project.sql new file mode 100644 index 0000000..abad31c --- /dev/null +++ b/dmedvedev/clickhouse/9.CH_pet_project.sql @@ -0,0 +1,283 @@ +Этап 1. Загрузка тестовых данных и создание таблиц + +CREATE TABLE dmedvedev.core_dim_course +ON CLUSTER clickhouse_cluster ( + course_id UInt32, + title String, + category String +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/core_dim_course', + '{replica}' +) +ORDER BY course_id; + +CREATE TABLE dmedvedev.core_dim_lesson +ON CLUSTER clickhouse_cluster ( + lesson_id UInt32, + title String, + duration_min UInt32, + course_id UInt32 +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/core_dim_lesson', + '{replica}' +) +ORDER BY lesson_id; + +CREATE TABLE dmedvedev.core_dim_user ON CLUSTER clickhouse_cluster( + user_id UInt32, + name String, + age UInt32, + email String, + registration_date DateTime(3) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/core_dim_user', + '{replica}' +) +ORDER BY user_id; + +CREATE TABLE dmedvedev.core_enrollments ON CLUSTER clickhouse_cluster( + user_id UInt32, + course_id UInt32, + enrolled_at DateTime(3) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/core_enrollments', + '{replica}' +) +ORDER BY user_id; + +CREATE TABLE dmedvedev.core_fact_lesson_views ON CLUSTER clickhouse_cluster ( + user_id UInt32, + course_id UInt32, + lesson_id UInt32, + viewed_at DateTime(3) +) +ENGINE = ReplicatedMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/core_fact_lesson_views', + '{replica}' +) +ORDER BY user_id; + +--Этап 2. Задание 2. +--СОЗДАЕМ МАТ ПРЕДСТАВЛЕНИЕ mv_lesson_popularity_summary +--для начала создаем таблицу в кликхаусе, реплицируемую с движком ReplicatedAggregatingMergeTree +CREATE TABLE dmedvedev.dm_lesson_popularity_summary ON CLUSTER clickhouse_cluster +( + lesson_id UInt32, + lesson_title String, + course_id UInt32, + course_title String, + total_views SimpleAggregateFunction(sum, UInt64), + unique_users AggregateFunction(uniq, UInt32), + first_view SimpleAggregateFunction(min, DateTime), + last_view SimpleAggregateFunction(max, DateTime) +) +ENGINE = ReplicatedAggregatingMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/dm_lesson_popularity_summary', + '{replica}') +ORDER BY (lesson_id, lesson_title, course_id, course_title); + +--далее создаем мат представление, считывает новые данные, делает джойны и кладет данные в dm_lesson_popularity_summary +-- +CREATE MATERIALIZED VIEW dmedvedev.mv_lesson_popularity_summary ON CLUSTER clickhouse_cluster +TO dmedvedev.dm_lesson_popularity_summary AS +SELECT + flv.lesson_id AS lesson_id, + dl.title AS lesson_title, + dc.course_id AS course_id, + dc.title AS course_title, + COUNT(*) AS total_views, + uniqState(flv.user_id) AS unique_users, + MIN(flv.viewed_at) AS first_view, + MAX(flv.viewed_at) AS last_view +FROM dmedvedev.core_fact_lesson_views AS flv +LEFT JOIN dmedvedev.core_dim_lesson AS dl ON flv.lesson_id = dl.lesson_id +LEFT JOIN dmedvedev.core_dim_course AS dc ON dl.course_id = dc.course_id +GROUP BY lesson_id, lesson_title, course_id, course_title; + +-- помещаем данные в dm_lesson_popularity_summary +INSERT INTO dmedvedev.dm_lesson_popularity_summary +SELECT + flv.lesson_id AS lesson_id, + dl.title AS lesson_title, + dc.course_id AS course_id, + dc.title AS course_title, + COUNT(*) AS total_views, + uniqState(flv.user_id) AS unique_users, + MIN(flv.viewed_at) AS first_view, + MAX(flv.viewed_at) AS last_view +FROM dmedvedev.core_fact_lesson_views AS flv +LEFT JOIN dmedvedev.core_dim_lesson AS dl ON flv.lesson_id = dl.lesson_id +LEFT JOIN dmedvedev.core_dim_course AS dc ON dl.course_id = dc.course_id +GROUP BY lesson_id, lesson_title, course_id, course_title; + +ЗАПРОС (ТОП 5 самых просматриваемых уроков уникальными пользователями): +SELECT + lesson_id, + lesson_title, + total_views, + uniqMerge(unique_users) AS unique_users +FROM dmedvedev.dm_lesson_popularity_summary dlps +GROUP BY lesson_id, lesson_title, course_id, course_title, total_views, first_view, last_view +ORDER BY unique_users DESC +LIMIT 5; + + +--Задание 3. СОЗДАЕМ МАТ ПРЕДСТАВЛЕНИЕ mv_inactive_users_summary +--для начала создаем таблицу в кликхаусе, реплицируемую с движком ReplicatedAggregatingMergeTree +CREATE TABLE dmedvedev.dm_inactive_users_summary ON CLUSTER clickhouse_cluster +( + user_id UInt32, + name String, + email String, + age UInt32, + registrated_courses_count AggregateFunction(uniqExact, UInt32), + registration_date DateTime +) +ENGINE = ReplicatedAggregatingMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/dm_inactive_users_summary', + '{replica}' +) +ORDER BY (user_id, name, email, age, registration_date); + +--создаем само мат представление, которое перехватывает данные из таблицы фактов и помещает в кликхаус dmedvedev.dm_inactive_users_summary +CREATE MATERIALIZED VIEW dmedvedev.mv_inactive_users_summary ON CLUSTER clickhouse_cluster +TO dmedvedev.dm_inactive_users_summary +AS +SELECT + ce.user_id, + cdu.name, + cdu.email, + cdu.age, + uniqExactState(ce.course_id) AS registrated_courses_count, + cdu.registration_date +FROM core_enrollments ce +LEFT JOIN core_dim_user cdu ON ce.user_id = cdu.user_id +GROUP BY ce.user_id, cdu.name, cdu.email, cdu.age, cdu.registration_date; + +-- помещаем данные в dm_inactive_users_summary +INSERT INTO dmedvedev.dm_inactive_users_summary +SELECT + ce.user_id, + cdu.name, + cdu.email, + cdu.age, + uniqExactState(ce.course_id) AS registrated_courses_count, + cdu.registration_date +FROM core_enrollments ce +LEFT JOIN core_dim_user cdu ON ce.user_id = cdu.user_id +GROUP BY ce.user_id, cdu.name, cdu.email, cdu.age, cdu.registration_date; + +--читаем данные из dm_inactive_users_summary +SELECT + user_id, + name, + email, + age, + uniqExactMerge(registrated_courses_count) AS registrated_courses_count, + registration_date +FROM dmedvedev.dm_inactive_users_summary +GROUP BY user_id, name, email, age, registration_date; + +ЗАПРОС (ученики зарегистрировавшиеся только на 1 курс): +SELECT + user_id, + name, + email, + age, + uniqExactMerge(registrated_courses_count) AS registrated_courses_count, + registration_date +FROM dmedvedev.dm_inactive_users_summary +GROUP BY user_id, name, email, age, registration_date +HAVING registrated_courses_count = 1; + +--Задание 4. создаем витрину course_completion_rate +--для начала создаем таблицу в кликхаус с движком +CREATE TABLE dmedvedev.dm_course_completion_rate ON CLUSTER clickhouse_cluster +( + user_id UInt32, + name String, + course_id UInt32, + course_title String, + lessons_in_course SimpleAggregateFunction(max, UInt32), + lessons_viewed AggregateFunction(uniqExact, UInt32) +) +ENGINE = ReplicatedAggregatingMergeTree( + '/clickhouse/tables/{shard}/dmedvedev/dm_course_completion_rate', + '{replica}' +) +ORDER BY (user_id, course_id, name, course_title); + +--далее создаем мат представление +CREATE MATERIALIZED VIEW dmedvedev.mv_course_completion_rate ON CLUSTER clickhouse_cluster +TO dmedvedev.dm_course_completion_rate +AS +SELECT + cflv.user_id AS user_id, + cdu.name AS name, + cdl.course_id AS course_id, + cdc.title AS course_title, + max(count_lesson_in_course.total_leeson) AS lessons_in_course, + uniqExactState(cflv.lesson_id) AS lessons_viewed +FROM core_fact_lesson_views cflv +LEFT JOIN core_dim_user cdu ON cflv.user_id = cdu.user_id +LEFT JOIN core_dim_lesson cdl ON cflv.lesson_id = cdl.lesson_id +LEFT JOIN core_dim_course cdc ON cdl.course_id = cdc.course_id +LEFT JOIN ( + SELECT course_id, uniqExact(lesson_id) AS total_leeson + FROM dmedvedev.core_dim_lesson cdm + GROUP BY course_id +) AS count_lesson_in_course ON count_lesson_in_course.course_id = cdl.course_id +GROUP BY cflv.user_id, cdl.course_id, cdu.name, cdc.title; + +--вставляем данные +INSERT INTO dmedvedev.dm_course_completion_rate +SELECT + cflv.user_id AS user_id, + cdu.name AS name, + cdl.course_id AS course_id, + cdc.title AS course_title, + max(count_lesson_in_course.total_leeson) AS lessons_in_course, + uniqExactState(cflv.lesson_id) AS lessons_viewed +FROM core_fact_lesson_views cflv +LEFT JOIN core_dim_user cdu ON cflv.user_id = cdu.user_id +LEFT JOIN core_dim_lesson cdl ON cflv.lesson_id = cdl.lesson_id +LEFT JOIN core_dim_course cdc ON cdl.course_id = cdc.course_id +LEFT JOIN ( + SELECT course_id, uniqExact(lesson_id) AS total_leeson + FROM dmedvedev.core_dim_lesson cdm + GROUP BY course_id +) AS count_lesson_in_course ON count_lesson_in_course.course_id = cdl.course_id +GROUP BY cflv.user_id, cdl.course_id, cdu.name, cdc.title; + + +ЗАПРОС: ученики просмотревшие какой-либо курс менее чем на половину +SELECT + user_id, + name, + course_id, + course_title, + max(lessons_in_course) AS total_lessons, + -- Объединяем просмотры: + uniqExactMerge(lessons_viewed) AS viewed_lessons, + IF(total_lessons > 0, CAST(viewed_lessons AS Float64) / total_lessons, 0.0) AS completion_rate +FROM dmedvedev.dm_course_completion_rate +GROUP BY user_id, name, course_id, course_title +HAVING completion_rate <= 0.5 +ORDER BY user_id, course_id; + +--ЭТАП 4. Тестирование и валидация +В ДАННОМ ЗАДАНИИ Я НЕ ПРОИЗВОДИЛ СРАВНЕНИЕ ТАБЛИЦ ИЗ ГП И КХ, ТАК КАК ТАКБЛИЦЫ +С ОДИНАКОВЫМИ НАЗВАНИЯМИ ПРОСТО НАПРОСТО СЧИТАЮТ РАЗНЫЕ ПОКАЗАТЕЛИ. +ПОЧЕМУ ТАК ПРОИЗОШЛО? ПОТОМУ ЧТО В ЗАДАНИИ КХ ДАНЫ СОВСЕМ ДРУГИЕ ПОКАЗАЛИТЕ, ПОЭТОМУ ТАБЛИЦЫ РАЗЛИЧАЮТСЯ ПО НАПОЛНЕНИЮ. + +ЧТО Я ЗАМЕТИЛ ПРИ СОЗДАНИИ МАТ ПРЕДСТАВЛЕНИЙ В КХ? +ТО ЧТО В ГП МАТ ПРЕДСТАВЛЕНИЯ ЭТО ОТДЕЛЬНАЯ ТАБЛИЦА, КОТОРАЯ ХРАНИТ ОПРЕДЕЛЕННЫЙ РЕЗУЛЬТАТ, ОНА НЕ ЯВЛЕТСЯ ДИНАМИЧЕСКОЙ, +ЕЕ НЕОБХОДИМО ОБНОВЛЯТЬ ПРИ ПОСТУПЛЕНИИ НОВЫХ ДАННЫХ, СООТВЕТСТВЕННО ВЕСЬ АНАЛИЗ ДАННЫХ ГП ДЕЛАЕТ ПО ВСЕМ ДАННЫМ ЗАПРОСА. +КХ В ОТЛИЧИЕ ОТ ГП РАБОТАЕТ ИНАЧЕ, ОН ПОДХВАТЫВАЕТ ЛИШЬ НОВЫЕ ДАННЫЕ, ПРИЧЕМ ЭТИ НОВЫЕ ДАННЫЕ МОГУТ ПОСТУПАТЬ ТОЛЬКО ИЗ ТАБЛИЦЫ ФАКТОВ, +ЕСЛИ ОНИ ПОСТУПЯТ ИЗ ТАБЛИЦ ИЗМЕРЕНИЙ, ТО НИКАКИХ РАСЧЕТОМ КХ ДЕЛАТЬ НЕ БУДЕТ. +ТАКЖЕ СТОИТ ОТМЕТИТЬ, ЧТО В КХ ПОЯВЛЯЕТСЯ ЦЕЛЕВАЯ ТАБЛИЦА, В КОТОРУЮ МАТ ПРЕДСТАВЛЕНИЕ И ПРОИЗВОДИТ ЗАПИСЬ ДАННЫХ И ПОТОМ +В ЗАПРОСЕ МЫ ОБРАЩАЕМСЯ ИМЕННО К ЦЕЛЕВОЙ ТАБЛИЦЕ, А НЕ К МАТ ПРЕДСТАВЛЕНИЮ (МАТ ПРЕДСТАВЛЕНИЕ ЭТО ВСЕГО ЛИШЬ ТРИГГЕР, КОТОРЫЕ УЛАВЛИВАЕТ НОВЫЕ ДАННЫЕ ИЗ ТАБЛИЦЫ ФАКТОВ).