исправлены предыдущие ошибки, добавлен полностью блок по кликхаус и пет-проект
This commit is contained in:
@@ -192,7 +192,7 @@
|
|||||||
1. SELECT * FROM users WHERE id = 100; нужен по полю id
|
1. SELECT * FROM users WHERE id = 100; нужен по полю id
|
||||||
2. SELECT * FROM users WHERE email = 'user@example.com'; нужен по полю email
|
2. SELECT * FROM users WHERE email = 'user@example.com'; нужен по полю email
|
||||||
3. SELECT * FROM products WHERE price BETWEEN 100 AND 500; нужен по price
|
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
|
5. SELECT * FROM orders WHERE status = 'DELIVERED'; нужен по status
|
||||||
6. SELECT * FROM order_items WHERE order_id = 123; нужен по order_id
|
6. SELECT * FROM order_items WHERE order_id = 123; нужен по order_id
|
||||||
|
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ SELECT
|
|||||||
) AS course_title,
|
) AS course_title,
|
||||||
COUNT(DISTINCT dl.lesson_id) AS lessons_in_course,
|
COUNT(DISTINCT dl.lesson_id) AS lessons_in_course,
|
||||||
COUNT(DISTINCT flv.lesson_id) AS lessons_viewed,
|
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
|
FROM core.dim_user du
|
||||||
|
|
||||||
LEFT JOIN core.enrollments e ON du.user_id = e.user_id
|
LEFT JOIN core.enrollments e ON du.user_id = e.user_id
|
||||||
|
|||||||
@@ -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