исправлены предыдущие ошибки, добавлен полностью блок по кликхаус и пет-проект

This commit is contained in:
Dmitry Medvedev
2026-09-15 15:34:46 +05:00
parent 297df348c1
commit fb0f37912d
11 changed files with 1235 additions and 2 deletions
@@ -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);
+230
View File
@@ -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?
МНОГОУРОВНЕВОСТЬ ЕСТЬ, А РЕКУРСИВНОСТИ НЕТ.
+167
View File
@@ -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".
+115
View File
@@ -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%'`?
В ТАКОМ СЛУЧАЕ ОБЫКНОВЕННЫЙ ФИЛЬТР БЛУМА СРАБОТАЕТ.
+26
View File
@@ -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))
+283
View File
@@ -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. Тестирование и валидация
В ДАННОМ ЗАДАНИИ Я НЕ ПРОИЗВОДИЛ СРАВНЕНИЕ ТАБЛИЦ ИЗ ГП И КХ, ТАК КАК ТАКБЛИЦЫ
С ОДИНАКОВЫМИ НАЗВАНИЯМИ ПРОСТО НАПРОСТО СЧИТАЮТ РАЗНЫЕ ПОКАЗАТЕЛИ.
ПОЧЕМУ ТАК ПРОИЗОШЛО? ПОТОМУ ЧТО В ЗАДАНИИ КХ ДАНЫ СОВСЕМ ДРУГИЕ ПОКАЗАЛИТЕ, ПОЭТОМУ ТАБЛИЦЫ РАЗЛИЧАЮТСЯ ПО НАПОЛНЕНИЮ.
ЧТО Я ЗАМЕТИЛ ПРИ СОЗДАНИИ МАТ ПРЕДСТАВЛЕНИЙ В КХ?
ТО ЧТО В ГП МАТ ПРЕДСТАВЛЕНИЯ ЭТО ОТДЕЛЬНАЯ ТАБЛИЦА, КОТОРАЯ ХРАНИТ ОПРЕДЕЛЕННЫЙ РЕЗУЛЬТАТ, ОНА НЕ ЯВЛЕТСЯ ДИНАМИЧЕСКОЙ,
ЕЕ НЕОБХОДИМО ОБНОВЛЯТЬ ПРИ ПОСТУПЛЕНИИ НОВЫХ ДАННЫХ, СООТВЕТСТВЕННО ВЕСЬ АНАЛИЗ ДАННЫХ ГП ДЕЛАЕТ ПО ВСЕМ ДАННЫМ ЗАПРОСА.
КХ В ОТЛИЧИЕ ОТ ГП РАБОТАЕТ ИНАЧЕ, ОН ПОДХВАТЫВАЕТ ЛИШЬ НОВЫЕ ДАННЫЕ, ПРИЧЕМ ЭТИ НОВЫЕ ДАННЫЕ МОГУТ ПОСТУПАТЬ ТОЛЬКО ИЗ ТАБЛИЦЫ ФАКТОВ,
ЕСЛИ ОНИ ПОСТУПЯТ ИЗ ТАБЛИЦ ИЗМЕРЕНИЙ, ТО НИКАКИХ РАСЧЕТОМ КХ ДЕЛАТЬ НЕ БУДЕТ.
ТАКЖЕ СТОИТ ОТМЕТИТЬ, ЧТО В КХ ПОЯВЛЯЕТСЯ ЦЕЛЕВАЯ ТАБЛИЦА, В КОТОРУЮ МАТ ПРЕДСТАВЛЕНИЕ И ПРОИЗВОДИТ ЗАПИСЬ ДАННЫХ И ПОТОМ
В ЗАПРОСЕ МЫ ОБРАЩАЕМСЯ ИМЕННО К ЦЕЛЕВОЙ ТАБЛИЦЕ, А НЕ К МАТ ПРЕДСТАВЛЕНИЮ (МАТ ПРЕДСТАВЛЕНИЕ ЭТО ВСЕГО ЛИШЬ ТРИГГЕР, КОТОРЫЕ УЛАВЛИВАЕТ НОВЫЕ ДАННЫЕ ИЗ ТАБЛИЦЫ ФАКТОВ).