исправлены предыдущие ошибки, добавлен полностью блок по кликхаус и пет-проект
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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?
|
||||
МНОГОУРОВНЕВОСТЬ ЕСТЬ, А РЕКУРСИВНОСТИ НЕТ.
|
||||
@@ -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 ЖЕ ВЫБИРАЕТСЯ КОГДА НЕОБХОДИМО ДЕКАРТОВО ПРОИЗВЕДЕНИЕ.
|
||||
@@ -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".
|
||||
@@ -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%'`?
|
||||
В ТАКОМ СЛУЧАЕ ОБЫКНОВЕННЫЙ ФИЛЬТР БЛУМА СРАБОТАЕТ.
|
||||
@@ -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))
|
||||
@@ -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. Тестирование и валидация
|
||||
В ДАННОМ ЗАДАНИИ Я НЕ ПРОИЗВОДИЛ СРАВНЕНИЕ ТАБЛИЦ ИЗ ГП И КХ, ТАК КАК ТАКБЛИЦЫ
|
||||
С ОДИНАКОВЫМИ НАЗВАНИЯМИ ПРОСТО НАПРОСТО СЧИТАЮТ РАЗНЫЕ ПОКАЗАТЕЛИ.
|
||||
ПОЧЕМУ ТАК ПРОИЗОШЛО? ПОТОМУ ЧТО В ЗАДАНИИ КХ ДАНЫ СОВСЕМ ДРУГИЕ ПОКАЗАЛИТЕ, ПОЭТОМУ ТАБЛИЦЫ РАЗЛИЧАЮТСЯ ПО НАПОЛНЕНИЮ.
|
||||
|
||||
ЧТО Я ЗАМЕТИЛ ПРИ СОЗДАНИИ МАТ ПРЕДСТАВЛЕНИЙ В КХ?
|
||||
ТО ЧТО В ГП МАТ ПРЕДСТАВЛЕНИЯ ЭТО ОТДЕЛЬНАЯ ТАБЛИЦА, КОТОРАЯ ХРАНИТ ОПРЕДЕЛЕННЫЙ РЕЗУЛЬТАТ, ОНА НЕ ЯВЛЕТСЯ ДИНАМИЧЕСКОЙ,
|
||||
ЕЕ НЕОБХОДИМО ОБНОВЛЯТЬ ПРИ ПОСТУПЛЕНИИ НОВЫХ ДАННЫХ, СООТВЕТСТВЕННО ВЕСЬ АНАЛИЗ ДАННЫХ ГП ДЕЛАЕТ ПО ВСЕМ ДАННЫМ ЗАПРОСА.
|
||||
КХ В ОТЛИЧИЕ ОТ ГП РАБОТАЕТ ИНАЧЕ, ОН ПОДХВАТЫВАЕТ ЛИШЬ НОВЫЕ ДАННЫЕ, ПРИЧЕМ ЭТИ НОВЫЕ ДАННЫЕ МОГУТ ПОСТУПАТЬ ТОЛЬКО ИЗ ТАБЛИЦЫ ФАКТОВ,
|
||||
ЕСЛИ ОНИ ПОСТУПЯТ ИЗ ТАБЛИЦ ИЗМЕРЕНИЙ, ТО НИКАКИХ РАСЧЕТОМ КХ ДЕЛАТЬ НЕ БУДЕТ.
|
||||
ТАКЖЕ СТОИТ ОТМЕТИТЬ, ЧТО В КХ ПОЯВЛЯЕТСЯ ЦЕЛЕВАЯ ТАБЛИЦА, В КОТОРУЮ МАТ ПРЕДСТАВЛЕНИЕ И ПРОИЗВОДИТ ЗАПИСЬ ДАННЫХ И ПОТОМ
|
||||
В ЗАПРОСЕ МЫ ОБРАЩАЕМСЯ ИМЕННО К ЦЕЛЕВОЙ ТАБЛИЦЕ, А НЕ К МАТ ПРЕДСТАВЛЕНИЮ (МАТ ПРЕДСТАВЛЕНИЕ ЭТО ВСЕГО ЛИШЬ ТРИГГЕР, КОТОРЫЕ УЛАВЛИВАЕТ НОВЫЕ ДАННЫЕ ИЗ ТАБЛИЦЫ ФАКТОВ).
|
||||
Reference in New Issue
Block a user