Merge branch 'main' into vberetar_dates_strings_logic
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
--Типы данных
|
||||
|
||||
--Задача 1
|
||||
CREATE TABLE raw_data
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id String,
|
||||
balance String,
|
||||
created_at String
|
||||
) ENGINE = MergeTree ORDER BY tuple();
|
||||
|
||||
SELECT DISTINCT
|
||||
toDecimal64OrNull(balance, 2) AS balance_decimal,
|
||||
toDateTime64OrNull(created_at, 3) AS created_at_date
|
||||
FROM raw_data
|
||||
WHERE
|
||||
toDecimal64OrNull(balance, 2) IS NOT NULL
|
||||
AND toDateTime64OrNull(created_at, 3) IS NOT NULL
|
||||
|
||||
/*
|
||||
CAST(balance AS Decimal(10, 2)) - этом случае будет ошибка, так как данный метод не поддеживает NULL
|
||||
|
||||
Следующие два метода по сути своей преобразование типа данных с условием ИЛИ, то есть:
|
||||
toDecimal64OrNull(balance, 2) - в этом методе на выходе будут корректные данные, в противном случае NULL
|
||||
toDecimal64OrZero(balance, 2) - аналогично с NULL, только будет ноль.
|
||||
*/
|
||||
|
||||
--Задача 2
|
||||
CREATE TABLE events
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
event_id String,
|
||||
event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid"
|
||||
event_time String -- форматы: "12:30:45", "25:99:99"
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
ORDER BY event_id;
|
||||
|
||||
--Заполнил данными для проверки
|
||||
INSERT INTO events (event_id, event_date, event_time) VALUES
|
||||
('1', '2023-01-01', '12:30:45'),
|
||||
('2', '01.02.2023', '08:15:00'),
|
||||
('3', 'invalid', '25:99:99'),
|
||||
('4', '2023-05-20', 'invalid'),
|
||||
('5', '31.12.2023', '23:59:59');
|
||||
|
||||
SELECT * FROM events;
|
||||
|
||||
SELECT DISTINCT
|
||||
toDate(DateTime_Valid) AS Event_Date,
|
||||
toHour(DateTime_Valid) AS Event_Hour,
|
||||
DateTime_Valid
|
||||
FROM (
|
||||
SELECT
|
||||
parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS DateTime_Valid
|
||||
FROM events
|
||||
)
|
||||
WHERE DateTime_Valid IS NOT NULL;
|
||||
|
||||
--Задача 3
|
||||
DROP TABLE IF EXISTS user_activity ON CLUSTER clickhouse_cluster;
|
||||
|
||||
CREATE TABLE user_activity
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt32,
|
||||
actions String -- JSON: ["login", "purchase", "logout"]
|
||||
) ENGINE = MergeTree ORDER BY tuple();
|
||||
|
||||
--Тест данные
|
||||
INSERT INTO user_activity (user_id, actions) VALUES
|
||||
(1, '["login", "purchase", "logout"]'),
|
||||
(2, '["login", "logout"]'),
|
||||
(3, '["login"]'),
|
||||
(4, '["login","purchase"]');
|
||||
|
||||
SELECT
|
||||
user_id,
|
||||
-- Сначала реобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Потом считваем длину массива и дальше суммируем длину всех массивов действий для каждого пользователя
|
||||
sum(length(JSONExtract(actions, 'Array(String)'))) AS count_actions
|
||||
FROM user_activity
|
||||
WHERE
|
||||
-- Проверяем наличие 'purchase' в массиве
|
||||
has(JSONExtract(actions, 'Array(String)'), 'purchase')
|
||||
GROUP BY
|
||||
user_id;
|
||||
/*
|
||||
Преобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Нашел пользователей в новом массиве Array(String), совершивших действие "purchase".
|
||||
Просто написать actions = 'purchase' не получится, так как изначально actions просто (String) и соответсвенно "["login", "purchase", "logout"]" нельзя сравнить с "purchase". Поэтому сначала преобразовываем в массив, а потом с функцией has(array, elem) ищем то, что нужно.
|
||||
То есть менять бы не пришлось, если бы в actions было изначально разные значения в каждой строке ("login", "purchase" или "logout")
|
||||
*/
|
||||
|
||||
--Задача 4
|
||||
DROP TABLE IF EXISTS sensor_data ON CLUSTER clickhouse_cluster;
|
||||
|
||||
CREATE TABLE sensor_data
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
sensor_id UInt32,
|
||||
value Float64,
|
||||
status Enum('ok' = 1, 'error' = 2)
|
||||
) ORDER BY sensor_id;
|
||||
|
||||
--Тест данные
|
||||
INSERT INTO sensor_data (sensor_id, value, status) VALUES
|
||||
(1, 25.5, 'ok'),
|
||||
(2, 120.75, 'error'),
|
||||
(3, 15.0, 1),
|
||||
(4, 9999999.99, 'ok'),
|
||||
(5, 0.001, 'ok');
|
||||
|
||||
SELECT
|
||||
toString(status) AS status_string,
|
||||
avg(value) AS avg_value --Среднее значение value для каждого status
|
||||
FROM sensor_data
|
||||
WHERE accurateCastOrNull(value, 'Decimal(10,2)') IS NOT NULL --Фильтруем строки, где value нельзя преобразовать в Decimal(10,2)
|
||||
GROUP BY status_string;
|
||||
@@ -0,0 +1,70 @@
|
||||
|
||||
-- Задачи на отработку Запросы - JOIN --
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.products ON CLUSTER clickhouse_cluster SYNC;
|
||||
DROP TABLE IF EXISTS dsolovianov.prices ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.products
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
product_id UInt32,
|
||||
name String
|
||||
) ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/products',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY product_id;
|
||||
|
||||
|
||||
INSERT INTO dsolovianov.products VALUES
|
||||
(101, 'iPhone 14'),
|
||||
(102, 'Galaxy S22'),
|
||||
(103, 'Pixel 7');
|
||||
|
||||
CREATE TABLE dsolovianov.prices
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
product_id UInt32,
|
||||
price UInt32,
|
||||
region String
|
||||
) ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/prices',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY product_id;
|
||||
|
||||
INSERT INTO dsolovianov.prices VALUES
|
||||
(101, 999, 'US'),
|
||||
(101, 1049, 'EU'),
|
||||
(102, 850, 'US'),
|
||||
(102, 899, 'CA'),
|
||||
(103, 799, 'JP');
|
||||
|
||||
|
||||
--№1 Сделай ANY LEFT JOIN, чтобы получить цену любой из регионов.
|
||||
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.name,
|
||||
pr.price,
|
||||
pr.region
|
||||
FROM dsolovianov.products AS p
|
||||
ANY LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id;
|
||||
|
||||
--№2 Что будет, если в таблице `prices` несколько цен на один `product_id`?
|
||||
|
||||
/* Для одного product_id будет выбрана только первая ЛЮБАЯ (ANY) подходящая в поиске.*/
|
||||
|
||||
--№3 Как повлияет `ALL LEFT JOIN`? Почему в ClickHouse его избегают?
|
||||
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.name,
|
||||
pr.price,
|
||||
pr.region
|
||||
FROM dsolovianov.products AS p
|
||||
ALL LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id;
|
||||
|
||||
/* `ALL LEFT JOIN` в Clickhouse ведет себя также, как LEFT JOIN в SQL, то есть собирает все совпадения.
|
||||
Начинается перегон данных между таблицами. Из-за этого растет нагрузка на ОЗУ и CPU.
|
||||
короче, по сути ALL LEFT JOIN создает декартово произведение*/
|
||||
@@ -0,0 +1,244 @@
|
||||
/* Запросы - Синтаксис*/
|
||||
|
||||
--! Задача 1 !--
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE users
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt32,
|
||||
name String,
|
||||
age UInt8,
|
||||
country String,
|
||||
is_active Bool
|
||||
) ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/users_v2',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
|
||||
INSERT INTO users VALUES
|
||||
(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);
|
||||
|
||||
--Получи 10 самых старших активных пользователей.
|
||||
SELECT
|
||||
user_id,
|
||||
name,
|
||||
age,
|
||||
is_active
|
||||
FROM users
|
||||
WHERE is_active = true
|
||||
ORDER BY age DESC
|
||||
LIMIT 10;
|
||||
|
||||
--Выведи пользователей из country = 'RU', отсортированных по имени.
|
||||
SELECT
|
||||
user_id,
|
||||
name,
|
||||
country
|
||||
FROM users
|
||||
WHERE country = 'RU'
|
||||
ORDER BY name;
|
||||
|
||||
--Что произойдёт, если написать LIMIT 10 без ORDER BY? Просто выведет 10 случайных активных прользователей без какой либо сортировки.
|
||||
SELECT
|
||||
user_id,
|
||||
name,
|
||||
age
|
||||
FROM users
|
||||
WHERE is_active = true
|
||||
LIMIT 10;
|
||||
|
||||
--Посчитай количество пользователей по странам.
|
||||
SELECT
|
||||
country,
|
||||
COUNT(*) as user_country
|
||||
FROM users
|
||||
GROUP BY country;
|
||||
|
||||
SELECT
|
||||
country,
|
||||
uniqExact(country) as unique_countries --возвращает точное количество уникальных значений в столбце country
|
||||
FROM users
|
||||
GROUP BY country;
|
||||
|
||||
--Для каждой страны выведи минимальный и максимальный возраст.
|
||||
SELECT
|
||||
country,
|
||||
MIN(age) as min_age,
|
||||
MAX(age) as max_age
|
||||
FROM users
|
||||
GROUP BY country
|
||||
ORDER BY country;
|
||||
|
||||
--Отсортируй страны по убыванию количества пользователей.
|
||||
SELECT
|
||||
country,
|
||||
COUNT(*) as user_country
|
||||
FROM users
|
||||
GROUP BY country
|
||||
ORDER BY user_country DESC;
|
||||
|
||||
--Всё вместе
|
||||
SELECT
|
||||
country,
|
||||
COUNT(*) as user_country,
|
||||
SUM(is_active) as active_users,
|
||||
MIN(age) as min_age,
|
||||
MAX(age) as max_age,
|
||||
uniqExact(country) as unique_countries
|
||||
FROM users
|
||||
GROUP BY country
|
||||
ORDER BY country;
|
||||
|
||||
--! Задача 2 !--
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.events ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt32,
|
||||
event_time DateTime,
|
||||
event_type String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/events_v2',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY (user_id, event_time);
|
||||
|
||||
INSERT INTO dsolovianov.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,
|
||||
event_time,
|
||||
event_type,
|
||||
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time) AS cumulative_events
|
||||
FROM dsolovianov.events
|
||||
ORDER BY user_id, event_time;
|
||||
|
||||
--2. Сделай аналог ранжирования событий внутри пользователя: `row_number() OVER (PARTITION BY user_id ORDER BY event_time)`
|
||||
SELECT
|
||||
user_id,
|
||||
event_time,
|
||||
event_type,
|
||||
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time) AS event_rank
|
||||
FROM dsolovianov.events
|
||||
ORDER BY user_id, event_rank;
|
||||
|
||||
--3. Что будет, если ты явно задашь `ROWS BETWEEN`? Поддерживается ли?
|
||||
--Да, `ROWS BETWEEN` поддерживается:
|
||||
SELECT
|
||||
user_id,
|
||||
event_time,
|
||||
event_type,
|
||||
-- кумулятивное окно (все предыдущие строки)
|
||||
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_events,
|
||||
-- скользящее окно: текущая + 2 предыдущих события
|
||||
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time
|
||||
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS last_3_events,
|
||||
-- окно: предыдущая + текущая + следующая
|
||||
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time
|
||||
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS prev_curr_next
|
||||
FROM dsolovianov.events
|
||||
ORDER BY user_id, event_time;
|
||||
|
||||
--4. Сделай `WITH recent_events AS (...)` — все события за последние 24 часа.
|
||||
WITH recent_events AS (
|
||||
SELECT *
|
||||
FROM dsolovianov.events
|
||||
WHERE event_time >= '2025-07-16 15:00:00'
|
||||
)
|
||||
SELECT * FROM recent_events
|
||||
ORDER BY event_time DESC;
|
||||
|
||||
--5. Используй CTE, чтобы посчитать топ-5 пользователей по числу этих событий.
|
||||
WITH recent_events AS (
|
||||
SELECT *
|
||||
FROM dsolovianov.events
|
||||
WHERE event_time >= '2025-07-16 15:00:00'
|
||||
)
|
||||
SELECT
|
||||
user_id,
|
||||
COUNT(*) AS event_count
|
||||
FROM recent_events
|
||||
GROUP BY user_id
|
||||
ORDER BY event_count DESC
|
||||
LIMIT 5;
|
||||
|
||||
--6. Добавь второй `WITH` — для маппинга `event_type → category` и используй его в основном запросе.
|
||||
WITH
|
||||
event_categories AS (
|
||||
SELECT
|
||||
event_type,
|
||||
CASE
|
||||
WHEN event_type = 'click' THEN 'engagement'
|
||||
WHEN event_type = 'view' THEN 'engagement'
|
||||
WHEN event_type = 'purchase' THEN 'conversion'
|
||||
ELSE 'other'
|
||||
END AS category
|
||||
FROM (SELECT DISTINCT event_type FROM dsolovianov.events)
|
||||
),
|
||||
recent_events AS (
|
||||
SELECT *
|
||||
FROM dsolovianov.events
|
||||
WHERE event_time >= '2025-07-16 15:00:00'
|
||||
)
|
||||
SELECT
|
||||
re.user_id,
|
||||
re.event_time,
|
||||
re.event_type,
|
||||
ec.category,
|
||||
ROW_NUMBER() OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS event_rank,
|
||||
COUNT(*) OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS cumulative_events
|
||||
FROM recent_events re
|
||||
LEFT JOIN event_categories ec ON re.event_type = ec.event_type
|
||||
ORDER BY re.user_id, re.event_time;
|
||||
|
||||
/*
|
||||
Вопрос: поддерживает ли ClickHouse многоуровневые или рекурсивные CTE?
|
||||
многоуровневые CTE - да
|
||||
рекурсивные CTE - нет в классическом понимании, а скорее в итеративном смысле.
|
||||
*/
|
||||
@@ -0,0 +1,166 @@
|
||||
-- Индексы и сжатие - Индексы --
|
||||
|
||||
-- #1
|
||||
/*
|
||||
3. Посмотри, сколько блоков было пропущено (в `system.query_log`, `rows_read`, `rows`).
|
||||
Не посмотрю, так как нет доступа system.query_log, rows_read, rows (ACCESS_DENIED)
|
||||
4. Что будет, если `ORDER BY log_id`, а не `ts`?
|
||||
ORDER BY log_id → данные физически отсортированы по log_id. Значения ts расположены хаотично относительно физической сортировки.
|
||||
Индекс minmax по ts будет почти бесполезен. Minmax индекс будет иметь широкие диапазоны
|
||||
*/
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.logs
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
log_id UInt64,
|
||||
ts DateTime
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/logs',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY ts
|
||||
PARTITION BY toYYYYMM(ts)
|
||||
PRIMARY KEY (ts);
|
||||
|
||||
INSERT INTO dsolovianov.logs (log_id, ts) VALUES
|
||||
(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. Добавляем minmax индекс:
|
||||
ALTER TABLE dsolovianov.logs ADD INDEX idx_ts ts TYPE minmax GRANULARITY 2;
|
||||
|
||||
-- 2. Сделай запрос: `SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03'`
|
||||
SELECT *
|
||||
FROM dsolovianov.logs
|
||||
WHERE ts
|
||||
BETWEEN '2024-06-02' AND '2024-06-03';
|
||||
|
||||
-- Раз нет доступа, но можно предположить, что вот такой запрос сработает:
|
||||
SELECT query, read_rows, result_rows
|
||||
FROM system.query_log
|
||||
WHERE query LIKE '%logs%'
|
||||
ORDER BY event_time DESC
|
||||
LIMIT 1;
|
||||
|
||||
|
||||
-- #2
|
||||
/*
|
||||
1. Создай таблицу с `ORDER BY id`, добавь индекс:
|
||||
|
||||
```sql
|
||||
ALTER TABLE clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2;
|
||||
```
|
||||
|
||||
|
||||
3. Посмотри `EXPLAIN` — индекс используется?
|
||||
Данные отсортированы по id, а не по country. Значения country распределены хаотически относительно физического порядка.
|
||||
4. Как будет вести себя индекс, если `country` имеет 10M уникальных значений?
|
||||
Будет создано 10M партиций, что убьёт производительность.
|
||||
**/
|
||||
DROP TABLE IF EXISTS dsolovianov.clicks ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.clicks
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
country String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/clicks',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY id
|
||||
PARTITION BY country
|
||||
PRIMARY KEY (id);
|
||||
|
||||
INSERT INTO dsolovianov.clicks (id, country) VALUES
|
||||
(1, 'RU'),
|
||||
(2, 'US'),
|
||||
(3, 'RU'),
|
||||
(4, 'DE'),
|
||||
(5, 'RU');
|
||||
|
||||
ALTER TABLE dsolovianov.clicks ON CLUSTER clickhouse_cluster
|
||||
ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2;
|
||||
|
||||
-- 2. Выполни запрос `SELECT * FROM clicks WHERE country = 'RU'`.
|
||||
EXPLAIN indexes = 1
|
||||
SELECT * FROM dsolovianov.clicks WHERE country = 'RU';
|
||||
|
||||
EXPLAIN
|
||||
SELECT * FROM dsolovianov.clicks WHERE country = 'RU';
|
||||
|
||||
/*
|
||||
Expression ((Project names + Projection))
|
||||
Expression ((WHERE + Change column names to column identifiers))
|
||||
ReadFromMergeTree (dsolovianov.clicks)
|
||||
Indexes:
|
||||
MinMax
|
||||
Keys:
|
||||
country
|
||||
Condition: (country in ['RU', 'RU'])
|
||||
Parts: 1/3
|
||||
Granules: 1/3
|
||||
Partition
|
||||
Keys:
|
||||
country
|
||||
Condition: (country in ['RU', 'RU'])
|
||||
Parts: 1/1
|
||||
Granules: 1/1
|
||||
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
|
||||
*/
|
||||
|
||||
-- #3
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.users
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
email String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/users',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY id
|
||||
PRIMARY KEY (id);
|
||||
|
||||
INSERT INTO dsolovianov.users (id, email) VALUES
|
||||
(1, 'alice@mail.com'),
|
||||
(2, 'bob@mail.com'),
|
||||
(3, 'charlie@mail.com'),
|
||||
(4, 'dave@mail.com'),
|
||||
(5, 'eve@example.com');
|
||||
|
||||
ALTER TABLE dsolovianov.users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2;
|
||||
|
||||
EXPLAIN indexes = 1
|
||||
SELECT * FROM users WHERE email = 'alice@mail.com';
|
||||
-- Bloom filter может пропустить некоторые гранулы, но не все, ибо данные не отсортированы по email и индексные блоки покрывают случайные email'ы
|
||||
|
||||
EXPLAIN indexes = 1
|
||||
SELECT * FROM users WHERE email LIKE '%mail%';
|
||||
|
||||
EXPLAIN indexes = 1
|
||||
SELECT * FROM users WHERE email LIKE 'alice%';
|
||||
/*
|
||||
Bloom filter не поддерживает частичные совпадения (LIKE '..%', LIKE '%..', LIKE '%..%'). Из-за этого ClickHouse не может пропустить гранулы.
|
||||
Клику становится сложно вычислить хэш для проверки, если подходящих значений по LIKE будет больше одного. Нет гарантии, что данные на выходе будут корректные.
|
||||
*/
|
||||
@@ -0,0 +1,146 @@
|
||||
-- Индексы и сжатие - Партиции --
|
||||
|
||||
-- #1
|
||||
/*
|
||||
1. Создай таблицу sales с `PARTITION BY toYYYYMM(sale_date)`.
|
||||
2. Выполни `ALTER TABLE ... DROP PARTITION '202405'`.
|
||||
3. Что произойдет, если не указывать `PARTITION BY`? <- удалит все данные из таблицы. партиция то одна :)
|
||||
|
||||
*/
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.sales ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.sales
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
region String,
|
||||
sale_date Date,
|
||||
amount Float64
|
||||
) ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/sales',
|
||||
'{replica}'
|
||||
)
|
||||
PARTITION BY toYYYYMM(sale_date)
|
||||
ORDER BY (region, sale_date);
|
||||
|
||||
INSERT INTO dsolovianov.sales (id, region, sale_date, amount) VALUES
|
||||
(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');
|
||||
|
||||
ALTER TABLE dsolovianov.sales DROP PARTITION '202405';
|
||||
|
||||
SELECT *
|
||||
FROM dsolovianov.sales;
|
||||
|
||||
-- #2
|
||||
/*
|
||||
1. Создай таблицу с `PARTITION BY event_type`.
|
||||
2. Удали все события `click` (подсказка: это `ALTER TABLE DROP PARTITION 'click'`).
|
||||
3. Чем такой подход может быть опасен (подумай про кардинальность значений)?
|
||||
При большом количестве уникальных event_type создастся много мелких партиций.
|
||||
*/
|
||||
DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.events
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
event_type String,
|
||||
event_date Date
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/events',
|
||||
'{replica}'
|
||||
)
|
||||
PARTITION BY event_type
|
||||
ORDER BY (event_date, id);
|
||||
|
||||
INSERT INTO dsolovianov.events (id, event_type, event_date) VALUES
|
||||
(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');
|
||||
|
||||
ALTER TABLE dsolovianov.events DROP PARTITION 'click';
|
||||
|
||||
-- #3
|
||||
/*
|
||||
1. Создай таблицу с `PARTITION BY cityHash64(user_id) % 100`.
|
||||
2. Что ты выиграешь с точки зрения баланса?
|
||||
Данные распределятся примерно поровну между 100 партициями. Упрощается управление удалением старых данных (можно дропать целые партиции)
|
||||
3. Почему такой тип `PARTITION` не ускорит `SELECT * WHERE user_id = 456`?
|
||||
Потому что партиции определяются хэшем, а не прямым значением user_id и к тому же сортировка в партициях по log_time, а не user_id.
|
||||
|
||||
*/
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.logs
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
user_id UInt64,
|
||||
log_time DateTime
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/logs',
|
||||
'{replica}'
|
||||
)
|
||||
PARTITION BY cityHash64(user_id) % 100 -- 100 партиций по хэшу user_id
|
||||
ORDER BY (log_time, id);
|
||||
|
||||
INSERT INTO dsolovianov.logs (id, user_id, log_time) VALUES
|
||||
(1, 123, '2024-07-17 10:00:00'),
|
||||
(2, 456, '2024-07-17 10:01:00'),
|
||||
(3, 789, '2024-07-17 10:02:00');
|
||||
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
cityHash64(user_id) % 100 AS partition_number
|
||||
FROM dsolovianov.logs
|
||||
ORDER BY id;
|
||||
|
||||
SELECT * FROM logs WHERE user_id = 456;
|
||||
|
||||
|
||||
-- #4
|
||||
/*
|
||||
1. Создай таблицу с `PARTITION BY (region, toYYYYMM(visit_date))`.
|
||||
2. Как будет выглядеть имя партиции?
|
||||
Имена партиций будут в формате: ('значение_region', 'YYYYMM'). Пример: ('RU', '202406')
|
||||
3. Сможешь ли ты эффективно удалить **все июльские данные по `RU`**? Да. Удаляется целая партиция без сканирования и нагрузки на систему.
|
||||
*/
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.visits ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.visits
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
region String,
|
||||
visit_date Date
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/visits',
|
||||
'{replica}'
|
||||
)
|
||||
PARTITION BY (region, toYYYYMM(visit_date))
|
||||
ORDER BY (region, visit_date, id);
|
||||
|
||||
INSERT INTO dsolovianov.visits (id, region, visit_date) VALUES
|
||||
(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');
|
||||
|
||||
ALTER TABLE dsolovianov.visits ON CLUSTER clickhouse_cluster
|
||||
DROP PARTITION ('RU', '202407');
|
||||
|
||||
SELECT * FROM dsolovianov.visits;
|
||||
@@ -0,0 +1,39 @@
|
||||
-- Индексы и сжатие - Сжатие --
|
||||
|
||||
-- #1 Создай таблицу logs с колонкой message CODEC(ZSTD(3)).
|
||||
-- #2 Добавь created_at CODEC(Delta, ZSTD) — это улучшит хранение по времени.
|
||||
DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.logs
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
level String,
|
||||
message String CODEC(ZSTD(3)),
|
||||
created_at DateTime CODEC(Delta, ZSTD)
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/logs',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY (created_at, id)
|
||||
PARTITION BY toYYYYMM(created_at);
|
||||
|
||||
INSERT INTO dsolovianov.logs (id, level, message, created_at) VALUES
|
||||
(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');
|
||||
|
||||
-- #3 Посмотри статистику сжатия:
|
||||
SELECT
|
||||
column,
|
||||
compression_codec
|
||||
FROM system.columns
|
||||
WHERE table = 'logs';
|
||||
|
||||
-- Стата вот так выглядит:
|
||||
column | compression_codec
|
||||
id
|
||||
level
|
||||
message CODEC(ZSTD(3))
|
||||
created_at CODEC(Delta(4), ZSTD(1))
|
||||
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
+18
-9
@@ -1,6 +1,9 @@
|
||||
--Задача 1: Создай таблицу page_views
|
||||
DROP TABLE page_views;
|
||||
|
||||
CREATE TABLE page_views (
|
||||
CREATE TABLE page_views
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
url String,
|
||||
view_time DateTime
|
||||
@@ -11,7 +14,9 @@ ORDER BY (user_id, view_time);
|
||||
|
||||
--Задача 2: Создай таблицу users
|
||||
|
||||
CREATE TABLE users (
|
||||
CREATE TABLE users
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
name String,
|
||||
updated_at DateTime
|
||||
@@ -21,7 +26,9 @@ ORDER BY id;
|
||||
|
||||
-- Задача 3: Создай таблицу region_sales
|
||||
|
||||
CREATE TABLE region_sales (
|
||||
CREATE TABLE region_sales
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
region String,
|
||||
date Date,
|
||||
sales UInt64
|
||||
@@ -30,17 +37,19 @@ ENGINE = SummingMergeTree()
|
||||
PARTITION BY toYYYYMM(date)
|
||||
ORDER BY (region, date);
|
||||
|
||||
--Задача 4.1: Создай таблицу user_metrics
|
||||
--Задача 4: Создай таблицу user_metrics
|
||||
|
||||
CREATE TABLE user_metrics (
|
||||
CREATE TABLE user_metrics
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
region String,
|
||||
avg_age_state AggregateFunction(avg, UInt8),
|
||||
active_users_state AggregateFunction(uniq, UInt64)
|
||||
)
|
||||
ENGINE = AggregatingMergeTree()
|
||||
ORDER BY region;
|
||||
ENGINE = AggregatingMergeTree() --Подзадача 4.2
|
||||
ORDER BY region; --Подзадача 4.3
|
||||
|
||||
--Задача 4.3: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()`
|
||||
--Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()`
|
||||
|
||||
INSERT INTO user_metrics
|
||||
SELECT
|
||||
@@ -61,7 +70,7 @@ FROM
|
||||
)
|
||||
GROUP BY region;
|
||||
|
||||
--Задача 4.4: - Читай их через `avgMerge()` и `uniqMerge()`
|
||||
--Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()`
|
||||
|
||||
SELECT
|
||||
region,
|
||||
@@ -0,0 +1,90 @@
|
||||
DROP TABLE IF EXISTS dsolovianov.core_fact_lesson_views ON CLUSTER clickhouse_cluster SYNC;
|
||||
DROP TABLE IF EXISTS dsolovianov.core_dim_user ON CLUSTER clickhouse_cluster SYNC;
|
||||
DROP TABLE IF EXISTS dsolovianov.core_dim_course ON CLUSTER clickhouse_cluster SYNC;
|
||||
DROP TABLE IF EXISTS dsolovianov.core_dim_lesson ON CLUSTER clickhouse_cluster SYNC;
|
||||
DROP TABLE IF EXISTS dsolovianov.core_dim_date ON CLUSTER clickhouse_cluster SYNC;
|
||||
DROP TABLE IF EXISTS dsolovianov.core_enrollments ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.core_dim_user
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id Int32,
|
||||
name String,
|
||||
age Int32,
|
||||
email String,
|
||||
registration_date Date
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_dim_user',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
|
||||
CREATE TABLE dsolovianov.core_dim_course
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
course_id Int32,
|
||||
title String,
|
||||
category String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_dim_course',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY course_id;
|
||||
|
||||
CREATE TABLE dsolovianov.core_dim_lesson
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
lesson_id Int32,
|
||||
title String,
|
||||
duration_min Int32,
|
||||
course_id Int32
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_dim_lesson',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY lesson_id;
|
||||
|
||||
CREATE TABLE dsolovianov.core_dim_date
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
date Date,
|
||||
year Int32,
|
||||
month Int32,
|
||||
day Int32,
|
||||
weekday String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_dim_date',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY date;
|
||||
|
||||
CREATE TABLE dsolovianov.core_enrollments
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id Int32,
|
||||
course_id Int32,
|
||||
enrolled_at DateTime64(3)
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_enrollments',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
|
||||
CREATE TABLE dsolovianov.core_fact_lesson_views
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id Int32,
|
||||
lesson_id Int32,
|
||||
course_id Int32,
|
||||
viewed_at DateTime64(3)
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_fact_lesson_views',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
@@ -0,0 +1,193 @@
|
||||
-- Этап 2. Создание витринных таблиц --
|
||||
|
||||
-- Витрина lesson_popularity_summary
|
||||
DROP TABLE IF EXISTS dsolovianov.lesson_popularity_summary ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.lesson_popularity_summary ON CLUSTER clickhouse_cluster (
|
||||
lesson_id UInt32,
|
||||
lesson_title String,
|
||||
course_id UInt32,
|
||||
course_title String,
|
||||
total_views UInt64,
|
||||
unique_users UInt64,
|
||||
first_view DateTime,
|
||||
last_view DateTime
|
||||
) ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/lesson_popularity_summary',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY (course_id, lesson_id);
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.lesson_popularity_summary_mv ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE MATERIALIZED VIEW dsolovianov.lesson_popularity_summary_mv ON CLUSTER clickhouse_cluster
|
||||
TO dsolovianov.lesson_popularity_summary
|
||||
AS
|
||||
SELECT
|
||||
l.lesson_id AS lesson_id,
|
||||
l.title AS lesson_title,
|
||||
l.course_id AS course_id,
|
||||
c.title AS course_title,
|
||||
COUNT(v.viewed_at) AS total_views,
|
||||
COUNT(DISTINCT v.user_id) AS unique_users,
|
||||
MIN(v.viewed_at) AS first_view,
|
||||
MAX(v.viewed_at) AS last_view
|
||||
FROM dsolovianov.core_fact_lesson_views v
|
||||
LEFT JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||
LEFT JOIN dsolovianov.core_dim_course c ON l.course_id = c.course_id
|
||||
GROUP BY l.lesson_id, l.title, l.course_id, c.title;
|
||||
|
||||
INSERT INTO dsolovianov.lesson_popularity_summary
|
||||
SELECT
|
||||
l.lesson_id AS lesson_id,
|
||||
l.title AS lesson_title,
|
||||
l.course_id AS course_id,
|
||||
c.title AS course_title,
|
||||
COUNT(v.viewed_at) AS total_views,
|
||||
COUNT(DISTINCT v.user_id) AS unique_users,
|
||||
MIN(v.viewed_at) AS first_view,
|
||||
MAX(v.viewed_at) AS last_view
|
||||
FROM dsolovianov.core_dim_lesson l
|
||||
JOIN dsolovianov.core_dim_course c ON l.course_id = c.course_id
|
||||
LEFT JOIN dsolovianov.core_fact_lesson_views v ON l.lesson_id = v.lesson_id
|
||||
GROUP BY l.lesson_id, l.title, l.course_id, c.title;
|
||||
|
||||
-- Витрину inactive_users_summary
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster (
|
||||
user_id UInt32,
|
||||
name String,
|
||||
email String,
|
||||
age UInt32,
|
||||
registered_courses_count UInt32,
|
||||
registration_date Date
|
||||
) ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/inactive_users_summary',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.inactive_users_summary_mv ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE MATERIALIZED VIEW dsolovianov.inactive_users_summary_mv ON CLUSTER clickhouse_cluster
|
||||
TO dsolovianov.inactive_users_summary
|
||||
AS
|
||||
SELECT
|
||||
u.user_id AS user_id,
|
||||
u.name AS name,
|
||||
u.email AS email,
|
||||
u.age AS age,
|
||||
COUNT(DISTINCT e.course_id) AS registered_courses_count,
|
||||
u.registration_date AS registration_date
|
||||
FROM dsolovianov.core_dim_user u
|
||||
LEFT JOIN dsolovianov.core_enrollments e ON u.user_id = e.user_id
|
||||
WHERE u.user_id NOT IN (SELECT DISTINCT user_id FROM dsolovianov.core_fact_lesson_views)
|
||||
GROUP BY u.user_id, u.name, u.email, u.age, u.registration_date;
|
||||
|
||||
INSERT INTO dsolovianov.inactive_users_summary
|
||||
SELECT
|
||||
u.user_id AS user_id,
|
||||
u.name AS name,
|
||||
u.email AS email,
|
||||
u.age AS age,
|
||||
COUNT(DISTINCT e.course_id) AS registered_courses_count,
|
||||
u.registration_date AS registration_date
|
||||
FROM dsolovianov.core_dim_user u
|
||||
LEFT JOIN dsolovianov.core_enrollments e ON u.user_id = e.user_id
|
||||
WHERE u.user_id NOT IN (SELECT DISTINCT user_id FROM dsolovianov.core_fact_lesson_views)
|
||||
GROUP BY u.user_id, u.name, u.email, u.age, u.registration_date;
|
||||
|
||||
-- Витрина course_completion_rate
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.course_completion_rate ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.course_completion_rate ON CLUSTER clickhouse_cluster (
|
||||
user_id UInt32,
|
||||
user_name String,
|
||||
course_id UInt32,
|
||||
course_title String,
|
||||
lessons_in_course UInt32,
|
||||
lessons_viewed UInt32,
|
||||
completion_rate Float32
|
||||
) ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/course_completion_rate',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY (user_id, course_id);
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.course_completion_rate_mv ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE MATERIALIZED VIEW dsolovianov.course_completion_rate_mv ON CLUSTER clickhouse_cluster
|
||||
TO dsolovianov.course_completion_rate
|
||||
AS
|
||||
SELECT
|
||||
e.user_id AS user_id,
|
||||
u.name AS user_name,
|
||||
e.course_id AS course_id,
|
||||
c.title AS course_title,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM dsolovianov.core_dim_lesson l
|
||||
WHERE l.course_id = e.course_id
|
||||
) AS lessons_in_course,
|
||||
(
|
||||
SELECT COUNT(DISTINCT v.lesson_id)
|
||||
FROM dsolovianov.core_fact_lesson_views v
|
||||
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||
) AS lessons_viewed,
|
||||
(
|
||||
SELECT COUNT(DISTINCT v.lesson_id)
|
||||
FROM dsolovianov.core_fact_lesson_views v
|
||||
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||
) / (
|
||||
SELECT COUNT(*)
|
||||
FROM dsolovianov.core_dim_lesson l
|
||||
WHERE l.course_id = e.course_id
|
||||
) AS completion_rate
|
||||
FROM dsolovianov.core_enrollments e
|
||||
JOIN dsolovianov.core_dim_user u ON e.user_id = u.user_id
|
||||
JOIN dsolovianov.core_dim_course c ON e.course_id = c.course_id
|
||||
GROUP BY e.user_id, u.name, e.course_id, c.title;
|
||||
|
||||
INSERT INTO dsolovianov.course_completion_rate
|
||||
SELECT
|
||||
e.user_id AS user_id,
|
||||
u.name AS user_name,
|
||||
e.course_id AS course_id,
|
||||
c.title AS course_title,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM dsolovianov.core_dim_lesson l
|
||||
WHERE l.course_id = e.course_id
|
||||
) AS lessons_in_course,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT COUNT(DISTINCT v.lesson_id)
|
||||
FROM dsolovianov.core_fact_lesson_views v
|
||||
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||
), 0
|
||||
) AS lessons_viewed,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT COUNT(DISTINCT v.lesson_id)
|
||||
FROM dsolovianov.core_fact_lesson_views v
|
||||
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||
), 0
|
||||
) / (
|
||||
SELECT COUNT(*)
|
||||
FROM dsolovianov.core_dim_lesson l
|
||||
WHERE l.course_id = e.course_id
|
||||
) AS completion_rate
|
||||
FROM dsolovianov.core_enrollments e
|
||||
JOIN dsolovianov.core_dim_user u ON e.user_id = u.user_id
|
||||
JOIN dsolovianov.core_dim_course c ON e.course_id = c.course_id
|
||||
GROUP BY e.user_id, u.name, e.course_id, c.title;
|
||||
|
||||
SELECT * FROM dsolovianov.course_completion_rate ORDER BY user_id;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
-- Этап 3. Анализ и запросы - Напиши SQL-запросы к витринам
|
||||
-- Напиши свои вариации для практики--
|
||||
|
||||
|
||||
-- Топ-5 самых просматриваемых уроков
|
||||
|
||||
SELECT
|
||||
lesson_title,
|
||||
course_title,
|
||||
total_views
|
||||
FROM dsolovianov.lesson_popularity_summary
|
||||
ORDER BY total_views DESC
|
||||
LIMIT 5;
|
||||
|
||||
|
||||
-- Неактивные пользователи, записавшиеся на курсы
|
||||
|
||||
SELECT
|
||||
name,
|
||||
email,
|
||||
registered_courses_count
|
||||
FROM dsolovianov.inactive_users_summary
|
||||
WHERE registered_courses_count > 0
|
||||
ORDER BY registration_date DESC;
|
||||
|
||||
-- Курсы с самым высоким процентом завершения
|
||||
|
||||
SELECT
|
||||
course_title,
|
||||
AVG(completion_rate) AS avg_completion
|
||||
FROM dsolovianov.course_completion_rate
|
||||
GROUP BY course_title
|
||||
ORDER BY avg_completion DESC
|
||||
LIMIT 5;
|
||||
|
||||
-- Мои вариации --
|
||||
|
||||
-- Среднее количество учеников на урок по каждому курсу
|
||||
|
||||
SELECT
|
||||
course_title,
|
||||
ROUND(AVG(unique_users), 2) AS avg_users_lesson,
|
||||
SUM(total_views) AS total_course_views
|
||||
FROM dsolovianov.lesson_popularity_summary
|
||||
GROUP BY course_title
|
||||
ORDER BY avg_users_lesson DESC;
|
||||
|
||||
-- Возрастное распределение неактивных пользователей
|
||||
|
||||
SELECT
|
||||
CASE
|
||||
WHEN age < 25 THEN 'до 25'
|
||||
WHEN age BETWEEN 25 AND 30 THEN '25-30'
|
||||
WHEN age BETWEEN 31 AND 35 THEN '31-35'
|
||||
ELSE '35+'
|
||||
END AS age_group,
|
||||
COUNT(*) AS inactive_users_count,
|
||||
AVG(registered_courses_count) AS avg_courses_enrolled
|
||||
FROM dsolovianov.inactive_users_summary
|
||||
GROUP BY age_group
|
||||
ORDER BY age_group;
|
||||
|
||||
-- Пользователи, которые начали, но не закончили курс (completion_rate между 0 и 1)
|
||||
|
||||
SELECT
|
||||
user_name,
|
||||
course_title,
|
||||
lessons_viewed,
|
||||
lessons_in_course,
|
||||
ROUND(completion_rate * 100, 0) AS no_completion,
|
||||
(lessons_in_course - lessons_viewed) AS remaining_lessons
|
||||
FROM dsolovianov.course_completion_rate
|
||||
WHERE completion_rate BETWEEN 0.01 AND 0.99
|
||||
ORDER BY completion_rate DESC, user_name;
|
||||
|
||||
-- Прогресс каждого пользователя по курсам (с указанием статуса)
|
||||
|
||||
SELECT
|
||||
user_name,
|
||||
course_title,
|
||||
lessons_viewed,
|
||||
lessons_in_course,
|
||||
ROUND(completion_rate * 100, 0) AS progress_percent,
|
||||
CASE
|
||||
WHEN completion_rate = 1.0 THEN 'Пройден'
|
||||
WHEN completion_rate = 0 THEN 'Не начат'
|
||||
WHEN completion_rate >= 0.5 THEN 'В процессе (>50%)'
|
||||
ELSE 'В процессе (<50%)'
|
||||
END AS status
|
||||
FROM dsolovianov.course_completion_rate
|
||||
ORDER BY user_name, completion_rate DESC;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
CREATE TABLE dsolovianov.users (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
age INT,
|
||||
email VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
is_active BOOLEAN,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE dsolovianov.products (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
category VARCHAR(50),
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
in_stock BOOLEAN
|
||||
);
|
||||
|
||||
CREATE TABLE dsolovianov.orders (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id INT,
|
||||
order_date TIMESTAMPTZ NOT NULL,
|
||||
status VARCHAR(20),
|
||||
CONSTRAINT orders_user_fk
|
||||
FOREIGN KEY (user_id) REFERENCES dsolovianov.users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE dsolovianov.order_items (
|
||||
order_id INT NOT NULL,
|
||||
product_id INT NOT NULL,
|
||||
quantity INT NOT NULL CHECK (quantity > 0),
|
||||
price_at_purchase DECIMAL(10,2) NOT NULL CHECK (price_at_purchase > 0),
|
||||
CONSTRAINT order_items_pk PRIMARY KEY (order_id, product_id),
|
||||
CONSTRAINT order_items_order_fk
|
||||
FOREIGN KEY (order_id) REFERENCES dsolovianov.orders(id),
|
||||
CONSTRAINT order_items_product_fk
|
||||
FOREIGN KEY (product_id) REFERENCES dsolovianov.products(id)
|
||||
);
|
||||
@@ -0,0 +1,59 @@
|
||||
INSERT INTO dsolovianov.users (name, age, email, country, is_active, created_at) VALUES
|
||||
('Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00+00'),
|
||||
('Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00+00'),
|
||||
('Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00+00'),
|
||||
('Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00+00'),
|
||||
('Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00+00'),
|
||||
('Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00+00'),
|
||||
('George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00+00'),
|
||||
('Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00+00'),
|
||||
('Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00+00'),
|
||||
('Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00+00');
|
||||
|
||||
INSERT INTO dsolovianov.products (name, category, price, in_stock) VALUES
|
||||
('iPhone 15', 'Электроника', 80000, true),
|
||||
('Kindle', 'Электроника', 15000, true),
|
||||
('Кроссовки Nike', 'Одежда', 9000, true),
|
||||
('Кроссовки NoName', 'Одежда', 3000, false),
|
||||
('Наушники Pro', 'Электроника', 12000, true),
|
||||
('Фитнес-браслет', 'Гаджеты', 6000, true),
|
||||
('Подарочная карта', 'Сервисы', 1000, true);
|
||||
|
||||
INSERT INTO dsolovianov.orders (user_id, order_date, status) VALUES
|
||||
(1, '2025-01-10 12:00:00+00', 'paid'),
|
||||
(3, '2025-01-12 09:30:00+00', 'cancelled'),
|
||||
(1, '2025-01-15 18:20:00+00', 'paid'),
|
||||
(2, '2025-01-20 11:05:00+00', 'created'),
|
||||
(4, '2025-02-01 17:45:00+00', 'paid'),
|
||||
(5, '2025-02-10 10:10:00+00', 'refunded'),
|
||||
(7, '2025-03-01 09:00:00+00', 'paid'),
|
||||
(9, '2025-03-05 19:30:00+00', 'paid');
|
||||
|
||||
INSERT INTO dsolovianov.order_items (order_id, product_id, quantity, price_at_purchase) VALUES
|
||||
-- Заказ #1: iPhone 15 (1 шт) + Наушники Pro (2 шт)
|
||||
(1, 1, 1, 80000.00),
|
||||
(1, 5, 2, 12000.00),
|
||||
|
||||
-- Заказ #2 (cancelled): Kindle (1 шт)
|
||||
(2, 2, 1, 15000.00),
|
||||
|
||||
-- Заказ #3: Кроссовки Nike (2 шт)
|
||||
(3, 3, 2, 9000.00),
|
||||
|
||||
-- Заказ #4 (created): Фитнес-браслет (1 шт)
|
||||
(4, 6, 1, 6000.00),
|
||||
|
||||
-- Заказ #5: iPhone 15 (1 шт) + Подарочная карта (3 шт)
|
||||
(5, 1, 1, 80000.00),
|
||||
(5, 7, 3, 1000.00),
|
||||
|
||||
-- Заказ #6 (refunded): Наушники Pro (1 шт)
|
||||
(6, 5, 1, 12000.00),
|
||||
|
||||
-- Заказ #7: Kindle (1 шт) + Фитнес-браслет (2 шт)
|
||||
(7, 2, 1, 15000.00),
|
||||
(7, 6, 2, 6000.00),
|
||||
|
||||
-- Заказ #8: iPhone 15 (1 шт) + Кроссовки Nike (1 шт)
|
||||
(8, 1, 1, 80000.00),
|
||||
(8, 3, 1, 9000.00);
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
- Задание 1 — Спроектируй «карточку пользователя»
|
||||
|
||||
Ты делаешь экран “Пользователи” в админке и решаешь, какие поля там важны.
|
||||
|
||||
Работаем с таблицей **`users`**
|
||||
|
||||
**✅ Требования:**
|
||||
|
||||
1. Напиши запрос, который вернёт в результате только столбцы: **`id`**, **`name`**, **`country`**, **`created_at`**, **`email`**
|
||||
2. Потом измени запрос так, чтобы:
|
||||
- сначала отсортировать по **`created_at`** (новые сверху)
|
||||
- потом по **`name`** по алфавиту внутри одной даты
|
||||
|
||||
- Задание 2 — Сегментация по странам (с выбором)
|
||||
|
||||
Маркетолог хочет понять, какая страна сейчас интереснее.
|
||||
|
||||
Работаем с таблицей **`users`**
|
||||
|
||||
**✅ Требования:**
|
||||
|
||||
1. Выбери страну **`US`**и напиши запрос, который:
|
||||
- вернёт **`id`**, **`name`**, **`country`**;
|
||||
- покажет только пользователей из выбранной страны;
|
||||
- отсортирует их по имени.
|
||||
2. Напиши второй запрос, который вернёт пользователей из страны **`UK`**.
|
||||
|
||||
- Задание 3 — Контакты для рассылки (думать про качество)
|
||||
|
||||
Нужно сделать список e‑mail для рассылки так, чтобы его не стыдно было отдавать маркетологу.
|
||||
|
||||
**✅ Требования:**
|
||||
|
||||
1. Напиши один запрос, который:
|
||||
- вернёт только пользователей, у которых email **есть**;
|
||||
- вернёт столбцы **`id`**, **`name`**, **`email`**;
|
||||
- отсортирует их по имени.
|
||||
2. Напиши второй запрос, который подготовит «чистый» вид email:
|
||||
- вернёт только **`email`** в нижнем регистре (как **`email_lower`**);
|
||||
- уберёт дубликаты email (нужно использовать **`DISTINCT`**).
|
||||
- Задание 4 — Новички и активность
|
||||
|
||||
«Тебе нужно посмотреть, кто пришёл недавно и кого стоит “подогреть”.»
|
||||
|
||||
**✅ Требования:**
|
||||
|
||||
1. Напиши запрос, который:
|
||||
- выберет пользователей, зарегистрированных **после** даты, которую ты сам выберешь (например, **`2024‑01‑11`**);
|
||||
- вернёт **`id`**, **`name`**, **`created_at`**;
|
||||
- отсортирует по **`created_at`** от новых к старым.
|
||||
2. Ограничь результат так, чтобы увидеть только 3 самых новых пользователя.
|
||||
3. Измени запрос так, чтобы пропустить самого нового и показать следующих двух (используй **`LIMIT`** и **`OFFSET`**).
|
||||
*/
|
||||
SELECT id,
|
||||
name,
|
||||
country,
|
||||
created_at,
|
||||
email
|
||||
FROM dsolovianov.users
|
||||
ORDER BY created_at DESC, name ASC;
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
country
|
||||
FROM dsolovianov.users
|
||||
WHERE country LIKE 'US'
|
||||
ORDER BY name;
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
country
|
||||
FROM dsolovianov.users
|
||||
WHERE country LIKE 'UK'
|
||||
ORDER BY name;
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
email,
|
||||
LOWER(email) AS email_lower
|
||||
FROM dsolovianov.users
|
||||
WHERE email IS NOT NULL
|
||||
ORDER BY name;
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
created_at
|
||||
FROM dsolovianov.users
|
||||
WHERE created_at > '2024-01-11'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 3 OFFSET 1;
|
||||
@@ -0,0 +1,124 @@
|
||||
/*INSERT INTO dsolovianov.products (name, category, price, in_stock) VALUES
|
||||
('Mi Band 7', 'smartwatch', 49.00, true),
|
||||
('Surface Pro', 'tablet', 1299.00, true),
|
||||
('iPhone 15', 'smartphone', 999.00, true),
|
||||
('Logitech MX Master', 'mouse', 120.00, true);*/
|
||||
|
||||
/*
|
||||
- Задача 1 — Товары для продажи
|
||||
|
||||
Ты настраиваешь главную витрину магазина. Нужно показывать только товары, которые сейчас реально готовы продавать.
|
||||
|
||||
**✅ Условия:**
|
||||
|
||||
- товар **в наличии**
|
||||
- категория **НЕ smartwatch**
|
||||
- цена **от 100 до 1300**
|
||||
- отдельно хотим видеть цену:
|
||||
- с наценкой 30% ( price + 30%)
|
||||
- с «виртуальным НДС» 50% (price + 50%)
|
||||
|
||||
**✅ Вывести:**
|
||||
|
||||
- `name`, `category`, `price`
|
||||
- `price_with_markup` = price + 30%
|
||||
- `price_with_vat` = price + 50%
|
||||
|
||||
- Задача 2 — Скидочная корзина: дешёвые или дорогие
|
||||
|
||||
Нужно собрать товары под акцию
|
||||
|
||||
**✅ Условия:**
|
||||
|
||||
- участвуют товары с ценой **меньше 100** или **больше 1000**
|
||||
- скидка 15% (price - 15%)
|
||||
|
||||
**✅ Вывести:**
|
||||
|
||||
- `name`, `price`
|
||||
- `price_with_discount` = price - 15%
|
||||
|
||||
- Задача 3 — Категории для рекламы
|
||||
|
||||
Маркетолог просит товары только из нужных категорий:
|
||||
|
||||
**✅ Условия:**
|
||||
|
||||
- category в списке: `tablet`, `smartphone`, `mouse`
|
||||
- цена **не меньше 100**
|
||||
|
||||
**✅ Вывести:**
|
||||
|
||||
- `id`, `name`, `category`, `price`
|
||||
- сортировка по цене по убыванию
|
||||
- Задача 4 — Убрать iPhone из выборки
|
||||
|
||||
Собираем список товаров, **кроме iPhone**, чтобы сделать подборку “не Apple”.
|
||||
|
||||
**✅ Условия:**
|
||||
|
||||
- название **НЕ содержит** `iPhone`
|
||||
|
||||
**✅ Вывести:**
|
||||
|
||||
- `name`, `category`, `price`
|
||||
- Задача 5 — Поиск товаров по названию
|
||||
|
||||
Представь, что у пользователя есть строка поиска на сайте. Нужно сделать **2 запроса**, которые помогут искать товары по названию.
|
||||
|
||||
- 1) Найти товары, где в названии есть `"pro"` (без учёта регистра)
|
||||
|
||||
📌 Условия:
|
||||
|
||||
- поиск должен сработать на `pro`, `PRO`, `Pro`
|
||||
- вывести: `name`, `category`, `price`
|
||||
- 2) Найти товары, где в названии есть `"mx"` (без учёта регистра)
|
||||
|
||||
📌 Условия:
|
||||
|
||||
- вывести: `name`, `category`, `price`
|
||||
- отсортировать по `price DESC`
|
||||
*/
|
||||
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price,
|
||||
(price * 0.30) + price AS price_with_markup,
|
||||
(price * 0.50) + price AS price_with_vat
|
||||
FROM dsolovianov.products
|
||||
WHERE category NOT IN ('smartwatch')
|
||||
AND (price >= 100 AND price <= 1300);
|
||||
|
||||
SELECT name,
|
||||
price,
|
||||
price - (price * 0.15) AS price_with_discount
|
||||
FROM dsolovianov.products
|
||||
WHERE price < 100 OR price > 1000;
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
category,
|
||||
price
|
||||
FROM dsolovianov.products
|
||||
WHERE category IN ('tablet', 'smartphone', 'mouse') AND price >= 100
|
||||
ORDER BY price DESC;
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price
|
||||
FROM dsolovianov.products
|
||||
WHERE category NOT LIKE '%iPhone%';
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price
|
||||
FROM dsolovianov.products
|
||||
WHERE name ILIKE '%pro%';
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price
|
||||
FROM dsolovianov.products
|
||||
WHERE name ILIKE '%mx%';
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
"course_id","title","category","created_at"
|
||||
1,SQL для начинающих,data,2023-01-01 00:00:00.000
|
||||
5,Основы машинного обучения,data,2023-03-15 00:00:00.000
|
||||
2,Python для анализа данных,programming,2023-01-15 00:00:00.000
|
||||
3,BI с нуля,business,2023-03-01 00:00:00.000
|
||||
4,Excel для аналитиков,business,2023-02-01 00:00:00.000
|
||||
|
+2923
File diff suppressed because it is too large
Load Diff
+19
@@ -0,0 +1,19 @@
|
||||
"lesson_id","title","duration_min","course_id"
|
||||
2,JOIN,15,1
|
||||
3,WHERE и фильтрация,12,1
|
||||
4,Агрегации и GROUP BY,18,1
|
||||
6,Pandas основы,20,2
|
||||
7,NumPy,15,2
|
||||
8,Визуализация данных,22,2
|
||||
9,Основы BI,12,3
|
||||
10,Метрики и дашборды,18,3
|
||||
13,Сводные таблицы,18,4
|
||||
16,Линейная регрессия,30,5
|
||||
18,Кластеризация,25,5
|
||||
1,SELECT и FROM,10,1
|
||||
5,Подзапросы,25,1
|
||||
11,Power BI введение,20,3
|
||||
12,Формулы в Excel,15,4
|
||||
14,Графики и диаграммы,14,4
|
||||
15,Введение в ML,20,5
|
||||
17,Классификация,28,5
|
||||
|
+21
@@ -0,0 +1,21 @@
|
||||
"user_id","name","age","email","registration_date"
|
||||
2,Bob,30,bob@gmail.com,2023-02-05 00:00:00.000
|
||||
3,Charlie,22,charlie@mail.com,2023-02-20 00:00:00.000
|
||||
4,Diana,28,diana@mail.com,2023-03-01 00:00:00.000
|
||||
6,Frank,29,frank@mail.com,2023-01-05 00:00:00.000
|
||||
7,Grace,26,grace@gmail.com,2023-01-12 00:00:00.000
|
||||
8,Henry,32,henry@mail.com,2023-01-20 00:00:00.000
|
||||
9,Iris,24,iris@gmail.com,2023-02-10 00:00:00.000
|
||||
10,Jack,27,jack@mail.com,2023-04-15 00:00:00.000
|
||||
13,Mia,26,mia@gmail.com,2023-01-18 00:00:00.000
|
||||
16,Paul,29,paul@mail.com,2023-02-15 00:00:00.000
|
||||
18,Rita,27,rita@mail.com,2023-04-01 00:00:00.000
|
||||
19,Sam,30,sam@gmail.com,2023-04-10 00:00:00.000
|
||||
1,Alice,25,alice@mail.com,2023-01-10 00:00:00.000
|
||||
5,Ethan,35,ethan@gmail.com,2023-03-10 00:00:00.000
|
||||
11,Kelly,23,kelly@gmail.com,2023-04-20 00:00:00.000
|
||||
12,Leo,31,leo@mail.com,2023-01-15 00:00:00.000
|
||||
14,Nina,28,nina@mail.com,2023-01-08 00:00:00.000
|
||||
15,Oscar,33,oscar@gmail.com,2023-01-10 00:00:00.000
|
||||
17,Quinn,24,quinn@gmail.com,2023-03-20 00:00:00.000
|
||||
20,Tina,25,tina@mail.com,2023-04-18 00:00:00.000
|
||||
|
+46
@@ -0,0 +1,46 @@
|
||||
"user_id","enrolled_at","course_id"
|
||||
2,2023-01-20 00:00:00.000,1
|
||||
3,2023-03-05 00:00:00.000,2
|
||||
6,2023-01-06 00:00:00.000,1
|
||||
6,2023-01-10 00:00:00.000,2
|
||||
6,2023-02-05 00:00:00.000,4
|
||||
7,2023-01-13 00:00:00.000,1
|
||||
7,2023-03-10 00:00:00.000,3
|
||||
8,2023-01-22 00:00:00.000,3
|
||||
13,2023-01-19 00:00:00.000,2
|
||||
13,2023-02-20 00:00:00.000,4
|
||||
16,2023-02-16 00:00:00.000,2
|
||||
16,2023-02-16 00:00:00.000,4
|
||||
19,2023-04-11 00:00:00.000,1
|
||||
19,2023-04-11 00:00:00.000,2
|
||||
2,2023-03-15 00:00:00.000,3
|
||||
4,2023-03-10 00:00:00.000,2
|
||||
7,2023-02-01 00:00:00.000,2
|
||||
9,2023-02-15 00:00:00.000,1
|
||||
18,2023-04-05 00:00:00.000,4
|
||||
4,2023-04-01 00:00:00.000,3
|
||||
8,2023-01-22 00:00:00.000,1
|
||||
8,2023-01-22 00:00:00.000,2
|
||||
9,2023-02-12 00:00:00.000,4
|
||||
10,2023-04-16 00:00:00.000,1
|
||||
18,2023-04-02 00:00:00.000,1
|
||||
3,2023-02-25 00:00:00.000,1
|
||||
6,2023-03-08 00:00:00.000,3
|
||||
1,2023-02-01 00:00:00.000,2
|
||||
11,2023-04-21 00:00:00.000,2
|
||||
14,2023-01-09 00:00:00.000,2
|
||||
14,2023-01-09 00:00:00.000,3
|
||||
14,2023-03-20 00:00:00.000,5
|
||||
17,2023-03-22 00:00:00.000,3
|
||||
20,2023-04-19 00:00:00.000,2
|
||||
12,2023-03-10 00:00:00.000,3
|
||||
1,2023-01-15 00:00:00.000,1
|
||||
12,2023-01-16 00:00:00.000,1
|
||||
14,2023-01-09 00:00:00.000,1
|
||||
14,2023-02-01 00:00:00.000,4
|
||||
15,2023-01-11 00:00:00.000,1
|
||||
15,2023-03-05 00:00:00.000,3
|
||||
17,2023-03-25 00:00:00.000,5
|
||||
5,2023-03-20 00:00:00.000,5
|
||||
1,2023-02-10 00:00:00.000,4
|
||||
15,2023-02-01 00:00:00.000,2
|
||||
|
+59
@@ -0,0 +1,59 @@
|
||||
"user_id","lesson_id","course_id","viewed_at"
|
||||
3,6,2,2023-03-06 12:00:00.000
|
||||
4,9,3,2023-04-02 10:00:00.000
|
||||
4,6,2,2023-03-12 14:00:00.000
|
||||
4,7,2,2023-03-13 09:00:00.000
|
||||
6,3,1,2023-01-08 09:00:00.000
|
||||
6,6,2,2023-01-11 14:00:00.000
|
||||
6,7,2,2023-01-12 10:00:00.000
|
||||
6,8,2,2023-01-13 11:00:00.000
|
||||
6,9,3,2023-03-09 11:00:00.000
|
||||
7,9,3,2023-03-11 14:00:00.000
|
||||
2,9,3,2023-03-16 10:00:00.000
|
||||
2,10,3,2023-03-16 10:30:00.000
|
||||
3,2,1,2023-02-26 11:20:00.000
|
||||
4,10,3,2023-04-03 10:00:00.000
|
||||
6,2,1,2023-01-07 10:15:00.000
|
||||
6,4,1,2023-01-08 09:30:00.000
|
||||
6,13,4,2023-02-06 09:30:00.000
|
||||
7,2,1,2023-01-14 10:20:00.000
|
||||
7,3,1,2023-01-15 09:00:00.000
|
||||
7,10,3,2023-03-11 14:30:00.000
|
||||
9,13,4,2023-02-13 10:30:00.000
|
||||
13,6,2,2023-01-20 14:00:00.000
|
||||
13,7,2,2023-01-21 10:00:00.000
|
||||
2,1,1,2023-01-21 09:00:00.000
|
||||
6,1,1,2023-01-07 10:00:00.000
|
||||
6,14,4,2023-02-07 10:00:00.000
|
||||
7,1,1,2023-01-14 10:00:00.000
|
||||
9,12,4,2023-02-13 10:00:00.000
|
||||
9,1,1,2023-02-16 11:00:00.000
|
||||
3,1,1,2023-02-26 11:00:00.000
|
||||
6,5,1,2023-01-09 10:00:00.000
|
||||
6,12,4,2023-02-06 09:00:00.000
|
||||
13,12,4,2023-02-21 09:00:00.000
|
||||
1,2,1,2023-01-16 10:15:00.000
|
||||
1,3,1,2023-01-17 09:00:00.000
|
||||
1,4,1,2023-01-17 09:30:00.000
|
||||
1,4,1,2023-01-18 10:00:00.000
|
||||
1,6,2,2023-02-02 14:00:00.000
|
||||
1,13,4,2023-02-15 10:30:00.000
|
||||
5,16,5,2023-03-22 11:00:00.000
|
||||
12,2,1,2023-01-17 10:20:00.000
|
||||
12,3,1,2023-04-10 11:30:00.000
|
||||
14,2,1,2023-01-09 08:15:00.000
|
||||
14,4,1,2023-01-10 10:30:00.000
|
||||
14,6,2,2023-01-10 14:00:00.000
|
||||
14,9,3,2023-01-10 08:00:00.000
|
||||
1,4,1,2023-01-17 11:00:00.000
|
||||
12,9,3,2023-03-12 10:00:00.000
|
||||
14,3,1,2023-01-10 10:00:00.000
|
||||
14,7,2,2023-01-11 10:00:00.000
|
||||
14,8,2,2023-01-12 11:00:00.000
|
||||
1,12,4,2023-02-15 10:00:00.000
|
||||
12,1,1,2023-01-17 10:00:00.000
|
||||
14,5,1,2023-01-11 09:00:00.000
|
||||
1,1,1,2023-01-16 10:00:00.000
|
||||
5,15,5,2023-03-21 10:00:00.000
|
||||
12,1,1,2023-04-10 11:00:00.000
|
||||
14,1,1,2023-01-09 08:00:00.000
|
||||
|
+97
@@ -0,0 +1,97 @@
|
||||
-- DIM
|
||||
|
||||
--DROP TABLE IF EXISTS mnoskov.core_dim_user ON CLUSTER clickhouse_cluster SYNC;
|
||||
CREATE TABLE mnoskov.core_dim_user
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
name String,
|
||||
age UInt8,
|
||||
email String,
|
||||
registration_date DateTime
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/core_dim_user',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
|
||||
--DROP TABLE IF EXISTS mnoskov.core_dim_course ON CLUSTER clickhouse_cluster SYNC;
|
||||
CREATE TABLE mnoskov.core_dim_course
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
course_id UInt64,
|
||||
title String,
|
||||
category LowCardinality(String),
|
||||
created_at DateTime
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/core_dim_course',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY course_id;
|
||||
|
||||
--DROP TABLE IF EXISTS mnoskov.core_dim_lesson ON CLUSTER clickhouse_cluster SYNC;
|
||||
CREATE TABLE mnoskov.core_dim_lesson
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
lesson_id UInt64,
|
||||
title String,
|
||||
duration_min UInt16,
|
||||
course_id UInt64
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/core_dim_lesson',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY (lesson_id, course_id);
|
||||
|
||||
--DROP TABLE IF EXISTS mnoskov.core_dim_date ON CLUSTER clickhouse_cluster SYNC;
|
||||
CREATE TABLE mnoskov.core_dim_date
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
date Date,
|
||||
year Int16,
|
||||
month Int8,
|
||||
day Int8,
|
||||
weekday LowCardinality(String)
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/core_dim_date',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY date;
|
||||
|
||||
|
||||
-- FACT
|
||||
|
||||
--DROP TABLE IF EXISTS mnoskov.core_fact_lesson_views;
|
||||
CREATE TABLE mnoskov.core_fact_lesson_views
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
lesson_id UInt64,
|
||||
course_id UInt64,
|
||||
viewed_at DateTime
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/core_fact_lesson_views',
|
||||
'{replica}'
|
||||
)
|
||||
PARTITION BY toYYYYMM(viewed_at)
|
||||
ORDER BY (viewed_at, user_id); -- я если честно не знаю как с этой таблицей поступить в ORDER BY. Наставьте меня на путь истиный.
|
||||
|
||||
--DROP TABLE IF EXISTS mnoskov.core_enrollments;
|
||||
CREATE TABLE mnoskov.core_enrollments
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
course_id UInt64,
|
||||
enrolled_at DateTime
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/core_enrollments',
|
||||
'{replica}'
|
||||
)
|
||||
PARTITION BY toYYYYMM(enrolled_at)
|
||||
ORDER BY (enrolled_at, user_id);
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
--DROP TABLE mnoskov.lesson_popularity_summary ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE TABLE mnoskov.lesson_popularity_summary
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
lesson_id UInt64,
|
||||
lesson_title String,
|
||||
course_id UInt64,
|
||||
course_title String,
|
||||
total_views AggregateFunction(count),
|
||||
unique_users AggregateFunction(uniq, UInt64),
|
||||
first_view AggregateFunction(min, DateTime),
|
||||
last_view AggregateFunction(max, DateTime)
|
||||
)
|
||||
ENGINE AggregatingMergeTree
|
||||
ORDER BY lesson_id;
|
||||
|
||||
--DROP VIEW mnoskov.mv_lesson_popularity_summary ON CLUSTER clickhouse_cluster SYNC;
|
||||
CREATE MATERIALIZED VIEW mnoskov.mv_lesson_popularity_summary
|
||||
ON CLUSTER clickhouse_cluster
|
||||
TO mnoskov.lesson_popularity_summary
|
||||
AS
|
||||
SELECT
|
||||
cdl.lesson_id AS lesson_id,
|
||||
cdl.title AS lesson_title,
|
||||
cdc.course_id AS course_id,
|
||||
cdc.title AS course_title,
|
||||
countIfState(flv.user_id != 0) AS total_views, -- я вообще ахуел когда узнал что такое существует(я про If у агрегаций). Я наткнулся на это когда осознал что кликхаус в join не ставит null, а ставит значение по умолчанию
|
||||
uniqIfState(flv.user_id, flv.user_id != 0) AS unique_users,
|
||||
minIfState(flv.viewed_at, flv.user_id != 0) AS first_view,
|
||||
maxIfState(flv.viewed_at, flv.user_id != 0) AS last_view
|
||||
FROM
|
||||
mnoskov.core_fact_lesson_views flv
|
||||
RIGHT JOIN
|
||||
mnoskov.core_dim_lesson cdl ON flv.lesson_id = cdl.lesson_id -- поидее нужно большую таблицу ставить в левую часть JOIN, но если я тут так сделаю с фактами, то запрос кореектно не будет работать
|
||||
JOIN
|
||||
mnoskov.core_dim_course cdc ON cdl.course_id = cdc.course_id
|
||||
GROUP BY
|
||||
cdl.lesson_id, cdl.title, cdc.course_id, cdc.title;
|
||||
|
||||
|
||||
SELECT * FROM mnoskov.core_fact_lesson_views;
|
||||
|
||||
SELECT
|
||||
lesson_id,
|
||||
any(lesson_title),
|
||||
any(course_id),
|
||||
any(course_title),
|
||||
countMerge(total_views) AS total_views,
|
||||
uniqMerge(unique_users) AS uniq_users,
|
||||
minMerge(first_view) AS first_view,
|
||||
maxMerge(last_view) AS last_view
|
||||
FROM
|
||||
mnoskov.lesson_popularity_summary
|
||||
GROUP BY
|
||||
lesson_id
|
||||
ORDER BY
|
||||
lesson_id;
|
||||
-- ЕБАТЬ КАК Я АХУЕЛ КОГДА ДОПЕР ДО ТОГО ЧТО ЭТО НУЖНО ЧИТАТЬ ТАКИМ СПОСОБОМ А НЕ ПРОСТО ЧЕРЕЗ ЗВЕЗДОЧКУ. У меня MV ебать сколько изменений потерпела чтобы заработать, а проблема возможно изначально была в чтении.
|
||||
-- Правда оно читается через раз, то есть результат то нет, но думаю это проблема движка AggregatingMergeTree.
|
||||
-- Это получается чтобы был удобный вывод, теперь поверх этого селекта можно сделать обычную VIEW?
|
||||
|
||||
-- Заполненния и очищения:
|
||||
TRUNCATE TABLE mnoskov.core_fact_lesson_views
|
||||
ON CLUSTER clickhouse_cluster sync;
|
||||
|
||||
TRUNCATE TABLE mnoskov.lesson_popularity_summary
|
||||
ON CLUSTER clickhouse_cluster sync;
|
||||
|
||||
INSERT INTO mnoskov.core_fact_lesson_views (user_id, lesson_id, course_id, viewed_at)
|
||||
VALUES
|
||||
(3, 6, 2, '2023-03-06 12:00:00'),
|
||||
(4, 9, 3, '2023-04-02 10:00:00'),
|
||||
(4, 6, 2, '2023-03-12 14:00:00'),
|
||||
(4, 7, 2, '2023-03-13 09:00:00'),
|
||||
(6, 3, 1, '2023-01-08 09:00:00'),
|
||||
(6, 6, 2, '2023-01-11 14:00:00'),
|
||||
(6, 7, 2, '2023-01-12 10:00:00'),
|
||||
(6, 8, 2, '2023-01-13 11:00:00'),
|
||||
(6, 9, 3, '2023-03-09 11:00:00'),
|
||||
(7, 9, 3, '2023-03-11 14:00:00'),
|
||||
(2, 9, 3, '2023-03-16 10:00:00'),
|
||||
(2, 10, 3, '2023-03-16 10:30:00'),
|
||||
(3, 2, 1, '2023-02-26 11:20:00'),
|
||||
(4, 10, 3, '2023-04-03 10:00:00'),
|
||||
(6, 2, 1, '2023-01-07 10:15:00'),
|
||||
(6, 4, 1, '2023-01-08 09:30:00'),
|
||||
(6, 13, 4, '2023-02-06 09:30:00'),
|
||||
(7, 2, 1, '2023-01-14 10:20:00'),
|
||||
(7, 3, 1, '2023-01-15 09:00:00'),
|
||||
(7, 10, 3, '2023-03-11 14:30:00'),
|
||||
(9, 13, 4, '2023-02-13 10:30:00'),
|
||||
(13, 6, 2, '2023-01-20 14:00:00'),
|
||||
(13, 7, 2, '2023-01-21 10:00:00'),
|
||||
(2, 1, 1, '2023-01-21 09:00:00'),
|
||||
(6, 1, 1, '2023-01-07 10:00:00'),
|
||||
(6, 14, 4, '2023-02-07 10:00:00'),
|
||||
(7, 1, 1, '2023-01-14 10:00:00'),
|
||||
(9, 12, 4, '2023-02-13 10:00:00'),
|
||||
(9, 1, 1, '2023-02-16 11:00:00'),
|
||||
(3, 1, 1, '2023-02-26 11:00:00'),
|
||||
(6, 5, 1, '2023-01-09 10:00:00'),
|
||||
(6, 12, 4, '2023-02-06 09:00:00'),
|
||||
(13, 12, 4, '2023-02-21 09:00:00'),
|
||||
(1, 2, 1, '2023-01-16 10:15:00'),
|
||||
(1, 3, 1, '2023-01-17 09:00:00'),
|
||||
(1, 4, 1, '2023-01-17 09:30:00'),
|
||||
(1, 4, 1, '2023-01-18 10:00:00'),
|
||||
(1, 6, 2, '2023-02-02 14:00:00'),
|
||||
(1, 13, 4, '2023-02-15 10:30:00'),
|
||||
(5, 16, 5, '2023-03-22 11:00:00'),
|
||||
(12, 2, 1, '2023-01-17 10:20:00'),
|
||||
(12, 3, 1, '2023-04-10 11:30:00'),
|
||||
(14, 2, 1, '2023-01-09 08:15:00'),
|
||||
(14, 4, 1, '2023-01-10 10:30:00'),
|
||||
(14, 6, 2, '2023-01-10 14:00:00'),
|
||||
(14, 9, 3, '2023-01-10 08:00:00'),
|
||||
(1, 4, 1, '2023-01-17 11:00:00'),
|
||||
(12, 9, 3, '2023-03-12 10:00:00'),
|
||||
(14, 3, 1, '2023-01-10 10:00:00'),
|
||||
(14, 7, 2, '2023-01-11 10:00:00'),
|
||||
(14, 8, 2, '2023-01-12 11:00:00'),
|
||||
(1, 12, 4, '2023-02-15 10:00:00'),
|
||||
(12, 1, 1, '2023-01-17 10:00:00'),
|
||||
(14, 5, 1, '2023-01-11 09:00:00'),
|
||||
(1, 1, 1, '2023-01-16 10:00:00'),
|
||||
(5, 15, 5, '2023-03-21 10:00:00'),
|
||||
(12, 1, 1, '2023-04-10 11:00:00'),
|
||||
(14, 1, 1, '2023-01-09 08:00:00');
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
--DROP TABLE mnoskov.inactive_users_summary ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE TABLE mnoskov.inactive_users_summary
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
name String,
|
||||
email String,
|
||||
age UInt8,
|
||||
registered_courses_count UInt32,
|
||||
registration_date DateTime
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/inactive_users_summary',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
|
||||
--DROP VIEW mnoskov.mv_inactive_users_summary ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE MATERIALIZED VIEW mnoskov.mv_inactive_users_summary
|
||||
ON CLUSTER clickhouse_cluster
|
||||
TO mnoskov.inactive_users_summary
|
||||
AS
|
||||
SELECT
|
||||
u.user_id AS user_id,
|
||||
u.name AS name,
|
||||
u.email AS email,
|
||||
u.age AS age,
|
||||
e.registered_courses_count AS registered_courses_count,
|
||||
u.registration_date AS registration_date
|
||||
FROM
|
||||
mnoskov.core_dim_user u
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
user_id,
|
||||
count() AS registered_courses_count
|
||||
FROM
|
||||
mnoskov.core_enrollments
|
||||
GROUP BY
|
||||
user_id
|
||||
) e ON u.user_id = e.user_id
|
||||
WHERE u.user_id NOT IN (SELECT lv.user_id FROM mnoskov.core_fact_lesson_views lv); -- я ломалася тут между NOT IN и NOT EXISTS, но решил оставить первый тк это уже витрины мы делаем, и NULL поидее быть не должно
|
||||
|
||||
|
||||
-- Чтение очищение заполнение
|
||||
SHOW TABLES FROM mnoskov;
|
||||
|
||||
SELECT * FROM mnoskov.core_dim_user;
|
||||
SELECT * FROM mnoskov.inactive_users_summary;
|
||||
|
||||
TRUNCATE TABLE mnoskov.core_dim_user ON CLUSTER clickhouse_cluster SYNC;
|
||||
TRUNCATE TABLE mnoskov.inactive_users_summary ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
INSERT INTO mnoskov.core_dim_user (user_id, name, age, email, registration_date)
|
||||
VALUES
|
||||
(1, 'Alice', 25, 'alice@mail.com', '2023-01-09 23:00:00'),
|
||||
(2, 'Bob', 30, 'bob@gmail.com', '2023-02-04 23:00:00'),
|
||||
(3, 'Charlie', 22, 'charlie@mail.com', '2023-02-19 23:00:00'),
|
||||
(4, 'Diana', 28, 'diana@mail.com', '2023-02-28 23:00:00'),
|
||||
(5, 'Ethan', 35, 'ethan@gmail.com', '2023-03-09 23:00:00'),
|
||||
(6, 'Frank', 29, 'frank@mail.com', '2023-01-04 23:00:00'),
|
||||
(7, 'Grace', 26, 'grace@gmail.com', '2023-01-11 23:00:00'),
|
||||
(8, 'Henry', 32, 'henry@mail.com', '2023-01-19 23:00:00'),
|
||||
(9, 'Iris', 24, 'iris@gmail.com', '2023-02-09 23:00:00'),
|
||||
(10, 'Jack', 27, 'jack@mail.com', '2023-04-14 23:00:00'),
|
||||
(11, 'Kelly', 23, 'kelly@gmail.com', '2023-04-19 23:00:00'),
|
||||
(12, 'Leo', 31, 'leo@mail.com', '2023-01-14 23:00:00'),
|
||||
(13, 'Mia', 26, 'mia@mail.com', '2023-01-17 23:00:00'),
|
||||
(14, 'Nina', 28, 'nina@mail.com', '2023-01-07 23:00:00'),
|
||||
(15, 'Oscar', 33, 'oscar@gmail.com', '2023-01-09 23:00:00'),
|
||||
(16, 'Paul', 29, 'paul@mail.com', '2023-02-14 23:00:00'),
|
||||
(17, 'Quinn', 24, 'quinn@gmail.com', '2023-03-19 23:00:00'),
|
||||
(18, 'Rita', 27, 'rita@mail.com', '2023-03-31 23:00:00'),
|
||||
(19, 'Sam', 30, 'sam@gmail.com', '2023-04-09 23:00:00'),
|
||||
(21, 'Tinanal', 22, 'tinanal@mail.com', '2023-04-19 23:02:00'),
|
||||
(22, 'Anal2', 24, 'tinanal222@mail.com', '2023-04-19 23:22:00');
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
--DROP TABLE mnoskov.course_completion_rate ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE TABLE mnoskov.course_completion_rate
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
user_name String,
|
||||
course_id UInt64,
|
||||
course_title String,
|
||||
lessons_in_course UInt16,
|
||||
lesson_viewed UInt16,
|
||||
completion_rate Float32
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/course_completion_rate',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY (user_id, course_id);
|
||||
|
||||
--DROP VIEW mnoskov.mv_course_completion_rate ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE MATERIALIZED VIEW mnoskov.mv_course_completion_rate
|
||||
ON CLUSTER clickhouse_cluster
|
||||
TO mnoskov.course_completion_rate
|
||||
AS
|
||||
SELECT
|
||||
u.user_id AS user_id,
|
||||
u.name AS user_name,
|
||||
cl.course_id AS course_id,
|
||||
cl.course_title AS course_title,
|
||||
cl.lessons_in_course AS lessons_in_course,
|
||||
lv.lesson_viewed AS lesson_viewed,
|
||||
ifnull(round(toFloat32(ifnull(lv.lesson_viewed, 0)) / nullif(cl.lessons_in_course, 0), 2), 0) AS completion_rate -- вроде все логично
|
||||
FROM
|
||||
mnoskov.core_enrollments e
|
||||
RIGHT JOIN
|
||||
mnoskov.core_dim_user u ON e.user_id = u.user_id
|
||||
JOIN (
|
||||
SELECT
|
||||
c.course_id,
|
||||
c.title AS course_title,
|
||||
uniq(lesson_id) lessons_in_course
|
||||
FROM
|
||||
mnoskov.core_dim_course c
|
||||
JOIN
|
||||
mnoskov.core_dim_lesson l ON l.course_id = c.course_id
|
||||
GROUP BY
|
||||
c.course_id, c.title
|
||||
) cl ON e.course_id = cl.course_id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
user_id,
|
||||
course_id,
|
||||
uniq(lesson_id) AS lesson_viewed
|
||||
FROM
|
||||
mnoskov.core_fact_lesson_views
|
||||
GROUP BY
|
||||
user_id, course_id
|
||||
) lv ON e.user_id = lv.user_id AND e.course_id = lv.course_id
|
||||
ORDER BY
|
||||
u.user_id, e.course_id;
|
||||
|
||||
-- Чтение очищение заполнение
|
||||
SHOW TABLES FROM mnoskov;
|
||||
|
||||
SELECT * FROM mnoskov.core_enrollments;
|
||||
SELECT * FROM mnoskov.course_completion_rate;
|
||||
|
||||
TRUNCATE TABLE mnoskov.core_enrollments ON CLUSTER clickhouse_cluster sync;
|
||||
TRUNCATE TABLE mnoskov.course_completion_rate ON CLUSTER clickhouse_cluster sync;
|
||||
|
||||
INSERT INTO mnoskov.core_enrollments
|
||||
(user_id, course_id, enrolled_at)
|
||||
VALUES
|
||||
(6, 1, '2023-01-05 23:00:00'),
|
||||
(14, 2, '2023-01-08 23:00:00'),
|
||||
(14, 3, '2023-01-08 23:00:00'),
|
||||
(14, 1, '2023-01-08 23:00:00'),
|
||||
(6, 2, '2023-01-09 23:00:00'),
|
||||
(15, 1, '2023-01-10 23:00:00'),
|
||||
(7, 1, '2023-01-12 23:00:00'),
|
||||
(1, 1, '2023-01-14 23:00:00'),
|
||||
(12, 1, '2023-01-15 23:00:00'),
|
||||
(13, 2, '2023-01-18 23:00:00'),
|
||||
(2, 1, '2023-01-19 23:00:00'),
|
||||
(8, 3, '2023-01-21 23:00:00'),
|
||||
(8, 1, '2023-01-21 23:00:00'),
|
||||
(8, 2, '2023-01-21 23:00:00'),
|
||||
(1, 2, '2023-01-31 23:00:00'),
|
||||
(7, 2, '2023-01-31 23:00:00'),
|
||||
(14, 4, '2023-01-31 23:00:00'),
|
||||
(15, 2, '2023-01-31 23:00:00'),
|
||||
(6, 4, '2023-02-04 23:00:00'),
|
||||
(1, 4, '2023-02-09 23:00:00'),
|
||||
(9, 4, '2023-02-11 23:00:00'),
|
||||
(9, 1, '2023-02-14 23:00:00'),
|
||||
(16, 2, '2023-02-15 23:00:00'),
|
||||
(16, 4, '2023-02-15 23:00:00'),
|
||||
(13, 4, '2023-02-19 23:00:00'),
|
||||
(3, 1, '2023-02-24 23:00:00'),
|
||||
(3, 2, '2023-03-04 23:00:00'),
|
||||
(15, 3, '2023-03-04 23:00:00'),
|
||||
(6, 3, '2023-03-07 23:00:00'),
|
||||
(4, 2, '2023-03-09 23:00:00'),
|
||||
(7, 3, '2023-03-09 23:00:00'),
|
||||
(12, 3, '2023-03-09 23:00:00'),
|
||||
(2, 3, '2023-03-14 23:00:00'),
|
||||
(5, 5, '2023-03-19 23:00:00'),
|
||||
(14, 5, '2023-03-19 23:00:00'),
|
||||
(17, 3, '2023-03-21 23:00:00'),
|
||||
(17, 5, '2023-03-24 23:00:00'),
|
||||
(4, 3, '2023-03-31 23:00:00'),
|
||||
(18, 1, '2023-04-01 23:00:00'),
|
||||
(18, 4, '2023-04-04 23:00:00'),
|
||||
(19, 1, '2023-04-10 23:00:00'),
|
||||
(19, 2, '2023-04-10 23:00:00'),
|
||||
(10, 1, '2023-04-15 23:00:00'),
|
||||
(20, 2, '2023-04-18 23:00:00'),
|
||||
(11, 2, '2023-04-20 23:00:00');
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
-- Топ 5 самых непросматреваемых уроков
|
||||
SELECT
|
||||
any(lesson_title) AS lesson_title,
|
||||
any(course_title) AS course_title,
|
||||
countMerge(total_views) AS total_views
|
||||
FROM
|
||||
mnoskov.lesson_popularity_summary
|
||||
GROUP BY
|
||||
lesson_id
|
||||
ORDER BY
|
||||
total_views
|
||||
LIMIT 5;
|
||||
|
||||
|
||||
-- Неактивные пользователи младше 26
|
||||
SELECT
|
||||
name,
|
||||
age,
|
||||
email
|
||||
FROM
|
||||
mnoskov.inactive_users_summary
|
||||
WHERE
|
||||
age < 26;
|
||||
|
||||
|
||||
-- 3 круса с самым низкми процентом завершения
|
||||
SELECT
|
||||
course_title,
|
||||
avg(completion_rate) AS avg_completion_rate
|
||||
FROM
|
||||
mnoskov.course_completion_rate
|
||||
GROUP BY
|
||||
course_title
|
||||
ORDER BY
|
||||
avg_completion_rate
|
||||
LIMIT 3;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
Разница есть только в таблицах mv_lesson_popularity_summary
|
||||
Где в Greenplum - NULL, в clickhouse - 0. Потому что clickhouser сам подставляет 0 вместо NULL.
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
-- Задание 1
|
||||
|
||||
CREATE TABLE mnoskov.page_views
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
url String,
|
||||
view_time DateTime
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
PARTITION BY toYYYYMM(view_time)
|
||||
ORDER BY (user_id, view_time);
|
||||
|
||||
|
||||
-- Задание 2
|
||||
CREATE TABLE mnoskov.users
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
name String,
|
||||
update_at DateTime
|
||||
)
|
||||
ENGINE = ReplacingMergeTree(update_at)
|
||||
ORDER BY id;
|
||||
|
||||
|
||||
-- Задание 3
|
||||
CREATE TABLE mnoskov.region_sales
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
region String,
|
||||
date Date,
|
||||
sales UInt64
|
||||
)
|
||||
ENGINE = SummingMergeTree
|
||||
PARTITION BY toYYYYMM(date)
|
||||
ORDER BY (region, date);
|
||||
|
||||
INSERT INTO mnoskov.region_sales (region, date, sales)
|
||||
VALUES
|
||||
('Moscow', '2026-05-01', 100),
|
||||
('Moscow', '2026-05-01', 150),
|
||||
('Moscow', '2026-05-01', 200),
|
||||
('Berlin', '2026-05-02', 300),
|
||||
('Berlin', '2026-05-02', 120),
|
||||
('Berlin', '2026-05-02', 80),
|
||||
('Paris', '2026-05-03', 500),
|
||||
('Paris', '2026-05-03', 250),
|
||||
('London', '2026-05-04', 400),
|
||||
('Tokyo', '2026-05-05', 700);
|
||||
|
||||
SELECT * FROM mnoskov.region_sales;
|
||||
-- Все четко работает.
|
||||
|
||||
|
||||
-- Задание 4
|
||||
CREATE TABLE mnoskov.user_metrics
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
region String,
|
||||
avg_age_state AggregateFunction(avg, UInt8),
|
||||
active_users_state AggregateFunction(uniq, UInt64)
|
||||
)
|
||||
ENGINE = AggregatingMergeTree
|
||||
ORDER BY region;
|
||||
|
||||
CREATE TABLE mnoskov.user_metrics_source
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
region String,
|
||||
age UInt8,
|
||||
user_id UInt64
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
ORDER BY region;
|
||||
|
||||
INSERT INTO mnoskov.user_metrics_source
|
||||
(region, age, user_id)
|
||||
VALUES
|
||||
('Moscow', 25, 1001),
|
||||
('Moscow', 30, 1002),
|
||||
('Moscow', 22, 1003),
|
||||
('Moscow', 25, 1001),
|
||||
('Berlin', 40, 2001),
|
||||
('Berlin', 35, 2002),
|
||||
('Berlin', 29, 2003),
|
||||
('Berlin', 35, 2002),
|
||||
('Paris', 31, 3001),
|
||||
('Paris', 28, 3002),
|
||||
('Paris', 45, 3003),
|
||||
('Paris', 31, 3001),
|
||||
('London', 50, 4001),
|
||||
('London', 41, 4002),
|
||||
('London', 38, 4003),
|
||||
('London', 41, 4002),
|
||||
('Tokyo', 27, 5001),
|
||||
('Tokyo', 33, 5002),
|
||||
('Tokyo', 29, 5003),
|
||||
('Tokyo', 33, 5002);
|
||||
SELECT * FROM mnoskov.user_metrics_source;
|
||||
|
||||
INSERT INTO mnoskov.user_metrics
|
||||
SELECT
|
||||
region,
|
||||
avgState(age) AS avg_age_state,
|
||||
uniqState(user_id) AS active_users_state
|
||||
FROM
|
||||
mnoskov.user_metrics_source
|
||||
GROUP BY
|
||||
region;
|
||||
|
||||
SELECT
|
||||
region,
|
||||
avgMerge(avg_age_state),
|
||||
uniqMerge(active_users_state)
|
||||
FROM
|
||||
mnoskov.user_metrics
|
||||
GROUP BY
|
||||
region;
|
||||
-- Че то кликхаус странно себя ведет, то таблицы сами очищаются, то удаленные таблицы воскресают.
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
-- Задание 1
|
||||
|
||||
CREATE TABLE mnoskov.logs
|
||||
ON CLUSTER clickhouse_cluster (
|
||||
dt DateTime,
|
||||
user_id UInt32,
|
||||
event_type String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/logs',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY dt;
|
||||
|
||||
CREATE TABLE mnoskov.dist_logs
|
||||
ON CLUSTER clickhouse_cluster
|
||||
ENGINE = Distributed(
|
||||
'clickhouse_cluster',
|
||||
'mnoskov',
|
||||
'logs',
|
||||
user_id
|
||||
);
|
||||
-- параметр rand() - это ключ распределния между шардами. Если стоит он, то распределяются данные случайно, если укащать колонку, то по колонке.
|
||||
|
||||
|
||||
-- Задание 2
|
||||
|
||||
CREATE TABLE mnoskov.events
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt32,
|
||||
event_type String,
|
||||
ts DateTime
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/events',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY (ts, user_id);
|
||||
|
||||
CREATE VIEW mnoskov.top_active_users AS
|
||||
SELECT
|
||||
user_id,
|
||||
count(*) AS cnt_user
|
||||
FROM
|
||||
mnoskov.events
|
||||
GROUP BY
|
||||
user_id
|
||||
ORDER BY
|
||||
cnt_user DESC, user_id
|
||||
LIMIT 100;
|
||||
-- VIEW не подойдет, тк вьюшка будет каждый раз считаться заново, а это энергозатратно, поэтому надо использовать MATERIALIZED VIEW.
|
||||
|
||||
|
||||
-- Задание 3
|
||||
|
||||
CREATE TABLE mnoskov.raw_events
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
event_time DateTime,
|
||||
user_id UInt32,
|
||||
event_type String
|
||||
)
|
||||
ENGINE=MergeTree
|
||||
ORDER BY(event_time,user_id);
|
||||
|
||||
CREATE TABLE mnoskov.agr_clicks_per_day
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
event_date Date,
|
||||
count UInt64
|
||||
)
|
||||
ENGINE=SummingMergeTree
|
||||
ORDER BY event_date;
|
||||
|
||||
CREATE MATERIALIZED VIEW mnoskov.mv_clicks_per_day
|
||||
ON CLUSTER clickhouse_cluster
|
||||
TO mnoskov.agr_clicks_per_day AS
|
||||
SELECT
|
||||
toDate(event_time) AS event_date,
|
||||
count(*) AS count
|
||||
FROM mnoskov.raw_events
|
||||
GROUP BY event_date;
|
||||
|
||||
SELECT * FROM mnoskov.raw_events;
|
||||
SELECT * FROM mnoskov.agr_clicks_per_day;
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
-- Задание 1
|
||||
|
||||
CREATE TABLE mnoskov.raw_data1
|
||||
ON CLUSTER clickhouse_cluster (
|
||||
user_id String,
|
||||
balance String,
|
||||
created_at String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/raw_data1',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY tuple();
|
||||
|
||||
SELECT
|
||||
user_id,
|
||||
balance,
|
||||
created_at
|
||||
FROM (
|
||||
SELECT
|
||||
user_id,
|
||||
toDecimal64OrNull(balance,2) AS balance,
|
||||
toDateTime64OrNull(created_at, 3) AS created_at
|
||||
FROM
|
||||
mnoskov.raw_data1
|
||||
)
|
||||
WHERE
|
||||
balance IS NOT NULL AND created_at IS NOT NULL;
|
||||
|
||||
|
||||
-- Задание 2
|
||||
|
||||
CREATE TABLE mnoskov.events
|
||||
ON CLUSTER clickhouse_cluster (
|
||||
event_id String,
|
||||
event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid"
|
||||
event_time String -- форматы: "12:30:45", "25:99:99"
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/events',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY
|
||||
event_id;
|
||||
|
||||
SELECT * FROM mnoskov.events;
|
||||
|
||||
|
||||
SELECT
|
||||
event_id,
|
||||
event_datetime
|
||||
FROM (
|
||||
SELECT
|
||||
event_id,
|
||||
parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS event_datetime
|
||||
FROM
|
||||
mnoskov.events
|
||||
)
|
||||
ORDER BY
|
||||
event_datetime;
|
||||
|
||||
|
||||
-- Задание 3
|
||||
|
||||
CREATE TABLE mnoskov.user_activity
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt32,
|
||||
actions String -- JSON: ["login", "purchase", "logout"]
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/user_activity',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY
|
||||
user_id;
|
||||
|
||||
SELECT
|
||||
user_id,
|
||||
count(*) as user_actions_cnt
|
||||
FROM
|
||||
mnoskov.user_activity
|
||||
WHERE
|
||||
has(JSONExtract(actions, 'Array(String)'), 'purchase')
|
||||
GROUP BY
|
||||
user_id
|
||||
ORDER BY
|
||||
user_id;
|
||||
|
||||
|
||||
-- Задание 4
|
||||
|
||||
CREATE TABLE mnoskov.sensor_data
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
sensor_id UInt32,
|
||||
value Float64,
|
||||
status Enum('ok' = 1, 'error' = 2)
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/sensor_data',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY sensor_id;
|
||||
|
||||
--1-2.
|
||||
SELECT
|
||||
toString(status) AS str_status,
|
||||
avg(value) AS avg_value
|
||||
FROM
|
||||
mnoskov.sensor_data
|
||||
GROUP BY
|
||||
str_status;
|
||||
--3
|
||||
-- не понял задание. все строки в value можно преобразовать в Decimal(10,2), потому что value - Float64, а он все null переделывает в 0.
|
||||
@@ -0,0 +1,215 @@
|
||||
-- Задание 1
|
||||
CREATE TABLE mnoskov.users
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt32,
|
||||
name String,
|
||||
age UInt8,
|
||||
country String,
|
||||
is_active Bool
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/users',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
|
||||
INSERT INTO mnoskov.users
|
||||
(user_id,name,age,country,is_active)
|
||||
VALUES
|
||||
(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,'Joao',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);
|
||||
|
||||
SELECT * FROM mnoskov.users;
|
||||
|
||||
--1
|
||||
SELECT
|
||||
user_id,
|
||||
age,
|
||||
is_active
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE
|
||||
is_active = TRUE
|
||||
ORDER BY
|
||||
age DESC
|
||||
LIMIT 10;
|
||||
|
||||
--2
|
||||
SELECT
|
||||
name,
|
||||
country
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE
|
||||
country = 'RU'
|
||||
ORDER BY
|
||||
name;
|
||||
|
||||
--3
|
||||
SELECT
|
||||
user_id,
|
||||
age,
|
||||
is_active
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE
|
||||
is_active = TRUE
|
||||
LIMIT 10;
|
||||
-- Данные вывелить так как лежат на диске.
|
||||
|
||||
--4
|
||||
SELECT
|
||||
country,
|
||||
count() as cnt_users
|
||||
FROM
|
||||
mnoskov.users
|
||||
GROUP BY
|
||||
country;
|
||||
|
||||
--5
|
||||
SELECT
|
||||
country,
|
||||
min(age) AS min_age,
|
||||
max(age) AS max_age
|
||||
FROM
|
||||
mnoskov.users
|
||||
GROUP BY
|
||||
country;
|
||||
|
||||
--6
|
||||
SELECT
|
||||
country,
|
||||
count() AS cnt_users
|
||||
FROM
|
||||
mnoskov.users
|
||||
GROUP BY
|
||||
country
|
||||
ORDER BY
|
||||
cnt_users DESC;
|
||||
|
||||
--Доп
|
||||
SELECT
|
||||
uniqExact(country),
|
||||
uniq(country)
|
||||
FROM
|
||||
mnoskov.users;
|
||||
-- uniqExact(country) - кол-во уникальных значений в колонке country. Но uniq работает также.
|
||||
-- В инете написано что uniq быстрее.
|
||||
|
||||
|
||||
-- Задание 2
|
||||
CREATE TABLE mnoskov.events1
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt32,
|
||||
event_time DateTime,
|
||||
event_type String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/events1',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY(user_id,event_time);
|
||||
|
||||
INSERT INTO mnoskov.events1
|
||||
(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');
|
||||
|
||||
SELECT * FROM mnoskov.events1;
|
||||
|
||||
--1
|
||||
SELECT
|
||||
user_id,
|
||||
event_time,
|
||||
event_type,
|
||||
count() OVER(PARTITION BY user_id ORDER BY event_time) AS com_users
|
||||
FROM
|
||||
mnoskov.events1;
|
||||
|
||||
--2
|
||||
SELECT
|
||||
user_id,
|
||||
event_time,
|
||||
event_type,
|
||||
row_number() OVER (PARTITION BY user_id ORDER BY event_time) rn,
|
||||
dense_rank() OVER (PARTITION BY user_id ORDER BY event_time) dr,
|
||||
count() OVER(PARTITION BY user_id ORDER BY event_time) AS c,
|
||||
sum(1) OVER(PARTITION BY user_id ORDER BY event_time) AS s
|
||||
FROM
|
||||
mnoskov.events1;
|
||||
|
||||
--3
|
||||
SELECT
|
||||
user_id,
|
||||
event_time,
|
||||
event_type,
|
||||
count() OVER(PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS com_users,
|
||||
count() OVER(PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS com_users2
|
||||
FROM
|
||||
mnoskov.events1;
|
||||
-- все четенько работает, никаких ошибкок нет
|
||||
|
||||
--4-5-6
|
||||
WITH recent_events AS (
|
||||
SELECT
|
||||
user_id,
|
||||
event_time,
|
||||
event_type
|
||||
FROM
|
||||
mnoskov.events1
|
||||
WHERE
|
||||
event_time < now() - INTERVAL 1 DAY
|
||||
)
|
||||
SELECT
|
||||
user_id,
|
||||
count(*) AS cnt_user_actions
|
||||
FROM
|
||||
recent_events
|
||||
GROUP BY
|
||||
user_id
|
||||
ORDER BY
|
||||
cnt_user_actions DESC
|
||||
LIMIT
|
||||
5;
|
||||
-- не понял, как сделать 6 пункт.
|
||||
|
||||
-- Ответ на вопрос - ну да - поддерживает.
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Задание 1
|
||||
|
||||
SELECT
|
||||
pro.product_id,
|
||||
pro.name,
|
||||
pri.price
|
||||
FROM
|
||||
mnoskov.products pro
|
||||
ANY LEFT JOIN
|
||||
mnoskov.prices pri ON pro.product_id = pri.product_id;
|
||||
-- 2 Выберется случайная из цен.
|
||||
-- 3 ALL LEFT JOIN - работает также как и LEFT JOIN. Его избегают потому что он тянет больше строк в оперативную память.
|
||||
|
||||
|
||||
-- Задание 2
|
||||
|
||||
SELECT
|
||||
o.time,
|
||||
o.symbol,
|
||||
o.size,
|
||||
t.price
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
ASOF JOIN
|
||||
mnoskov.ticks t ON o.symbol = t.symbol AND t.time <= o.time
|
||||
WHERE symbol = 'AAPL';
|
||||
-- Потому что дата сделки и дата последней фиксации цены - разные.
|
||||
|
||||
|
||||
-- Задание 3
|
||||
|
||||
SELECT
|
||||
user_id,
|
||||
name
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE user_id IN (
|
||||
SELECT user_id
|
||||
FROM mnoskov.payments
|
||||
WHERE amount > 1000
|
||||
);
|
||||
-- 3 Потому что это фильтрация по существованию. Как оптимизируется не знаю, наверное как подзапрос.
|
||||
|
||||
|
||||
-- Задание 4
|
||||
|
||||
SELECT
|
||||
bl.user_id,
|
||||
u.name AS user_name,
|
||||
bl.event_time,
|
||||
bl.event_type
|
||||
FROM
|
||||
mnoskov.big_logs bl
|
||||
JOIN
|
||||
mnoskov.users u ON bl.user_id = u.user_id;
|
||||
-- 2 потому что таблица users меньше, а значит в оператику полетит меньше данных.
|
||||
-- 3 если user_id уникален, то нужно выбирать ALL, так как дублей нет. В текущем задании дубли есть, но мы все равно выбираем ALL, тк нужно вывести имя для каждого лога а не юзера.
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
-- Задание 1
|
||||
|
||||
--1-2
|
||||
SELECT
|
||||
user_id,
|
||||
tags,
|
||||
indexOf(tags, 'promo') AS pos_promo
|
||||
FROM
|
||||
mnoskov.events
|
||||
WHERE
|
||||
has(tags, 'promo');
|
||||
|
||||
--3
|
||||
SELECT
|
||||
user_id,
|
||||
arrayJoin(tags) AS tag
|
||||
FROM
|
||||
mnoskov.events;
|
||||
|
||||
|
||||
-- Задание 2
|
||||
|
||||
--1
|
||||
SELECT user_id, scores['math'] FROM mnoskov.metrics;
|
||||
|
||||
--2
|
||||
SELECT user_id, mapValues(scores) FROM mnoskov.metrics;
|
||||
|
||||
|
||||
-- Заданик 3
|
||||
|
||||
--1
|
||||
SELECT * FROM mnoskov.users_nested WHERE has(contacts.type, 'email');
|
||||
|
||||
--2
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
c.type,
|
||||
c.value
|
||||
FROM
|
||||
mnoskov.users_nested
|
||||
ARRAY JOIN
|
||||
contacts AS c;
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
-- Задание 1
|
||||
|
||||
CREATE TABLE mnoskov.sales
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
region String,
|
||||
sale_date Date,
|
||||
amount Float64
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/sales',
|
||||
'{replica}'
|
||||
)
|
||||
PARTITION BY toYYYYMM(sale_date)
|
||||
ORDER BY sale_date;
|
||||
|
||||
ALTER TABLE mnoskov.sales DROP PARTITION '202405';
|
||||
SELECT * FROM mnoskov.sales;
|
||||
-- Пропали строки где год/месяц 202405.
|
||||
-- Если не указывать PARTITION BY, то строки бы искались по всей таблице, что дольше и затранее.
|
||||
|
||||
|
||||
-- Задание 2
|
||||
|
||||
CREATE TABLE mnoskov.events
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
event_type String,
|
||||
event_date Date
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/events_partitioned',
|
||||
'{replica}'
|
||||
)
|
||||
PARTITION BY event_type
|
||||
ORDER BY event_date;
|
||||
|
||||
ALTER TABLE mnoskov.events DROP PARTITION 'click';
|
||||
ELECT * FROM mnoskov.events;
|
||||
-- Такой подход опасен, тк например login выполняется не часто по сравнению с click и получается жесткий перевес.
|
||||
|
||||
|
||||
-- Задание 3
|
||||
|
||||
CREATE TABLE mnoskov.logs
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
user_id UInt64,
|
||||
log_time DateTime
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/logs_partitioned',
|
||||
'{replica}'
|
||||
)
|
||||
PARTITION BY cityHash64(user_id) % 100
|
||||
ORDER BY log_time;
|
||||
|
||||
SELECT * FROM mnoskov.logs
|
||||
|
||||
-- 2 с точки зрения баланса, у нас юзеры распределные равномерно и нет никаких перекосов.
|
||||
-- 3 хз если честно, мне кажется должны ускорять.
|
||||
|
||||
|
||||
-- Задание 4
|
||||
|
||||
CREATE TABLE mnoskov.visits
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
region String,
|
||||
visit_date Date
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/visits',
|
||||
'{replica}'
|
||||
)
|
||||
PARTITION BY (region, toYYYYMM(visit_date))
|
||||
ORDER BY visit_date;
|
||||
|
||||
SELECT * FROM mnoskov.visits;
|
||||
-- 2 доступа к системным таблицам нет, потэтому я не могу посмотреть имя партиции.
|
||||
-- 3 ну да
|
||||
ALTER TABLE mnoskov.visits DROP PARTITION ('RU', '202407');
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
-- Задание 1
|
||||
|
||||
CREATE TABLE mnoskov.logs
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
log_id UInt64,
|
||||
ts DateTime
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/logs_ttl',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY ts;
|
||||
ALTER TABLE mnoskov.logs ADD INDEX idx_ts ts TYPE minmax granularity 2;
|
||||
|
||||
SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03';
|
||||
|
||||
-- 2 У меня нет доступа к системным таблицам, я не могу посмотреть.
|
||||
-- Если в order by будет log_id а не ts, то индекс не будет работать.
|
||||
|
||||
|
||||
-- Задание 2
|
||||
|
||||
CREATE TABLE mnoskov.clicks
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
country String
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/clicks',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY id;
|
||||
|
||||
ALTER TABLE mnoskov.clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2;
|
||||
ALTER TABLE mnoskov.clicks DROP INDEX idx_country;
|
||||
|
||||
explain
|
||||
SELECT * FROM clicks WHERE country = 'RU';
|
||||
-- 3 не использутся индеск, я туда 11тыс строк заинсертил, все равно не используется.
|
||||
-- 4 я думаю индекс не будет работать эффективно, а может быть и вообще будет запрос замедлять.
|
||||
|
||||
|
||||
-- Задание 3
|
||||
|
||||
CREATE TABLE mnoskov.users
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
email String
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/users_simple',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY id;
|
||||
|
||||
ALTER TABLE mnoskov.users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2;
|
||||
|
||||
explain
|
||||
SELECT * FROM users WHERE email = 'alice@mail.com';
|
||||
-- 2 Ну я не вижу в EXPLAIN какое либо упоминание индекса, но я ссылаюсь на то, что просто мало данных в таблице.
|
||||
|
||||
-- 3 Потому что этот индекс работает только на точные совпадения - exact match.
|
||||
|
||||
explain
|
||||
SELECT * FROM users WHERE email LIKE 'alice%';
|
||||
-- 4 Индекс не будет использоваться.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
-- Задание 1
|
||||
|
||||
CREATE TABLE mnoskov.logs
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
level String,
|
||||
message String CODEC(ZSTD(3)),
|
||||
created_at DateTime CODEC(Delta, ZSTD)
|
||||
)
|
||||
ENGINE=ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/logs',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY created_at;
|
||||
|
||||
SELECT
|
||||
column,
|
||||
compression_codec
|
||||
FROM system.columns
|
||||
WHERE table = 'logs';
|
||||
-- если делать с codec_expression, то ошибка вылазит мол такой колонки нет
|
||||
-- Выглядит вот так:
|
||||
-- id
|
||||
-- level
|
||||
-- message CODEC(ZSTD(3))
|
||||
-- created_at CODEC(Delta(4), ZSTD(1))
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
-- Задание 1
|
||||
|
||||
--DROP TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE TABLE mnoskov.raw_events
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
ts DateTime,
|
||||
event_type String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/raw_events',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY ts;
|
||||
|
||||
--DROP TABLE mnoskov.clean_events ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE TABLE mnoskov.clean_events
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
ts DateTime,
|
||||
event_type String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/clean_events',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY ts;
|
||||
|
||||
CREATE MATERIALIZED VIEW mnoskov.mv_clean
|
||||
ON CLUSTER clickhouse_cluster
|
||||
TO mnoskov.clean_events
|
||||
AS
|
||||
SELECT
|
||||
user_id,
|
||||
ts,
|
||||
nullif(event_type, '') AS event_type -- это я сделать чтобы выводилось NULL а не '' - пустая строка.
|
||||
FROM mnoskov.raw_events
|
||||
WHERE user_id != 0 AND event_type IS NOT NULL
|
||||
ORDER BY ts;
|
||||
|
||||
-- Тесты:
|
||||
SELECT * FROM raw_events;
|
||||
SELECT * FROM mnoskov.mv_clean;
|
||||
|
||||
TRUNCATE TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync;
|
||||
INSERT INTO mnoskov.raw_events
|
||||
(user_id, ts, event_type)
|
||||
VALUES
|
||||
(0, '2024-07-17 10:00:00', 'click'),
|
||||
(1, '2024-07-17 10:01:00', 'view'),
|
||||
(2, '2024-07-17 10:02:00', NULL);
|
||||
|
||||
|
||||
-- Задание 2
|
||||
|
||||
--raw_events
|
||||
--DROP TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE TABLE mnoskov.raw_events
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
ts DateTime,
|
||||
event_type String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/raw_events',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY ts;
|
||||
|
||||
--click_events
|
||||
--DROP TABLE mnoskov.click_events ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE TABLE mnoskov.click_events
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
ts DateTime,
|
||||
event_type String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/click_events',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY ts;
|
||||
|
||||
--login_events
|
||||
--DROP TABLE mnoskov.login_events ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE TABLE mnoskov.login_events1
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
ts DateTime,
|
||||
event_type String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/login_events1',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY ts;
|
||||
|
||||
--purchase_events
|
||||
--DROP TABLE mnoskov.purchase_events ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE TABLE mnoskov.purchase_events1
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
ts DateTime,
|
||||
event_type String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/purchase_events1',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY ts;
|
||||
|
||||
--mv_click
|
||||
--DROP VIEW mnoskov.mv_click ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE MATERIALIZED VIEW mnoskov.mv_click
|
||||
ON CLUSTER clickhouse_cluster
|
||||
TO mnoskov.click_events
|
||||
AS
|
||||
SELECT * FROM mnoskov.raw_events WHERE event_type = 'click'
|
||||
ORDER BY ts;
|
||||
|
||||
--mv_login
|
||||
--DROP VIEW mnoskov.mv_login ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE MATERIALIZED VIEW mnoskov.mv_login
|
||||
ON CLUSTER clickhouse_cluster
|
||||
TO mnoskov.login_events1
|
||||
AS
|
||||
SELECT * FROM mnoskov.raw_events WHERE event_type = 'login'
|
||||
ORDER BY ts;
|
||||
|
||||
--mv_purchase
|
||||
--DROP VIEW mnoskov.mv_purchase ON CLUSTER clickhouse_cluster sync;
|
||||
CREATE MATERIALIZED VIEW mnoskov.mv_purchase
|
||||
ON CLUSTER clickhouse_cluster
|
||||
TO mnoskov.purchase_events1
|
||||
AS
|
||||
SELECT * FROM mnoskov.raw_events WHERE event_type = 'purchase'
|
||||
ORDER BY ts;
|
||||
|
||||
|
||||
-- Тесты:
|
||||
SELECT * FROM mnoskov.click_events;
|
||||
SELECT * FROM mnoskov.login_events1;
|
||||
SELECT * FROM mnoskov.purchase_events1;
|
||||
|
||||
TRUNCATE TABLE mnoskov.raw_events ON CLUSTER clickhouse_cluster sync;
|
||||
INSERT INTO mnoskov.raw_events
|
||||
(user_id, ts, event_type)
|
||||
VALUES
|
||||
(1, '2024-07-17 10:00:00', 'click'),
|
||||
(2, '2024-07-17 10:01:00', 'login'),
|
||||
(3, '2024-07-17 10:02:00', 'purchase');
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
-- Задание 1
|
||||
|
||||
CREATE TABLE mnoskov.page_views
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
url String,
|
||||
view_time DateTime
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
PARTITION BY toYYYYMM(view_time)
|
||||
ORDER BY (user_id, view_time);
|
||||
|
||||
|
||||
-- Задание 2
|
||||
CREATE TABLE mnoskov.users
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
name String,
|
||||
update_at DateTime
|
||||
)
|
||||
ENGINE = ReplacingMergeTree(update_at)
|
||||
ORDER BY id;
|
||||
|
||||
|
||||
-- Задание 3
|
||||
CREATE TABLE mnoskov.region_sales
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
region String,
|
||||
date Date,
|
||||
sales UInt64
|
||||
)
|
||||
ENGINE = SummingMergeTree
|
||||
PARTITION BY toYYYYMM(date)
|
||||
ORDER BY (region, date);
|
||||
|
||||
INSERT INTO mnoskov.region_sales (region, date, sales)
|
||||
VALUES
|
||||
('Moscow', '2026-05-01', 100),
|
||||
('Moscow', '2026-05-01', 150),
|
||||
('Moscow', '2026-05-01', 200),
|
||||
('Berlin', '2026-05-02', 300),
|
||||
('Berlin', '2026-05-02', 120),
|
||||
('Berlin', '2026-05-02', 80),
|
||||
('Paris', '2026-05-03', 500),
|
||||
('Paris', '2026-05-03', 250),
|
||||
('London', '2026-05-04', 400),
|
||||
('Tokyo', '2026-05-05', 700);
|
||||
|
||||
SELECT * FROM mnoskov.region_sales;
|
||||
-- Все четко работает.
|
||||
|
||||
|
||||
-- Задание 4
|
||||
CREATE TABLE mnoskov.user_metrics
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
region String,
|
||||
avg_age_state AggregateFunction(avg, UInt8),
|
||||
active_users_state AggregateFunction(uniq, UInt64)
|
||||
)
|
||||
ENGINE = AggregatingMergeTree
|
||||
ORDER BY region;
|
||||
|
||||
CREATE TABLE mnoskov.user_metrics_source
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
region String,
|
||||
age UInt8,
|
||||
user_id UInt64
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
ORDER BY region;
|
||||
|
||||
INSERT INTO mnoskov.user_metrics_source
|
||||
(region, age, user_id)
|
||||
VALUES
|
||||
('Moscow', 25, 1001),
|
||||
('Moscow', 30, 1002),
|
||||
('Moscow', 22, 1003),
|
||||
('Moscow', 25, 1001),
|
||||
('Berlin', 40, 2001),
|
||||
('Berlin', 35, 2002),
|
||||
('Berlin', 29, 2003),
|
||||
('Berlin', 35, 2002),
|
||||
('Paris', 31, 3001),
|
||||
('Paris', 28, 3002),
|
||||
('Paris', 45, 3003),
|
||||
('Paris', 31, 3001),
|
||||
('London', 50, 4001),
|
||||
('London', 41, 4002),
|
||||
('London', 38, 4003),
|
||||
('London', 41, 4002),
|
||||
('Tokyo', 27, 5001),
|
||||
('Tokyo', 33, 5002),
|
||||
('Tokyo', 29, 5003),
|
||||
('Tokyo', 33, 5002);
|
||||
SELECT * FROM mnoskov.user_metrics_source;
|
||||
|
||||
INSERT INTO mnoskov.user_metrics
|
||||
SELECT
|
||||
region,
|
||||
avgState(age) AS avg_age_state,
|
||||
uniqState(user_id) AS active_users_state
|
||||
FROM
|
||||
mnoskov.user_metrics_source
|
||||
GROUP BY
|
||||
region;
|
||||
|
||||
SELECT
|
||||
region,
|
||||
avgMerge(avg_age_state),
|
||||
uniqMerge(active_users_state)
|
||||
FROM
|
||||
mnoskov.user_metrics
|
||||
GROUP BY
|
||||
region;
|
||||
-- Че то кликхаус странно себя ведет, то таблицы сами очищаются, то удаленные таблицы воскресают.
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
-- Задание 1
|
||||
|
||||
CREATE TABLE mnoskov.logs
|
||||
ON CLUSTER clickhouse_cluster (
|
||||
dt DateTime,
|
||||
user_id UInt32,
|
||||
event_type String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/logs',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY dt;
|
||||
|
||||
CREATE TABLE mnoskov.dist_logs
|
||||
ON CLUSTER clickhouse_cluster
|
||||
ENGINE = Distributed(
|
||||
'clickhouse_cluster',
|
||||
'mnoskov',
|
||||
'logs',
|
||||
user_id
|
||||
);
|
||||
-- параметр rand() - это ключ распределния между шардами. Если стоит он, то распределяются данные случайно, если укащать колонку, то по колонке.
|
||||
|
||||
|
||||
-- Задание 2
|
||||
|
||||
CREATE TABLE mnoskov.events
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt32,
|
||||
event_type String,
|
||||
ts DateTime
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/events',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY (ts, user_id);
|
||||
|
||||
CREATE VIEW mnoskov.top_active_users AS
|
||||
SELECT
|
||||
user_id,
|
||||
count(*) AS cnt_user
|
||||
FROM
|
||||
mnoskov.events
|
||||
GROUP BY
|
||||
user_id
|
||||
ORDER BY
|
||||
cnt_user DESC, user_id
|
||||
LIMIT 100;
|
||||
-- VIEW не подойдет, тк вьюшка будет каждый раз считаться заново, а это энергозатратно, поэтому надо использовать MATERIALIZED VIEW.
|
||||
|
||||
|
||||
-- Задание 3
|
||||
|
||||
CREATE TABLE mnoskov.raw_events
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
event_time DateTime,
|
||||
user_id UInt32,
|
||||
event_type String
|
||||
)
|
||||
ENGINE=MergeTree
|
||||
ORDER BY(event_time,user_id);
|
||||
|
||||
CREATE TABLE mnoskov.agr_clicks_per_day
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
event_date Date,
|
||||
count UInt64
|
||||
)
|
||||
ENGINE=SummingMergeTree
|
||||
ORDER BY event_date;
|
||||
|
||||
CREATE MATERIALIZED VIEW mnoskov.mv_clicks_per_day
|
||||
ON CLUSTER clickhouse_cluster
|
||||
TO mnoskov.agr_clicks_per_day AS
|
||||
SELECT
|
||||
toDate(event_time) AS event_date,
|
||||
count(*) AS count
|
||||
FROM mnoskov.raw_events
|
||||
GROUP BY event_date;
|
||||
|
||||
SELECT * FROM mnoskov.raw_events;
|
||||
SELECT * FROM mnoskov.agr_clicks_per_day;
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
-- Задание 1
|
||||
|
||||
CREATE TABLE mnoskov.raw_data1
|
||||
ON CLUSTER clickhouse_cluster (
|
||||
user_id String,
|
||||
balance String,
|
||||
created_at String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/raw_data1',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY tuple();
|
||||
|
||||
SELECT
|
||||
user_id,
|
||||
balance,
|
||||
created_at
|
||||
FROM (
|
||||
SELECT
|
||||
user_id,
|
||||
toDecimal64OrNull(balance,2) AS balance,
|
||||
toDateTime64OrNull(created_at, 3) AS created_at
|
||||
FROM
|
||||
mnoskov.raw_data1
|
||||
)
|
||||
WHERE
|
||||
balance IS NOT NULL AND created_at IS NOT NULL;
|
||||
|
||||
|
||||
-- Задание 2
|
||||
|
||||
CREATE TABLE mnoskov.events
|
||||
ON CLUSTER clickhouse_cluster (
|
||||
event_id String,
|
||||
event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid"
|
||||
event_time String -- форматы: "12:30:45", "25:99:99"
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/events',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY
|
||||
event_id;
|
||||
|
||||
SELECT * FROM mnoskov.events;
|
||||
|
||||
|
||||
SELECT
|
||||
event_id,
|
||||
event_datetime
|
||||
FROM (
|
||||
SELECT
|
||||
event_id,
|
||||
parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS event_datetime
|
||||
FROM
|
||||
mnoskov.events
|
||||
)
|
||||
ORDER BY
|
||||
event_datetime;
|
||||
|
||||
|
||||
-- Задание 3
|
||||
|
||||
CREATE TABLE mnoskov.user_activity
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt32,
|
||||
actions String -- JSON: ["login", "purchase", "logout"]
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/user_activity',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY
|
||||
user_id;
|
||||
|
||||
SELECT
|
||||
user_id,
|
||||
count(*) as user_actions_cnt
|
||||
FROM
|
||||
mnoskov.user_activity
|
||||
WHERE
|
||||
has(JSONExtract(actions, 'Array(String)'), 'purchase')
|
||||
GROUP BY
|
||||
user_id
|
||||
ORDER BY
|
||||
user_id;
|
||||
|
||||
|
||||
-- Задание 4
|
||||
|
||||
CREATE TABLE mnoskov.sensor_data
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
sensor_id UInt32,
|
||||
value Float64,
|
||||
status Enum('ok' = 1, 'error' = 2)
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/mnoskov/sensor_data',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY sensor_id;
|
||||
|
||||
--1-2.
|
||||
SELECT
|
||||
toString(status) AS str_status,
|
||||
avg(value) AS avg_value
|
||||
FROM
|
||||
mnoskov.sensor_data
|
||||
GROUP BY
|
||||
str_status;
|
||||
--3
|
||||
-- не понял задание. все строки в value можно преобразовать в Decimal(10,2), потому что value - Float64, а он все null переделывает в 0.
|
||||
@@ -0,0 +1,46 @@
|
||||
-- 1
|
||||
CREATE SCHEMA raw;
|
||||
CREATE SCHEMA core;
|
||||
CREATE SCHEMA dm;
|
||||
|
||||
-- 2
|
||||
--DROP TABLE IF EXISTS raw.users;
|
||||
CREATE TABLE raw.users (
|
||||
id bigint NOT NULL,
|
||||
name varchar(100),
|
||||
age SMALLINT,
|
||||
email varchar(100),
|
||||
registration_date timestamptz
|
||||
);
|
||||
|
||||
--DROP TABLE IF EXISTS raw.courses;
|
||||
CREATE TABLE raw.courses (
|
||||
id bigint NOT NULL,
|
||||
title varchar(150),
|
||||
category varchar(50),
|
||||
created_at timestamptz
|
||||
);
|
||||
|
||||
--DROP TABLE IF EXISTS raw.lessons;
|
||||
CREATE TABLE raw.lessons (
|
||||
id bigint NOT NULL,
|
||||
course_id bigint,
|
||||
title varchar(150),
|
||||
duration_min SMALLINT
|
||||
);
|
||||
|
||||
--DROP TABLE IF EXISTS raw.enrollments;
|
||||
CREATE TABLE raw.enrollments (
|
||||
id bigint NOT NULL,
|
||||
user_id bigint,
|
||||
course_id bigint,
|
||||
enrolled_at timestamptz
|
||||
);
|
||||
|
||||
--DROP TABLE IF EXISTS raw.lesson_views;
|
||||
CREATE TABLE raw.lesson_views (
|
||||
id bigint NOT NULL,
|
||||
user_id bigint,
|
||||
lesson_id bigint,
|
||||
viewed_at timestamptz
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
"id","title","category","created_at"
|
||||
1,SQL для начинающих,data,2023-01-01 00:00:00.000 +0300
|
||||
2,Python для анализа данных,programming,2023-01-15 00:00:00.000 +0300
|
||||
3,BI с нуля,business,2023-03-01 00:00:00.000 +0300
|
||||
4,Excel для аналитиков,business,2023-02-01 00:00:00.000 +0300
|
||||
5,Основы машинного обучения,data,2023-03-15 00:00:00.000 +0300
|
||||
|
@@ -0,0 +1,46 @@
|
||||
"id","user_id","course_id","enrolled_at"
|
||||
1,1,1,2023-01-15 00:00:00.000 +0300
|
||||
2,1,2,2023-02-01 00:00:00.000 +0300
|
||||
3,2,1,2023-01-20 00:00:00.000 +0300
|
||||
4,3,2,2023-03-05 00:00:00.000 +0300
|
||||
5,4,3,2023-04-01 00:00:00.000 +0300
|
||||
6,6,1,2023-01-06 00:00:00.000 +0300
|
||||
7,6,2,2023-01-10 00:00:00.000 +0300
|
||||
8,6,4,2023-02-05 00:00:00.000 +0300
|
||||
9,7,1,2023-01-13 00:00:00.000 +0300
|
||||
10,7,3,2023-03-10 00:00:00.000 +0300
|
||||
11,8,1,2023-01-22 00:00:00.000 +0300
|
||||
12,8,2,2023-01-22 00:00:00.000 +0300
|
||||
13,8,3,2023-01-22 00:00:00.000 +0300
|
||||
14,9,4,2023-02-12 00:00:00.000 +0300
|
||||
15,10,1,2023-04-16 00:00:00.000 +0300
|
||||
16,11,2,2023-04-21 00:00:00.000 +0300
|
||||
17,12,1,2023-01-16 00:00:00.000 +0300
|
||||
18,13,2,2023-01-19 00:00:00.000 +0300
|
||||
19,13,4,2023-02-20 00:00:00.000 +0300
|
||||
20,14,1,2023-01-09 00:00:00.000 +0300
|
||||
21,14,2,2023-01-09 00:00:00.000 +0300
|
||||
22,14,3,2023-01-09 00:00:00.000 +0300
|
||||
23,14,4,2023-02-01 00:00:00.000 +0300
|
||||
24,14,5,2023-03-20 00:00:00.000 +0300
|
||||
25,15,1,2023-01-11 00:00:00.000 +0300
|
||||
26,15,3,2023-03-05 00:00:00.000 +0300
|
||||
27,16,2,2023-02-16 00:00:00.000 +0300
|
||||
28,16,4,2023-02-16 00:00:00.000 +0300
|
||||
29,17,3,2023-03-22 00:00:00.000 +0300
|
||||
30,17,5,2023-03-25 00:00:00.000 +0300
|
||||
31,18,1,2023-04-02 00:00:00.000 +0300
|
||||
32,19,1,2023-04-11 00:00:00.000 +0300
|
||||
33,19,2,2023-04-11 00:00:00.000 +0300
|
||||
34,20,2,2023-04-19 00:00:00.000 +0300
|
||||
35,5,5,2023-03-20 00:00:00.000 +0300
|
||||
36,1,4,2023-02-10 00:00:00.000 +0300
|
||||
37,2,3,2023-03-15 00:00:00.000 +0300
|
||||
38,3,1,2023-02-25 00:00:00.000 +0300
|
||||
39,4,2,2023-03-10 00:00:00.000 +0300
|
||||
40,6,3,2023-03-08 00:00:00.000 +0300
|
||||
41,7,2,2023-02-01 00:00:00.000 +0300
|
||||
42,9,1,2023-02-15 00:00:00.000 +0300
|
||||
43,12,3,2023-03-10 00:00:00.000 +0300
|
||||
44,15,2,2023-02-01 00:00:00.000 +0300
|
||||
45,18,4,2023-04-05 00:00:00.000 +0300
|
||||
|
@@ -0,0 +1,59 @@
|
||||
"id","user_id","lesson_id","viewed_at"
|
||||
1,1,1,2023-01-16 10:00:00.000 +0300
|
||||
2,1,2,2023-01-16 10:15:00.000 +0300
|
||||
3,1,3,2023-01-17 09:00:00.000 +0300
|
||||
4,1,4,2023-01-17 09:30:00.000 +0300
|
||||
5,1,4,2023-01-17 11:00:00.000 +0300
|
||||
6,1,4,2023-01-18 10:00:00.000 +0300
|
||||
7,1,6,2023-02-02 14:00:00.000 +0300
|
||||
8,1,12,2023-02-15 10:00:00.000 +0300
|
||||
9,1,13,2023-02-15 10:30:00.000 +0300
|
||||
10,2,1,2023-01-21 09:00:00.000 +0300
|
||||
11,2,9,2023-03-16 10:00:00.000 +0300
|
||||
12,2,10,2023-03-16 10:30:00.000 +0300
|
||||
13,3,6,2023-03-06 12:00:00.000 +0300
|
||||
14,3,1,2023-02-26 11:00:00.000 +0300
|
||||
15,3,2,2023-02-26 11:20:00.000 +0300
|
||||
16,4,9,2023-04-02 10:00:00.000 +0300
|
||||
17,4,10,2023-04-03 10:00:00.000 +0300
|
||||
18,4,6,2023-03-12 14:00:00.000 +0300
|
||||
19,4,7,2023-03-13 09:00:00.000 +0300
|
||||
20,5,15,2023-03-21 10:00:00.000 +0300
|
||||
21,5,16,2023-03-22 11:00:00.000 +0300
|
||||
22,6,1,2023-01-07 10:00:00.000 +0300
|
||||
23,6,2,2023-01-07 10:15:00.000 +0300
|
||||
24,6,3,2023-01-08 09:00:00.000 +0300
|
||||
25,6,4,2023-01-08 09:30:00.000 +0300
|
||||
26,6,5,2023-01-09 10:00:00.000 +0300
|
||||
27,6,6,2023-01-11 14:00:00.000 +0300
|
||||
28,6,7,2023-01-12 10:00:00.000 +0300
|
||||
29,6,8,2023-01-13 11:00:00.000 +0300
|
||||
30,6,12,2023-02-06 09:00:00.000 +0300
|
||||
31,6,13,2023-02-06 09:30:00.000 +0300
|
||||
32,6,14,2023-02-07 10:00:00.000 +0300
|
||||
33,6,9,2023-03-09 11:00:00.000 +0300
|
||||
34,7,1,2023-01-14 10:00:00.000 +0300
|
||||
35,7,2,2023-01-14 10:20:00.000 +0300
|
||||
36,7,3,2023-01-15 09:00:00.000 +0300
|
||||
37,7,9,2023-03-11 14:00:00.000 +0300
|
||||
38,7,10,2023-03-11 14:30:00.000 +0300
|
||||
39,9,12,2023-02-13 10:00:00.000 +0300
|
||||
40,9,13,2023-02-13 10:30:00.000 +0300
|
||||
41,9,1,2023-02-16 11:00:00.000 +0300
|
||||
42,12,1,2023-01-17 10:00:00.000 +0300
|
||||
43,12,2,2023-01-17 10:20:00.000 +0300
|
||||
44,12,1,2023-04-10 11:00:00.000 +0300
|
||||
45,12,3,2023-04-10 11:30:00.000 +0300
|
||||
46,12,9,2023-03-12 10:00:00.000 +0300
|
||||
47,13,6,2023-01-20 14:00:00.000 +0300
|
||||
48,13,7,2023-01-21 10:00:00.000 +0300
|
||||
49,13,12,2023-02-21 09:00:00.000 +0300
|
||||
50,14,1,2023-01-09 08:00:00.000 +0300
|
||||
51,14,2,2023-01-09 08:15:00.000 +0300
|
||||
52,14,3,2023-01-10 10:00:00.000 +0300
|
||||
53,14,4,2023-01-10 10:30:00.000 +0300
|
||||
54,14,5,2023-01-11 09:00:00.000 +0300
|
||||
55,14,6,2023-01-10 14:00:00.000 +0300
|
||||
56,14,7,2023-01-11 10:00:00.000 +0300
|
||||
57,14,8,2023-01-12 11:00:00.000 +0300
|
||||
58,14,9,2023-01-10 08:00:00.000 +0300
|
||||
|
@@ -0,0 +1,19 @@
|
||||
"id","course_id","title","duration_min"
|
||||
1,1,SELECT и FROM,10
|
||||
2,1,JOIN,15
|
||||
3,1,WHERE и фильтрация,12
|
||||
4,1,Агрегации и GROUP BY,18
|
||||
5,1,Подзапросы,25
|
||||
6,2,Pandas основы,20
|
||||
7,2,NumPy,15
|
||||
8,2,Визуализация данных,22
|
||||
9,3,Основы BI,12
|
||||
10,3,Метрики и дашборды,18
|
||||
11,3,Power BI введение,20
|
||||
12,4,Формулы в Excel,15
|
||||
13,4,Сводные таблицы,18
|
||||
14,4,Графики и диаграммы,14
|
||||
15,5,Введение в ML,20
|
||||
16,5,Линейная регрессия,30
|
||||
17,5,Классификация,28
|
||||
18,5,Кластеризация,25
|
||||
|
@@ -0,0 +1,21 @@
|
||||
"id","name","age","email","registration_date"
|
||||
1,Alice,25,alice@mail.com,2023-01-10 00:00:00.000 +0300
|
||||
2,Bob,30,bob@gmail.com,2023-02-05 00:00:00.000 +0300
|
||||
3,Charlie,22,charlie@mail.com,2023-02-20 00:00:00.000 +0300
|
||||
4,Diana,28,diana@mail.com,2023-03-01 00:00:00.000 +0300
|
||||
5,Ethan,35,ethan@gmail.com,2023-03-10 00:00:00.000 +0300
|
||||
6,Frank,29,frank@mail.com,2023-01-05 00:00:00.000 +0300
|
||||
7,Grace,26,grace@gmail.com,2023-01-12 00:00:00.000 +0300
|
||||
8,Henry,32,henry@mail.com,2023-01-20 00:00:00.000 +0300
|
||||
9,Iris,24,iris@gmail.com,2023-02-10 00:00:00.000 +0300
|
||||
10,Jack,27,jack@mail.com,2023-04-15 00:00:00.000 +0300
|
||||
11,Kelly,23,kelly@gmail.com,2023-04-20 00:00:00.000 +0300
|
||||
12,Leo,31,leo@mail.com,2023-01-15 00:00:00.000 +0300
|
||||
13,Mia,26,mia@gmail.com,2023-01-18 00:00:00.000 +0300
|
||||
14,Nina,28,nina@mail.com,2023-01-08 00:00:00.000 +0300
|
||||
15,Oscar,33,oscar@gmail.com,2023-01-10 00:00:00.000 +0300
|
||||
16,Paul,29,paul@mail.com,2023-02-15 00:00:00.000 +0300
|
||||
17,Quinn,24,quinn@gmail.com,2023-03-20 00:00:00.000 +0300
|
||||
18,Rita,27,rita@mail.com,2023-04-01 00:00:00.000 +0300
|
||||
19,Sam,30,sam@gmail.com,2023-04-10 00:00:00.000 +0300
|
||||
20,Tina,25,tina@mail.com,2023-04-18 00:00:00.000 +0300
|
||||
|
@@ -0,0 +1,189 @@
|
||||
-- 4
|
||||
--DROP TABLE IF EXISTS core.dim_user;
|
||||
CREATE TABLE core.dim_user (
|
||||
user_id bigint PRIMARY KEY,
|
||||
name varchar(100),
|
||||
age smallint,
|
||||
email varchar(100) NOT NULL,
|
||||
registration_date timestamptz NOT NULL
|
||||
) DISTRIBUTED BY (user_id);
|
||||
|
||||
--DROP TABLE IF EXISTS core.dim_course;
|
||||
CREATE TABLE core.dim_course (
|
||||
course_id bigint PRIMARY KEY,
|
||||
title varchar(150) NOT NULL,
|
||||
category varchar(50),
|
||||
created_at timestamptz NOT NULL
|
||||
) DISTRIBUTED BY (course_id);
|
||||
|
||||
--DROP TABLE IF EXISTS core.dim_lesson;
|
||||
CREATE TABLE core.dim_lesson (
|
||||
lesson_id bigint PRIMARY KEY,
|
||||
title varchar(150) NOT NULL,
|
||||
duration_min smallint,
|
||||
course_id bigint NOT NULL REFERENCES core.dim_course(course_id)
|
||||
) DISTRIBUTED BY (lesson_id);
|
||||
CREATE INDEX idx_dim_lesson_course_id ON core.dim_lesson(course_id);
|
||||
|
||||
--DROP TABLE IF EXISTS core.dim_date;
|
||||
CREATE TABLE core.dim_date (
|
||||
date date PRIMARY KEY,
|
||||
year int,
|
||||
month int,
|
||||
day int,
|
||||
weekday text
|
||||
) DISTRIBUTED BY (date);
|
||||
|
||||
|
||||
-- 5
|
||||
--DROP TABLE IF EXISTS core.fact_lesson_views;
|
||||
CREATE TABLE core.fact_lesson_views (
|
||||
user_id bigint NOT NULL REFERENCES core.dim_user(user_id),
|
||||
lesson_id bigint NOT NULL REFERENCES core.dim_lesson(lesson_id),
|
||||
course_id bigint NOT NULL REFERENCES core.dim_course(course_id),
|
||||
viewed_at timestamptz NOT NULL
|
||||
) DISTRIBUTED BY (user_id);
|
||||
CREATE INDEX idx_fact_lesson_views_viewed_at ON core.fact_lesson_views(viewed_at);
|
||||
CREATE INDEX idx_fact_lesson_views_user_id ON core.fact_lesson_views(user_id);
|
||||
CREATE INDEX idx_fact_lesson_views_lesson_id ON core.fact_lesson_views(lesson_id);
|
||||
CREATE INDEX idx_fact_lesson_views_course_id ON core.fact_lesson_views(course_id);
|
||||
|
||||
--DROP TABLE IF EXISTS core.enrollments;
|
||||
CREATE TABLE core.enrollments (
|
||||
user_id bigint NOT NULL REFERENCES core.dim_user(user_id),
|
||||
course_id bigint NOT NULL REFERENCES core.dim_course(course_id),
|
||||
enrolled_at timestamptz NOT NULL
|
||||
) DISTRIBUTED BY (user_id);
|
||||
CREATE INDEX idx_enrollments_user_id ON core.enrollments(user_id);
|
||||
CREATE INDEX idx_enrollments_course_id ON core.enrollments(course_id);
|
||||
|
||||
|
||||
--6
|
||||
-- dim_user
|
||||
INSERT INTO core.dim_user (user_id, name, age, email, registration_date)
|
||||
SELECT
|
||||
raw_u.id,
|
||||
raw_u.name,
|
||||
raw_u.age,
|
||||
raw_u.email,
|
||||
raw_u.registration_date
|
||||
FROM
|
||||
raw.users raw_u
|
||||
WHERE
|
||||
NOT EXISTS (SELECT 1 FROM core.dim_user core_u WHERE core_u.user_id = raw_u.id)
|
||||
AND raw_u.email IS NOT NULL AND raw_u.registration_date IS NOT NULL;
|
||||
|
||||
UPDATE core.dim_user core_u
|
||||
SET
|
||||
name = raw_u.name,
|
||||
age = raw_u.age,
|
||||
email = raw_u.email,
|
||||
registration_date = raw_u.registration_date
|
||||
FROM
|
||||
raw.users raw_u
|
||||
WHERE
|
||||
core_u.user_id = raw_u.id
|
||||
AND (
|
||||
core_u.name != raw_u.name OR
|
||||
core_u.age != raw_u.age OR
|
||||
core_u.email != raw_u.email OR
|
||||
core_u.registration_date != raw_u.registration_date
|
||||
);
|
||||
|
||||
-- dim_course
|
||||
INSERT INTO core.dim_course (course_id, title, category, created_at)
|
||||
SELECT
|
||||
raw_c.id,
|
||||
raw_c.title,
|
||||
raw_c.category,
|
||||
raw_c.created_at
|
||||
FROM
|
||||
raw.courses raw_c
|
||||
WHERE
|
||||
NOT EXISTS (SELECT 1 FROM core.dim_course core_c WHERE core_c.course_id = raw_c.id)
|
||||
AND raw_c.title IS NOT NULL AND raw_c.created_at IS NOT NULL;
|
||||
|
||||
UPDATE core.dim_course core_c
|
||||
SET
|
||||
title = raw_c.title,
|
||||
category = raw_c.category,
|
||||
created_at = raw_c.created_at
|
||||
FROM
|
||||
raw.courses raw_c
|
||||
WHERE
|
||||
core_c.course_id = raw_c.id
|
||||
AND (
|
||||
core_c.title != raw_c.title OR
|
||||
core_c.category != raw_c.category OR
|
||||
core_c.created_at != raw_c.created_at
|
||||
);
|
||||
|
||||
-- dim_lesson
|
||||
INSERT INTO core.dim_lesson (lesson_id, title, duration_min, course_id)
|
||||
SELECT
|
||||
raw_l.id,
|
||||
raw_l.title,
|
||||
raw_l.duration_min,
|
||||
raw_l.course_id
|
||||
FROM
|
||||
raw.lessons raw_l
|
||||
WHERE
|
||||
NOT EXISTS (SELECT 1 FROM core.dim_lesson core_l WHERE core_l.lesson_id = raw_l.id)
|
||||
AND raw_l.title IS NOT NULL AND raw_l.course_id IS NOT NULL;
|
||||
|
||||
UPDATE core.dim_lesson core_l
|
||||
SET
|
||||
title = raw_l.title,
|
||||
duration_min = raw_l.duration_min,
|
||||
course_id = raw_l.course_id -- решил оставить, тк чисто теоретические же урок может начать принадлежать другому курсу
|
||||
FROM
|
||||
raw.lessons raw_l
|
||||
WHERE
|
||||
core_l.lesson_id = raw_l.id
|
||||
AND (
|
||||
core_l.title != raw_l.title OR
|
||||
core_l.duration_min != raw_l.duration_min OR
|
||||
core_l.course_id != raw_l.course_id
|
||||
);
|
||||
|
||||
-- загуглил как заполнять dim_date:
|
||||
INSERT INTO core.dim_date (date, year, month, day, weekday)
|
||||
SELECT
|
||||
d::date,
|
||||
EXTRACT(YEAR FROM d)::int,
|
||||
EXTRACT(MONTH FROM d)::int,
|
||||
EXTRACT(DAY FROM d)::int,
|
||||
to_char(d, 'day')
|
||||
FROM
|
||||
generate_series('2023-01-01'::date, '2030-12-31'::date, INTERVAL '1 day') AS d;
|
||||
|
||||
-- fact_lesson_views
|
||||
INSERT INTO core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at)
|
||||
SELECT
|
||||
raw_lv.user_id,
|
||||
raw_lv.lesson_id,
|
||||
raw_l.course_id,
|
||||
raw_lv.viewed_at
|
||||
FROM
|
||||
raw.lesson_views raw_lv
|
||||
JOIN
|
||||
raw.lessons raw_l ON raw_lv.lesson_id = raw_l.id
|
||||
WHERE
|
||||
raw_lv.viewed_at > (SELECT max(viewed_at) FROM core.fact_lesson_views) -- но при первой загрузке это не сработало, тк max(vied_at) = NULL, как это исправить - я не знаю
|
||||
AND raw_lv.user_id IS NOT NULL AND raw_lv.lesson_id IS NOT NULL
|
||||
AND raw_l.course_id IS NOT NULL AND raw_lv.viewed_at IS NOT NULL;
|
||||
|
||||
-- enrollments
|
||||
INSERT INTO core.enrollments (user_id, course_id, enrolled_at)
|
||||
SELECT
|
||||
raw_e.user_id,
|
||||
raw_e.course_id,
|
||||
raw_e.enrolled_at
|
||||
FROM
|
||||
raw.enrollments raw_e
|
||||
WHERE
|
||||
NOT EXISTS (SELECT 1 FROM core.enrollments core_e WHERE
|
||||
core_e.user_id = raw_e.user_id
|
||||
AND core_e.course_id = raw_e.course_id
|
||||
AND core_e.enrolled_at = raw_e.enrolled_at)
|
||||
AND raw_e.user_id IS NOT NULL AND raw_e.course_id IS NOT NULL AND raw_e.enrolled_at IS NOT NULL;
|
||||
@@ -0,0 +1,99 @@
|
||||
-- 7
|
||||
--DROP MATERIALIZED VIEW dm.lesson_popularity_summary;
|
||||
CREATE MATERIALIZED VIEW dm.lesson_popularity_summary AS
|
||||
WITH results_fact_lesson_views AS (
|
||||
SELECT
|
||||
flv.lesson_id,
|
||||
count(flv.user_id) AS total_views,
|
||||
count(DISTINCT flv.user_id) AS unique_users,
|
||||
min(flv.viewed_at)::date AS first_view,
|
||||
max(flv.viewed_at)::date AS last_view
|
||||
FROM
|
||||
core.fact_lesson_views flv
|
||||
GROUP BY
|
||||
flv.lesson_id
|
||||
)
|
||||
SELECT
|
||||
dl.lesson_id,
|
||||
dl.title AS lesson_title,
|
||||
dl.course_id,
|
||||
dc.title AS course_title,
|
||||
rflv.total_views,
|
||||
rflv.unique_users,
|
||||
rflv.first_view,
|
||||
rflv.last_view
|
||||
FROM
|
||||
core.dim_lesson dl
|
||||
JOIN
|
||||
core.dim_course dc ON dl.course_id = dc.course_id
|
||||
LEFT JOIN
|
||||
results_fact_lesson_views rflv ON dl.lesson_id = rflv.lesson_id;
|
||||
|
||||
|
||||
-- 8
|
||||
--DROP MATERIALIZED VIEW dm.inactive_users_summary;
|
||||
CREATE MATERIALIZED VIEW dm.inactive_users_summary AS
|
||||
SELECT
|
||||
du.user_id,
|
||||
du.name AS user_name,
|
||||
du.email,
|
||||
du.age,
|
||||
e.enrollments_count,
|
||||
du.registration_date
|
||||
FROM
|
||||
core.dim_user du
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
user_id,
|
||||
count(course_id) AS enrollments_count
|
||||
FROM
|
||||
core.enrollments
|
||||
GROUP BY
|
||||
user_id
|
||||
) e ON du.user_id = e.user_id
|
||||
WHERE
|
||||
NOT EXISTS (SELECT 1 FROM core.fact_lesson_views flv WHERE flv.user_id = du.user_id);
|
||||
-- интересно, что во время проверки плана запроса, когда кусок с enrollments_count был в CTE, план запроса выдавал какую-то хуйню, а когда я засунул его в подзапрос, сразу индексы заработали, и строки начали эффективней читаться
|
||||
|
||||
|
||||
-- 9
|
||||
--DROP MATERIALIZED VIEW dm.course_completion_rate;
|
||||
CREATE MATERIALIZED VIEW dm.course_completion_rate AS
|
||||
WITH results_dim_course_lesson AS (
|
||||
SELECT
|
||||
dc.course_id,
|
||||
dc.title,
|
||||
count(lesson_id) AS lessons_in_course
|
||||
FROM
|
||||
core.dim_course dc
|
||||
JOIN
|
||||
core.dim_lesson dl ON dc.course_id = dl.course_id
|
||||
GROUP BY
|
||||
dc.course_id, dc.title
|
||||
),
|
||||
results_fact_lesson_views AS (
|
||||
SELECT
|
||||
flv.user_id,
|
||||
flv.course_id,
|
||||
count(DISTINCT lesson_id) AS lessons_viewed
|
||||
FROM
|
||||
core.fact_lesson_views flv
|
||||
GROUP BY
|
||||
flv.user_id, flv.course_id
|
||||
)
|
||||
SELECT
|
||||
du.user_id,
|
||||
du.name AS user_name,
|
||||
rdcl.course_id,
|
||||
rdcl.title AS course_title,
|
||||
rdcl.lessons_in_course,
|
||||
COALESCE(rflv.lessons_viewed, 0) AS lessons_viewed,
|
||||
round(COALESCE(rflv.lessons_viewed, 0)::numeric / NULLIF(rdcl.lessons_in_course, 0), 2) AS completion_rate
|
||||
FROM
|
||||
core.dim_user du
|
||||
JOIN
|
||||
core.enrollments e ON du.user_id = e.user_id
|
||||
JOIN
|
||||
results_dim_course_lesson rdcl ON e.course_id = rdcl.course_id
|
||||
LEFT JOIN
|
||||
results_fact_lesson_views rflv ON e.user_id = rflv.user_id AND e.course_id = rflv.course_id;
|
||||
@@ -0,0 +1,4 @@
|
||||
SELECT * FROM dm.lesson_popularity_summary ORDER BY lesson_id LIMIT 10;
|
||||
SELECT * FROM dm.inactive_users_summary ORDER BY user_id LIMIT 10;
|
||||
SELECT * FROM dm.course_completion_rate ORDER BY user_id, course_id;
|
||||
--пришлось добавить order_by для красивого вывода, потому что создать витрины отсортированными не получается
|
||||
@@ -0,0 +1,9 @@
|
||||
Задание 1
|
||||
- запос не будет локальным
|
||||
- гринплам будет делать data motion
|
||||
|
||||
Задание 2
|
||||
- distributed by не подходит, тк таблица не будет учавствовать в join
|
||||
|
||||
Задание 3
|
||||
- можно добавить event_id и распределять по нему, или же сделать destributed randomly
|
||||
@@ -0,0 +1,6 @@
|
||||
Задание 1
|
||||
- если orders DISTRIBUTED BY user_id, то будет 'Seq Scan on orders' -> 'HashAggregate'. Redistribute Motion(по user_id) пропадет, потому что все уже распределено по user_id;
|
||||
|
||||
Задание 2
|
||||
- Это называется Skew.
|
||||
- Чтобы избежать skew, нужно выбрать столбец для распределения с нормальной кардинальностью. Или делать destributed randomly если не нужно join.
|
||||
@@ -0,0 +1,10 @@
|
||||
Задание 1
|
||||
- данные физически находятся в Hive;
|
||||
- писать обратно в Hive нельзя, тк таблица не writable;
|
||||
- не знаю, в теорри это не сказано - думаю будет ошибка. После того как загуглил:
|
||||
1) если в hive добавились новые столбцы, то при чтении все будет отображаться так, как было во время создание внешней таблицы в greenplum, без ошибок;
|
||||
2) если в hive удалась часть столбцов, то возникнет ошибка тк, в greenplum по сути будут лишние столбцы.
|
||||
|
||||
Задание 2
|
||||
- данные не копируются в greenplum, а только читаются из s3;
|
||||
- я думаю в момент создания таблицы - быстрее external, тк ничего не копируется фищически, но при чтении должен быть быстрее CREATE TABLE AS SELECT, тк данные физически хранятся на нашем сервере, а не где-то в облаке.
|
||||
@@ -0,0 +1,17 @@
|
||||
Задание 1:
|
||||
- Будут собираться все 10млн строк с разных сегментов, а тольок потом сработает LIMIT. Это очень дорого стоит, и медленно тк очень много данных в пустую соединятся в мастер.
|
||||
- Я думаю можно к текущему запросу добавить DENSE_RANK по total и написать условие where -тут имя dense_rank- <= 10.
|
||||
!!! В теории не сказано про работу order by - пришлось загуглить !!!
|
||||
|
||||
Задание 2:
|
||||
- PARTITION BY по. region_id, а должен быть по order_id, чтобы работало без задержек, а при текущем варианте запроса будет происходить data motion как я понял, а это лишние вычисления.
|
||||
- Тогда надо распределять таблицу по region_id полагаю.
|
||||
|
||||
Задание 3:
|
||||
- Ответ до подсказки: думаю все будет нормально, данные просто сгруппируются и вставятся, никаких зарежек быть не должно.
|
||||
- Ответ после подсказки: greenplum будет пересылать(data motion) user_id между сегментами, чтобы собрать их в группы, а только потом собирать в мастер и инсертить, инсерт уже поидее будет происходить хорошо, тк данные после data motion будут лежать нормально.
|
||||
- Orders надо дестрибьютить по user_id, тогда все четко и быстро будет - и group by, и join.
|
||||
|
||||
Задание 4:
|
||||
- Я хочу увидеть Motion, Broadcast, Gather
|
||||
- Что означает Broadcast Motion, Redistribute Motion, Gather Motion? - в душе ни чаю, там вопрос с ответом сразу, взял оттуда: Broadcast Motion - разослать копии каждой строки на все сегменты; Redistribute Motion - перераспределить по новому ключу; Gather Motion - собрать на мастер.
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
-- ПРАКТИКА DDL
|
||||
-- Задание 1
|
||||
DROP TABLE IF EXISTS mnoskov.users;
|
||||
CREATE TABLE mnoskov.users (
|
||||
id bigint PRIMARY KEY,
|
||||
name varchar(100) NOT NULL,
|
||||
age SMALLINT,
|
||||
email varchar(100),
|
||||
country char(2), -- прогуглил что распространено сохранять iso-код а не все название
|
||||
is_active boolean,
|
||||
created_at timestamptz NOT NULL -- думаю timestamptz, если не учитывать часовой пояс, то даты могут отображаться неверно
|
||||
);
|
||||
|
||||
|
||||
-- Задание 2
|
||||
DROP TABLE IF EXISTS mnoskov.products;
|
||||
CREATE TABLE mnoskov.products (
|
||||
id bigint PRIMARY KEY,
|
||||
name varchar(100) NOT NULL,
|
||||
category varchar(50),
|
||||
price decimal(10,2) NOT NULL,
|
||||
in_stock boolean
|
||||
);
|
||||
|
||||
|
||||
-- Задание 3
|
||||
DROP TABLE IF EXISTS mnoskov.orders;
|
||||
CREATE TABLE mnoskov.orders (
|
||||
id bigint PRIMARY KEY,
|
||||
user_id bigint,
|
||||
order_date timestamptz NOT NULL, -- в названии date, и тип думаю date
|
||||
status varchar(20), -- думал лучше int, но судя по заданию DML лучше varchar
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
|
||||
-- Задание 4
|
||||
DROP TABLE IF EXISTS mnoskov.order_items;
|
||||
CREATE TABLE mnoskov.order_items (
|
||||
order_id bigint,
|
||||
product_id bigint,
|
||||
quantity int, -- думал надо smallint но потом подумал что вдруг 50к гвоздей
|
||||
price_at_purchase decimal(10,2),
|
||||
PRIMARY KEY (order_id, product_id),
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id),
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
);
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
-- Задание 1
|
||||
INSERT INTO mnoskov.users (id, name, age, email, country, is_active, created_at)
|
||||
VALUES
|
||||
(1, 'Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00'),
|
||||
(2, 'Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00'),
|
||||
(3, 'Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00'),
|
||||
(4, 'Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00'),
|
||||
(5, 'Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00'),
|
||||
(6, 'Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00'),
|
||||
(7, 'George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00'),
|
||||
(8, 'Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00'),
|
||||
(9, 'Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00'),
|
||||
(10, 'Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00');
|
||||
|
||||
-- Задание 2
|
||||
INSERT INTO mnoskov.products (id, name, category, price, in_stock)
|
||||
VALUES
|
||||
(1, 'iPhone 15', 'Электроника', 80000, true),
|
||||
(2, 'Kindle', 'Электроника', 15000, true),
|
||||
(3, 'Кроссовки Nike', 'Одежда', 9000, true),
|
||||
(4, 'Кроссовки NoName', 'Одежда', 3000, false),
|
||||
(5, 'Наушники Pro', 'Электроника', 12000, true),
|
||||
(6, 'Фитнес-браслет', 'Гаджеты', 6000, true),
|
||||
(7, 'Подарочная карта', 'Сервисы', 1000, true);
|
||||
|
||||
-- Задание 3
|
||||
INSERT INTO mnoskov.orders (id, user_id, order_date, status)
|
||||
VALUES
|
||||
(1, 1, '2025-01-10 12:00:00', 'paid'),
|
||||
(2, 3, '2025-01-12 09:30:00', 'cancelled'),
|
||||
(3, 1, '2025-01-15 18:20:00', 'paid'),
|
||||
(4, 2, '2025-01-20 11:05:00', 'created'),
|
||||
(5, 4, '2025-02-01 17:45:00', 'paid'),
|
||||
(6, 5, '2025-02-10 10:10:00', 'refunded'),
|
||||
(7, 7, '2025-03-01 09:00:00', 'paid'),
|
||||
(8, 9, '2025-03-05 19:30:00', 'paid');
|
||||
|
||||
-- Задание 4
|
||||
INSERT INTO mnoskov.order_items (order_id, product_id, quantity, price_at_purchase)
|
||||
VALUES
|
||||
(1, 1, 1, 78000),
|
||||
(1, 3, 2, 8500),
|
||||
(2, 2, 1, 14000),
|
||||
(3, 5, 1, 11000),
|
||||
(3, 7, 1, 1000),
|
||||
(4, 2, 1, 15000),
|
||||
(5, 6, 1, 6000),
|
||||
(6, 1, 1, 82000),
|
||||
(7, 3, 1, 9000),
|
||||
(8, 4, 2, 2800);
|
||||
@@ -0,0 +1,59 @@
|
||||
-- Задание 1
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
country,
|
||||
created_at,
|
||||
email
|
||||
FROM mnoskov.users
|
||||
ORDER BY created_at DESC, name;
|
||||
-- выбрал эти столбцы потому что так сказано в задании, я бы добавил ещё is_active
|
||||
-- такая сортировка для того, чтобы даты шли от новых к старым и имена людей за одну дату шли по алфавиту
|
||||
|
||||
|
||||
-- Задание 2
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
country
|
||||
FROM mnoskov.users
|
||||
WHERE country = 'US'
|
||||
ORDER BY name;
|
||||
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
country
|
||||
FROM mnoskov.users
|
||||
WHERE country = 'UK'
|
||||
ORDER BY name;
|
||||
-- для маркетолого полезнее первый запрос т.к. там больше данных
|
||||
|
||||
|
||||
-- Задание 3
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
email
|
||||
FROM mnoskov.users
|
||||
WHERE email IS NOT NULL
|
||||
ORDER BY name;
|
||||
|
||||
SELECT DISTINCT
|
||||
lower(email) AS email_lower
|
||||
FROM mnoskov.users
|
||||
WHERE email IS NOT NULL;
|
||||
-- приводить email к одному регистру мб для красоты, вроде везде email принимается и в нижем и в верхнем регистре
|
||||
-- дубликаты убираем чтобы вытрина занимала меньше места, и чтобы не было повторных рассылок
|
||||
|
||||
|
||||
-- Задание 4
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
created_at
|
||||
FROM mnoskov.users
|
||||
WHERE created_at > '2024-02-20 23:59'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 2 OFFSET 1;
|
||||
-- OFFSET может пригодиться, чтобы взять интервал данных
|
||||
@@ -0,0 +1,78 @@
|
||||
-- Задание 1
|
||||
SELECT
|
||||
name,
|
||||
category,
|
||||
price,
|
||||
price * 1.30 AS price_with_markup,
|
||||
price * 1.50 AS price_with_vat
|
||||
FROM
|
||||
mnoskov.products
|
||||
WHERE
|
||||
in_stock = TRUE AND
|
||||
category != 'smartwatch' AND -- категории smartwatch нет в предоставленных данных для заполнения
|
||||
price BETWEEN 100 AND 1300;
|
||||
-- Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему?
|
||||
-- Лучше читается первый вариант, потому что просто прочитал и понял, а не думаешь над знаками, и первый вариант короче
|
||||
|
||||
|
||||
-- Задание 2
|
||||
SELECT
|
||||
name,
|
||||
price,
|
||||
price * 0.85 AS price_with_discount
|
||||
FROM
|
||||
mnoskov.products
|
||||
WHERE
|
||||
(price < 100 OR price > 1000);
|
||||
-- Нужно отделять OR скобками, потому что если в условии будет ещё и AND, то логика может поломаться потому что AND выполняется раньше чем OR
|
||||
|
||||
|
||||
-- Задание 3
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
category,
|
||||
price
|
||||
FROM
|
||||
mnoskov.products
|
||||
WHERE
|
||||
category IN ('tablet', 'smartphone', 'mouse') AND
|
||||
price >= 100
|
||||
ORDER BY
|
||||
price DESC;
|
||||
|
||||
|
||||
--Задание 4
|
||||
SELECT
|
||||
name,
|
||||
category,
|
||||
price
|
||||
FROM
|
||||
mnoskov.products
|
||||
WHERE
|
||||
name NOT LIKE '%iPhone%';
|
||||
|
||||
|
||||
-- Задание 5
|
||||
-- 1)
|
||||
SELECT
|
||||
name,
|
||||
category,
|
||||
price
|
||||
FROM
|
||||
mnoskov.products
|
||||
WHERE
|
||||
name ILIKE '%pro%';
|
||||
|
||||
-- 2)
|
||||
SELECT
|
||||
name,
|
||||
category,
|
||||
price
|
||||
FROM
|
||||
mnoskov.products
|
||||
WHERE
|
||||
name ILIKE '%mx%'
|
||||
ORDER BY
|
||||
price DESC;
|
||||
-- Для поисковой строи лучше ILIKE, потому что он не учитывает регистр - кто-то пишет с большой буквы, кто-то с маленькой, а кто-то случайно включил капс.
|
||||
@@ -0,0 +1,188 @@
|
||||
-- Задание 1
|
||||
SELECT
|
||||
o.id AS order_id,
|
||||
o.order_date,
|
||||
o.status,
|
||||
u.name AS user_name,
|
||||
u.country
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
LEFT JOIN
|
||||
mnoskov.users u ON o.user_id = u.id;
|
||||
-- Логичнее начать с orders, тк надо вывести пользователей по заказам, а не наоборот.
|
||||
-- Здесть уместнее LEFT, потому что в условии сказано ВСЕ заказы и уже по ним пользователей. Попробовал и INNER и LEFT - вывод один и тот же.
|
||||
|
||||
|
||||
-- Задание 2
|
||||
SELECT
|
||||
oi.order_id,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM
|
||||
mnoskov.order_items oi
|
||||
JOIN
|
||||
mnoskov.products p ON oi.product_id = p.id
|
||||
WHERE
|
||||
oi.order_id = 1; -- если 101, как в условии то ничего не будет
|
||||
-- Если фильтрацию поставить после ON, то выведутся все продукты по 2 раза, у всех id=1, quantity и price возьмутсы из двух строк в order_items где id=1.
|
||||
|
||||
|
||||
-- Задание 3
|
||||
SELECT
|
||||
oi.order_id,
|
||||
u.name AS user_name,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM
|
||||
mnoskov.order_items oi
|
||||
JOIN
|
||||
mnoskov.products p ON oi.product_id = p.id
|
||||
JOIN
|
||||
mnoskov.orders o ON oi.order_id = o.id
|
||||
JOIN
|
||||
mnoskov.users u ON o.user_id = u.id;
|
||||
-- Заказ с id=3 повторяется два раза, потому что в заказе два товара, и они отображаются на двух строках.
|
||||
-- Поскольку мы выводис все заказы, у нас больше строк по сравнению с заданием-2, и ещё мы выводим имена заказчиков.
|
||||
|
||||
|
||||
--Задание 4
|
||||
SELECT
|
||||
u.name AS user_name,
|
||||
o.id AS order_id,
|
||||
o.status
|
||||
FROM
|
||||
mnoskov.users u
|
||||
LEFT JOIN
|
||||
mnoskov.orders o ON u.id = o.user_id;
|
||||
-- Этот запрос отличается от INNER тем, что тут отображаются ещё и пользователи без заказов.
|
||||
|
||||
|
||||
-- Задание 5
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.name AS user_name
|
||||
FROM
|
||||
mnoskov.users u
|
||||
LEFT JOIN
|
||||
mnoskov.orders o ON u.id = o.user_id
|
||||
WHERE
|
||||
o.user_id IS NULL;
|
||||
-- Важно фильтровать по колонке из orders, потому что она будет NULL из-за LEFT JOIN. Если есть несостыковки, то NULL будет в правой таблице после LEFT JOIN.
|
||||
|
||||
|
||||
-- Задание 6
|
||||
SELECT
|
||||
oi.order_id,
|
||||
u.id AS user_id,
|
||||
p.name AS product_name,
|
||||
oi.quantity
|
||||
FROM
|
||||
mnoskov.order_items oi
|
||||
JOIN
|
||||
mnoskov.products p ON oi.product_id = p.id
|
||||
JOIN
|
||||
mnoskov.orders o ON oi.order_id = o.id
|
||||
JOIN
|
||||
mnoskov.users u ON o.user_id = u.id
|
||||
WHERE
|
||||
p.name LIKE 'Kindle';
|
||||
-- Заказ 2 встречается 1 раз, потому что там всего один товар, и потому что мы ищем именно товар с названием - Kindle.
|
||||
|
||||
|
||||
-- Задание 7
|
||||
SELECT
|
||||
p.name AS product_name,
|
||||
oi.order_id,
|
||||
oi.quantity
|
||||
FROM
|
||||
mnoskov.products p
|
||||
LEFT JOIN
|
||||
mnoskov.order_items oi ON p.id = oi.product_id;
|
||||
-- Если товар не покупали то в order_id будет NULL
|
||||
-- Если использовать INNER, то выведутся только товары у которых есть заказы, в случае с LEFT выведутся все заказы.
|
||||
|
||||
|
||||
-- Задание 8
|
||||
SELECT
|
||||
oi.order_id,
|
||||
u.name AS user_name,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM
|
||||
mnoskov.order_items oi
|
||||
JOIN
|
||||
mnoskov.products p ON oi.product_id = p.id
|
||||
JOIN
|
||||
mnoskov.orders o ON oi.order_id = o.id
|
||||
JOIN
|
||||
mnoskov.users u ON o.user_id = u.id
|
||||
WHERE
|
||||
o.status = 'paid';
|
||||
-- Если написать status = 'paid' после ON в orders, то ничего не поменяется, потому что после ON пишутся условия соединения таблиц также как и в WHERE, но как я понял лучше писать в WHERE.
|
||||
|
||||
|
||||
-- Задание 9
|
||||
SELECT
|
||||
oi.order_id,
|
||||
o.order_date,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM
|
||||
mnoskov.users u
|
||||
JOIN
|
||||
mnoskov.orders o ON u.id = o.user_id
|
||||
JOIN
|
||||
mnoskov.order_items oi ON o.id = oi.order_id
|
||||
JOIN
|
||||
mnoskov.order_items oi2 ON oi.product_id = oi2.product_id
|
||||
JOIN
|
||||
mnoskov.orders o2 ON oi2.order_id = o2.id
|
||||
JOIN
|
||||
mnoskov.products p ON oi2.product_id = p.id
|
||||
WHERE
|
||||
u.name = 'Alice';
|
||||
-- Если у Alice не будет заказов то ничего не выведется.
|
||||
-- Мы получается берем Alice -> идем до id товаров Alice -> идем обратно к заказам и выводим товары которые были у Alice.
|
||||
|
||||
-- Мне подсказала нейронка как использовать SELF JOIN - я понимал что он нужен, но не понимал как именно им воспользоваться.
|
||||
-- Только после такого запроса я разобрался как это работает:
|
||||
/*SELECT
|
||||
oi.order_id,
|
||||
o.order_date,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase,
|
||||
u2.name,
|
||||
u2.id
|
||||
FROM
|
||||
mnoskov.users u
|
||||
JOIN
|
||||
mnoskov.orders o ON u.id = o.user_id
|
||||
JOIN
|
||||
mnoskov.order_items oi ON o.id = oi.order_id
|
||||
JOIN
|
||||
mnoskov.order_items oi2 ON oi.product_id = oi2.product_id
|
||||
JOIN
|
||||
mnoskov.orders o2 ON oi2.order_id = o2.id
|
||||
JOIN
|
||||
mnoskov.users u2 ON o2.user_id = u2.id
|
||||
JOIN
|
||||
mnoskov.products p ON oi2.product_id = p.id
|
||||
WHERE
|
||||
u.name = 'Alice';*/
|
||||
|
||||
|
||||
-- Задание 10
|
||||
SELECT
|
||||
o.id AS order_id,
|
||||
o.user_id,
|
||||
o.status
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
LEFT JOIN
|
||||
mnoskov.order_items oi ON o.id = oi.order_id;
|
||||
-- Если написать INNER JOIN, то появятся только существующие заказы, которые есть в обеих таблицах.
|
||||
@@ -0,0 +1,75 @@
|
||||
-- Задание 1
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
'active' AS user_status
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE
|
||||
is_active = TRUE
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
'inactive' AS user_status
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE
|
||||
is_active = FALSE
|
||||
ORDER BY name;
|
||||
-- Если написать просто UNION, то в данном задании ничего не изменится
|
||||
|
||||
|
||||
-- Задание 2
|
||||
SELECT
|
||||
id AS order_id,
|
||||
user_id,
|
||||
order_date,
|
||||
'old' AS period
|
||||
FROM
|
||||
mnoskov.orders
|
||||
WHERE
|
||||
order_date < '2025-02-01'
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
id AS order_id,
|
||||
user_id,
|
||||
order_date,
|
||||
'new' AS period
|
||||
FROM
|
||||
mnoskov.orders
|
||||
WHERE
|
||||
order_date >= '2025-02-01'
|
||||
ORDER BY
|
||||
order_date, order_id;
|
||||
-- По колонке period я пойму старый(old) заказ или новый(new)
|
||||
-- Я не могу ответить, потому что CASE WHEN ещё не проходили
|
||||
|
||||
|
||||
-- Задание 3
|
||||
SELECT DISTINCT
|
||||
oi.product_id,
|
||||
p.name AS product_name,
|
||||
'ordered' AS product_status
|
||||
FROM
|
||||
mnoskov.products p
|
||||
JOIN
|
||||
mnoskov.order_items oi ON p.id = oi.product_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT DISTINCT
|
||||
p.id AS product_id,
|
||||
p.name AS product_name,
|
||||
'never_ordered' AS product_status
|
||||
FROM
|
||||
mnoskov.products p
|
||||
LEFT JOIN
|
||||
mnoskov.order_items oi ON p.id = oi.product_id
|
||||
WHERE oi.product_id is NULL;
|
||||
-- Проверка IS NULL по полю из order_items, потому что это права таблица, и null будут добавляться в неё
|
||||
-- 1) Понизить цену товара 2) лучше прорекламировать 3) снять с продажи
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
-- Задание 1
|
||||
SELECT
|
||||
category,
|
||||
count(*) products_count
|
||||
FROM
|
||||
mnoskov.products
|
||||
GROUP BY
|
||||
category
|
||||
ORDER BY
|
||||
products_count;
|
||||
-- Если убрать order by, то статистика будет в случайном разрбосанном виде, а не по возрастанию
|
||||
-- Увеличится products_count на одно значение
|
||||
|
||||
|
||||
-- Задание 2
|
||||
SELECT
|
||||
order_id,
|
||||
sum(quantity) AS total_quantity,
|
||||
sum(quantity * price_at_purchase) AS order_revenue
|
||||
FROM
|
||||
mnoskov.order_items
|
||||
GROUP BY
|
||||
order_id
|
||||
ORDER BY
|
||||
order_revenue DESC;
|
||||
-- Если убрать GROUP BY, то будет ошибка
|
||||
-- Нельзя просто добавить в SELECT ещё одну колонку (например, product_id) без добавления её в GROUP BY или агрегат, потому что вылезет ошибка. Ну и колонка должна быть либо сгруппирована либо в агрегате, она не может существовать сама по себе в запросе с grouup by
|
||||
|
||||
|
||||
-- Задание 3
|
||||
SELECT
|
||||
p.category,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS category_revenue
|
||||
FROM
|
||||
mnoskov.order_items oi
|
||||
JOIN
|
||||
mnoskov.products p ON oi.product_id = p.id
|
||||
GROUP BY
|
||||
p.category
|
||||
ORDER BY
|
||||
category_revenue DESC;
|
||||
-- Группировка по p.category - потому что мы хотим узнать сумму прибли по каждой категории
|
||||
-- Если добавить p.name - будет ошибка потому что p.name тоже нужно будет заносить в GROUP BY
|
||||
|
||||
|
||||
-- Задание 4
|
||||
SELECT
|
||||
oi.order_id,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS order_revenue
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
JOIN
|
||||
mnoskov.order_items oi ON o.id = oi.order_id
|
||||
WHERE
|
||||
o.status = 'paid'
|
||||
GROUP BY
|
||||
oi.order_id
|
||||
ORDER BY
|
||||
order_revenue DESC;
|
||||
-- В задаче-2 были все заказы, а тут только оплаченные
|
||||
-- Я бы смотрел толлько на paid, чтобы понять прибыль которая уже есть
|
||||
|
||||
|
||||
-- Задание 5
|
||||
SELECT
|
||||
p.category,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS category_revenue
|
||||
FROM
|
||||
mnoskov.order_items oi
|
||||
JOIN
|
||||
mnoskov.products p ON oi.product_id = p.id
|
||||
GROUP BY
|
||||
p.category
|
||||
HAVING
|
||||
sum(oi.quantity * oi.price_at_purchase) > 1000
|
||||
ORDER BY
|
||||
category_revenue DESC;
|
||||
-- Если добавить это условие в WHERE, то терминал будет ругаться мол агрегаты недопустимы в WHERE
|
||||
-- Обычно условие(прим: WHERE price > 1000) отличается от агрегированного условия(прим: HAVING SUM(...) > 1000) тем, что в первом случае ничего считать не нужно, значение уже есть, а во втором нужно сначала чтобы функция все посчитала. Ну и HAVING запускается уже после группировки
|
||||
|
||||
|
||||
-- Задание 6
|
||||
SELECT
|
||||
country,
|
||||
count(*) AS active_user_count
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE
|
||||
is_active = true
|
||||
GROUP BY
|
||||
country
|
||||
HAVING
|
||||
count(*) >= 2
|
||||
ORDER BY
|
||||
active_user_count DESC;
|
||||
-- is_active = true логичнее писать в WHERE, потому что is_active - не агрегат
|
||||
-- Если нет страны где больше двух активных юзеров, то ничего не выведется
|
||||
|
||||
|
||||
-- Задание 7
|
||||
SELECT
|
||||
p.name AS product_name,
|
||||
sum(oi.quantity) AS total_quantity_sold
|
||||
FROM
|
||||
mnoskov.order_items oi
|
||||
JOIN
|
||||
mnoskov.products p ON oi.product_id = p.id
|
||||
GROUP BY
|
||||
p.name
|
||||
ORDER BY
|
||||
total_quantity_sold DESC
|
||||
LIMIT 3;
|
||||
-- total_quantity_sold - количество проданного товара, а product_revenue - выручка каждого товара
|
||||
-- Бизнес сморит на количество, может быть когда товара очень много и нужно понять какой % уже продан, и когда нужно найти популярные или непопулярные товары
|
||||
|
||||
|
||||
-- Задание 8
|
||||
SELECT
|
||||
order_id,
|
||||
sum(quantity * price_at_purchase) AS order_revenue
|
||||
FROM
|
||||
mnoskov.order_items
|
||||
GROUP BY
|
||||
order_id
|
||||
HAVING
|
||||
sum(quantity * price_at_purchase) < 10000
|
||||
ORDER BY
|
||||
order_revenue;
|
||||
-- Не понял вопрос: Какие реальные причины могут быть у очень маленьких заказов (1–2 гипотезы)? Ответ: «доставка пробников/тестовой партии», «использование купонов/бонусов», «технический тест витрины»
|
||||
-- Проще увидеть глазами отфильтрованные микро-заказы
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
-- Задание 1
|
||||
SELECT
|
||||
name,
|
||||
created_at::date,
|
||||
current_date - created_at::date AS days_since_registration
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE
|
||||
is_active = TRUE
|
||||
AND current_date - created_at::date > 180
|
||||
ORDER BY
|
||||
days_since_registration desc;
|
||||
-- Очень старым пользователям можно кинуть рассылку на почту на подобии 'Представляете, мы вместе уже 777 дней, вот вам купон на скидку'
|
||||
-- Удобнее вычитать ::date, чтобы days_since_registration красиво выглядел в таблице
|
||||
|
||||
|
||||
-- Задание 2
|
||||
SELECT
|
||||
name,
|
||||
lower(email) AS email_clean,
|
||||
substring(email FROM POSITION('@' IN email) + 1) AS domain
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE email IS NOT NULL
|
||||
ORDER BY domain, name;
|
||||
-- email приводят к нижнему регистру для нормализации записи, а зачем выделяют домен не понимаю, может быть для аналитики по типу кто какой почтой пользуется
|
||||
-- Получу NULL в domain
|
||||
|
||||
|
||||
-- Задание 3
|
||||
SELECT
|
||||
name,
|
||||
age,
|
||||
CASE
|
||||
WHEN age < 25 THEN 'young'
|
||||
WHEN age BETWEEN 25 AND 35 THEN 'adult'
|
||||
WHEN age > 35 THEN 'senior'
|
||||
END AS age_group
|
||||
FROM
|
||||
mnoskov.users
|
||||
ORDER BY
|
||||
age;
|
||||
-- Почему удобнее держать логику сегментации в SQL (CASE), а не только в коде приложения? В каком коде? Если в 'коде приложения' - это какая-то отдельная программа, то могут предположить, чтобы передавать в приложение уже отфильтрованные данные, чтобы задачи базы данных не выносить из базы.
|
||||
-- Если забыть в ветку ELSE, то будет NULL у тех кто никуда не попал
|
||||
|
||||
|
||||
-- Задание 4
|
||||
SELECT
|
||||
name,
|
||||
created_at::date,
|
||||
current_date - created_at::date AS days_since_registration,
|
||||
CASE
|
||||
WHEN created_at >= current_date - INTERVAL '30 days' THEN 'hot' ELSE NULL
|
||||
END AS hot_flag
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE
|
||||
created_at >= current_date - INTERVAL '30 days';
|
||||
-- Фильтрация по дате в WHERE исключает неподходящие варианты, а простой CASE в нашем случае возвращает результат в виде двух вариантов - подошло/не подошло и эти оба варианта остаются в таблице
|
||||
-- Стоит запускать какие нибудь привлекательные варианты со скидкам и акциями, чтобы удержать пользователся
|
||||
|
||||
|
||||
-- Задание 5
|
||||
SELECT
|
||||
count(*) AS total_users,
|
||||
count(email) AS with_email,
|
||||
count(*) - count(email) AS without_email,
|
||||
((count(*) - count(email))::float / count(*)) * 100.0 AS percent_without_email
|
||||
FROM
|
||||
mnoskov.users;
|
||||
-- Агрегаты дают более точную информацию чем просто на глаз
|
||||
-- Я бы предолжил запустить активно вылезающие блоки с предложением зарегестрироваться, или же запретить пользоваение продуктом без регистрации
|
||||
|
||||
|
||||
-- Задание 6
|
||||
SELECT
|
||||
name,
|
||||
current_date - created_at::date AS days_since_registraion,
|
||||
CASE
|
||||
WHEN current_date - created_at::date <= 30 THEN 'new'
|
||||
WHEN current_date - created_at::date BETWEEN 31 AND 180 THEN 'middle'
|
||||
WHEN current_date - created_at::date > 180 THEN 'old'
|
||||
END AS tenure_group,
|
||||
is_active
|
||||
FROM
|
||||
mnoskov.users
|
||||
ORDER BY
|
||||
tenure_group, days_since_registraion;
|
||||
-- Сегментация по стажу говорит о опыте человека, а по возрасту только о возрасте
|
||||
-- Пересчитывать каждый день - плюсы: не занимает места на диске, минусы: тратит больше вычислительных ресурсов, а если хранить статическое поле, то все наоборот
|
||||
@@ -0,0 +1,155 @@
|
||||
-- Задание 1
|
||||
SELECT
|
||||
name
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE id IN (
|
||||
SELECT user_id
|
||||
FROM mnoskov.orders
|
||||
)
|
||||
ORDER BY
|
||||
name;
|
||||
-- Тут не приходится делать JOIN и DISTINCT, а просто ищем айдишник в списке
|
||||
-- N (подзапрос) может быть более читаемым, когда подзапрос очень простой и короткий
|
||||
|
||||
|
||||
-- Задание 2
|
||||
SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email
|
||||
FROM
|
||||
mnoskov.users u
|
||||
WHERE NOT EXISTS (
|
||||
SELECT user_id
|
||||
FROM mnoskov.orders o
|
||||
WHERE
|
||||
u.id = o.user_id
|
||||
)
|
||||
-- NOT EXISTS безопаснее в плане NULL, и часто планируется лучше чем NOT IN
|
||||
-- Определить какое количество людей не делают заказы, и кинуть им например купон на скидку на первый заказ
|
||||
|
||||
|
||||
-- Задание 3
|
||||
SELECT
|
||||
u.name,
|
||||
(
|
||||
SELECT
|
||||
count(*)
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
WHERE
|
||||
u.id = o.user_id
|
||||
) AS orders_count
|
||||
FROM
|
||||
mnoskov.users u
|
||||
ORDER BY
|
||||
orders_count DESC, u.name;
|
||||
-- Подзапрос в SELECT не может возвращать несколько строк, потому что SELECT ожидает толбко одну
|
||||
-- Как изменится сложность запроса, если пользователей станет, например, 1 млн? Не совсем понял этот вопрос, но думаю запрос просто будет выполняться дольше
|
||||
|
||||
|
||||
-- Задание 4
|
||||
WITH revenue_user AS (
|
||||
SELECT
|
||||
o.user_id,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS total_revenue
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
JOIN
|
||||
mnoskov.order_items oi ON o.id = oi.order_id
|
||||
GROUP BY
|
||||
o.user_id
|
||||
)
|
||||
SELECT
|
||||
u.name,
|
||||
ru.total_revenue
|
||||
FROM
|
||||
mnoskov.users u
|
||||
JOIN
|
||||
revenue_user ru ON u.id = ru.user_id
|
||||
ORDER BY
|
||||
ru.total_revenue desc;
|
||||
-- Проще читать когда шаг «посчитать выручку по пользователям» вынесен отдельно
|
||||
-- Чтобы избежать дублирования импользуется CTE, а не простой подзапрос
|
||||
|
||||
|
||||
-- Задание 5
|
||||
WITH revenue_product AS (
|
||||
SELECT
|
||||
product_id,
|
||||
sum(quantity * price_at_purchase) AS product_revenue
|
||||
FROM
|
||||
mnoskov.order_items
|
||||
GROUP BY
|
||||
product_id
|
||||
)
|
||||
SELECT
|
||||
p.name,
|
||||
p.category,
|
||||
rp.product_revenue
|
||||
FROM
|
||||
mnoskov.products p
|
||||
JOIN
|
||||
revenue_product rp ON p.id = rp.product_id
|
||||
ORDER BY
|
||||
rp.product_revenue
|
||||
LIMIT 3;
|
||||
-- Сначала удобнее считать агрегаты, чтобы код не выгляжел грооздким и чтобы в нем было легче разобраться
|
||||
-- Отличае в удобстве чтения и написания. И этот агрегат можно потом ещё и переиспользовать
|
||||
|
||||
|
||||
-- Задание 6
|
||||
WITH revenue_user AS (
|
||||
SELECT
|
||||
o.user_id,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS total_revenue
|
||||
FROM
|
||||
mnoskov.order_items oi
|
||||
JOIN
|
||||
mnoskov.orders o ON oi.order_id = o.id
|
||||
GROUP BY
|
||||
o.user_id
|
||||
)
|
||||
SELECT
|
||||
u.name,
|
||||
u.email,
|
||||
ru.total_revenue
|
||||
FROM
|
||||
mnoskov.users u
|
||||
JOIN
|
||||
revenue_user ru ON u.id = ru.user_id
|
||||
WHERE
|
||||
u.is_active = TRUE AND
|
||||
ru.total_revenue > 10000
|
||||
ORDER BY
|
||||
ru.total_revenue DESC, u.name;
|
||||
-- Я бы назвал этот сегмегт - frequent_buyers
|
||||
-- Таким пользователям надо предлагать какую ниюудь специальную программу лояльности
|
||||
|
||||
|
||||
-- Задание 7
|
||||
WITH revenue_order AS (
|
||||
SELECT
|
||||
order_id,
|
||||
sum(quantity * price_at_purchase) AS order_revenue
|
||||
FROM
|
||||
mnoskov.order_items
|
||||
GROUP BY
|
||||
order_id
|
||||
)
|
||||
SELECT
|
||||
ro.order_id,
|
||||
o.order_date::date,
|
||||
ro.order_revenue
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
JOIN
|
||||
revenue_order ro ON o.id = ro.order_id
|
||||
WHERE
|
||||
o.status = 'paid' AND
|
||||
ro.order_revenue < 10000
|
||||
ORDER BY
|
||||
ro.order_revenue;
|
||||
-- Причины: злоупотребление промокодами, ошибки
|
||||
-- По 'paid' удобнее фильтровать снаржуи, потому тут мы подключаем orders, а подключать orders ещё и внутри было бы бессмысленно
|
||||
@@ -0,0 +1,168 @@
|
||||
-- Задание 1
|
||||
SELECT
|
||||
o.user_id,
|
||||
u.name AS user_name,
|
||||
o.id AS order_id,
|
||||
o.order_date::date,
|
||||
row_number() OVER (PARTITION BY o.user_id ORDER BY o.order_date) AS order_number_for_user
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
JOIN
|
||||
mnoskov.users u ON o.user_id = u.id;
|
||||
-- Отличае от обычного order by в том, что мы не просто сортируем, а сортируем в пределах одного пользователя, и ещё выводим нумерацию заказов
|
||||
|
||||
|
||||
-- Задание 2
|
||||
WITH order_total_revenue AS (
|
||||
SELECT
|
||||
o.user_id,
|
||||
oi.order_id,
|
||||
o.order_date::date,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS order_revenue
|
||||
FROM
|
||||
mnoskov.order_items oi
|
||||
JOIN
|
||||
mnoskov.orders o ON oi.order_id = o.id
|
||||
GROUP BY
|
||||
o.user_id, oi.order_id, o.order_date
|
||||
)
|
||||
SELECT
|
||||
otr.user_id,
|
||||
u.name AS user_name,
|
||||
otr.order_id,
|
||||
otr.order_date::date,
|
||||
otr.order_revenue,
|
||||
sum(otr.order_revenue) OVER (PARTITION BY otr.user_id ORDER BY otr.order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_revenue_for_user
|
||||
FROM
|
||||
mnoskov.users u
|
||||
JOIN
|
||||
order_total_revenue otr ON u.id = otr.user_id;
|
||||
-- Если не задвать рамку, то могут пояиться проблемы с дубликатами
|
||||
-- Если накопительная выручка пользователя становится больше, значит он начинает тратить больше
|
||||
|
||||
|
||||
-- Задание 3
|
||||
WITH user_total_revenue AS (
|
||||
SELECT
|
||||
o.user_id,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS total_revenue
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
JOIN
|
||||
mnoskov.order_items oi ON o.id = oi.order_id
|
||||
GROUP BY
|
||||
o.user_id
|
||||
)
|
||||
SELECT
|
||||
u.name AS user_name,
|
||||
utr.total_revenue,
|
||||
rank() OVER (ORDER BY utr.total_revenue) AS revenue_rank,
|
||||
dense_rank() OVER (ORDER BY utr.total_revenue) AS revenue_dense_rank
|
||||
FROM
|
||||
mnoskov.users u
|
||||
JOIN
|
||||
user_total_revenue utr ON u.id = utr.user_id;
|
||||
-- Когда появляется одинаковая выручка, dense_rank нумерует по порядку, без пропусков(1,2,2,3), а rank по 'расположению'(1,2,2,4)
|
||||
|
||||
|
||||
-- Задание 4
|
||||
SELECT
|
||||
u.name AS user_name,
|
||||
o.id AS order_id,
|
||||
o.order_date::date,
|
||||
first_value(o.order_date::date) OVER (PARTITION BY u.id ORDER BY o.order_date) AS first_order_date,
|
||||
last_value(o.order_date::date) OVER (PARTITION BY u.id ORDER BY o.order_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_order_date
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
JOIN
|
||||
mnoskov.users u ON o.user_id = u.id;
|
||||
-- Первый заказ можно использовать для отчета 'как давно клиент начал активничать', а последний заказ, например если давно был последний заказ, то кинуть рассылку с промокодом, чтобы клиент снова начала заказывать как вариант
|
||||
-- Если не задать рамку, то дата последнего заказа будет равна текущей строке, а не последней
|
||||
|
||||
|
||||
-- Задание 5
|
||||
SELECT
|
||||
u.name AS user_name,
|
||||
o.id AS order_id,
|
||||
count(*) OVER (PARTITION BY u.id) AS orders_per_user
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
JOIN
|
||||
mnoskov.users u ON o.user_id = u.id;
|
||||
-- Если использовать просто group by, то пользователи сожмутся в одну ячейку
|
||||
|
||||
|
||||
-- Задание 6
|
||||
WITH order_total_revenue AS (
|
||||
SELECT
|
||||
o.user_id,
|
||||
oi.order_id,
|
||||
o.order_date::date,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS order_revenue
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
JOIN
|
||||
mnoskov.order_items oi ON o.id = oi.order_id
|
||||
GROUP BY
|
||||
o.user_id, oi.order_id, o.order_date
|
||||
)
|
||||
SELECT
|
||||
u.name AS user_name,
|
||||
otr.order_id,
|
||||
otr.order_date::date,
|
||||
otr.order_revenue,
|
||||
lag(otr.order_revenue) OVER (PARTITION BY u.id ORDER BY otr.order_date) AS prev_order_revenue
|
||||
FROM
|
||||
mnoskov.users u
|
||||
JOIN
|
||||
order_total_revenue otr ON u.id = otr.user_id;
|
||||
-- если текущее значение среднего чека больше чем предидущие, то увеличивается, а если наоборот, то уменьшается
|
||||
|
||||
|
||||
-- Задание 7
|
||||
-- По месяцам:
|
||||
WITH category_total_revenue AS (
|
||||
SELECT
|
||||
p.category,
|
||||
date_trunc('month', o.order_date)::date AS order_date_month,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS monthly_category_revenue
|
||||
FROM
|
||||
mnoskov.products p
|
||||
JOIN
|
||||
mnoskov.order_items oi ON p.id = oi.product_id
|
||||
JOIN
|
||||
mnoskov.orders o ON oi.order_id = o.id
|
||||
GROUP BY
|
||||
p.category, order_date_month
|
||||
)
|
||||
SELECT
|
||||
category,
|
||||
order_date_month,
|
||||
monthly_category_revenue,
|
||||
sum(monthly_category_revenue) OVER (PARTITION BY category ORDER BY order_date_month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_category_revenue
|
||||
FROM
|
||||
category_total_revenue;
|
||||
|
||||
-- По дням:
|
||||
WITH category_total_revenue AS (
|
||||
SELECT
|
||||
p.category,
|
||||
o.order_date::date AS order_date,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS daily_category_revenue
|
||||
FROM
|
||||
mnoskov.products p
|
||||
JOIN
|
||||
mnoskov.order_items oi ON p.id = oi.product_id
|
||||
JOIN
|
||||
mnoskov.orders o ON oi.order_id = o.id
|
||||
GROUP BY
|
||||
p.category, order_date
|
||||
)
|
||||
SELECT
|
||||
category,
|
||||
order_date,
|
||||
daily_category_revenue,
|
||||
sum(daily_category_revenue) OVER (PARTITION BY category ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_category_revenue
|
||||
FROM
|
||||
category_total_revenue;
|
||||
-- По такому графику мы можем видеть РОСТ выручки по дням например, а не просто сколько в какой день принесла категория
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
-- Задание 1
|
||||
CREATE MATERIALIZED VIEW mv_category_revenue AS
|
||||
SELECT
|
||||
p.category,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS total_revenue
|
||||
FROM
|
||||
mnoskov.products p
|
||||
JOIN
|
||||
mnoskov.order_items oi ON p.id = oi.product_id
|
||||
JOIN
|
||||
mnoskov.orders o ON oi.order_id = o.id
|
||||
WHERE o.status = 'paid'
|
||||
GROUP BY
|
||||
p.category
|
||||
ORDER BY
|
||||
total_revenue desc;
|
||||
-- удобнее иметь wiev, чтобы небыло дублирования кода, и чтобы легко обращаться к запросу не переписывая все заново
|
||||
|
||||
|
||||
-- Задание 2
|
||||
CREATE MATERIALIZED VIEW mv_user_activity AS
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.name AS user_name,
|
||||
count(*) AS orders_count
|
||||
FROM
|
||||
mnoskov.users u
|
||||
JOIN
|
||||
mnoskov.orders o ON u.id = o.user_id
|
||||
WHERE
|
||||
o.status = 'paid'
|
||||
GROUP BY
|
||||
u.id, u.name
|
||||
HAVING
|
||||
count(*) > 0
|
||||
ORDER BY
|
||||
orders_count desc;
|
||||
-- Чем больше у пользователя заказов тем он более 'лояльный'
|
||||
-- Тогда будет отображаться статистика только за последний месяц - 30 дней
|
||||
|
||||
|
||||
-- Задание 3
|
||||
CREATE MATERIALIZED VIEW mv_daily_revenue AS
|
||||
SELECT
|
||||
o.order_date::date AS sales_date,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS daily_revenue
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
JOIN
|
||||
mnoskov.order_items oi ON o.id = oi.order_id
|
||||
WHERE
|
||||
o.status = 'paid'
|
||||
GROUP BY
|
||||
o.order_date
|
||||
ORDER BY
|
||||
sales_date;
|
||||
-- Можно построить график, и видеть пики с просадками
|
||||
-- Быстрее считать выручку из mv_daily_revenue, тк витрина уже готова, и её не нужно высчитывать
|
||||
|
||||
|
||||
-- Задание 4
|
||||
REFRESH MATERIALIZED VIEW mv_daily_revenue;
|
||||
-- 1. интернет‑магазин с несколькими заказами в день - каждые сутки в 8 утра например
|
||||
-- 2. крупный магазин с тысячами заказов в час - каждые 30 минут
|
||||
-- Слишком редкий REFRESH сделает витрину бесполезной для аналитиков
|
||||
-- Обновлять каждую секунжу нельзя, тк это тратит ресурсы компьютера, и тогда уж проще просто VIEW сделать, и вызывать когда надо
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
-- Задание 1. Оформление закзаза
|
||||
INSERT INTO mnoskov.orders (id, user_id, order_date, status) VALUES
|
||||
(9, 10, '2026-03-13', 'paid'),
|
||||
(10, 10, '2026-03-14', 'paid');
|
||||
|
||||
INSERT INTO mnoskov.order_items (order_id, product_id, quantity, price_at_purchase) VALUES
|
||||
(9, 3, 2, 88000),
|
||||
(10, 6, 1, 5900);
|
||||
|
||||
UPDATE mnoskov.products
|
||||
SET in_stock = FALSE
|
||||
WHERE id IN(3, 6);
|
||||
-- Если выполнятся не все шаги, то транзакция откатится и все останется как было
|
||||
-- Потому что заказ если оформляется, то полностью, частично он выполняться не должен
|
||||
|
||||
|
||||
-- Задание 2
|
||||
-- При таком сценарии все должно откатаиться к самому началу, ничего не сохранится
|
||||
-- Если не делать rollback, то можно получить заказы без продуктов, или без стомости, или полностью оформленный заказ, но продуктов нет в наличии
|
||||
-- Любые 'битые' заказы повлияют на отчеты и витрины
|
||||
|
||||
|
||||
-- Задание 3
|
||||
-- При изменении цены товара в products, это действие могут начать выполнять два человека одновременное, и если это не будет транзакцией то получится непонятно что, а если как транзакция, то сначала запишется 1 результат, а потом 2, и не будет никаких ошибок
|
||||
-- Если выполняетя как транзакция, то просто откатится назад и все будет супер
|
||||
-- Тк база сама откатывает транзакции в случае надобности, это освобождает инженера от доли ответственности
|
||||
@@ -0,0 +1,21 @@
|
||||
-- Задание 1
|
||||
SELECT * FROM mnoskov.users WHERE id = 1;
|
||||
-- Если две одновременные сессии выполнят этот запрос, то в обеих все выведется, и ошибок или очереди не бкдет
|
||||
-- Потому что этот запрос только на чтение
|
||||
|
||||
|
||||
-- Задание 2
|
||||
-- Первая сессия будет просто ждать окончания выполнения первой
|
||||
-- Если разрешить обоимUPDATE пройти одновременно, то получится какая-то херня пологаю, они смешаются, или че-то такое
|
||||
-- У Alice в итоге останется второй email, тк он выполнится последним. Порядок COMMIT важен, тк в последствии этого порядка будут выполняться запросы
|
||||
|
||||
|
||||
-- Задание 3
|
||||
-- select выполнится тк просто читает, а update встанет в очередь, тк он попытается изменить 'забронированные данные'
|
||||
-- FOR UPDATE помогает избежать ошибок когда происходит покупка товара или че то такое, когда идет процесс изменений в данных
|
||||
|
||||
|
||||
-- Задание 4
|
||||
-- Дедлок возникает потому что каждая сессия заблокировал то, что нужно другой сессии. База отменит одну из сессий, чтобы прошла хоть какая-то
|
||||
-- Чтобы дедлок не возникал, нужно обновления все производить в одном порядке
|
||||
-- Если при массовых обновлениях не соблюдать порядок строк, то будет возникать жопа из дедлоков
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
-- Задание 1
|
||||
-- Вариант А
|
||||
EXPLAIN ANALYZE
|
||||
SELECT name, age
|
||||
FROM mnoskov.users
|
||||
WHERE country = 'RU' AND age > 25;
|
||||
|
||||
-- Вариант Б
|
||||
EXPLAIN ANALYZE
|
||||
SELECT name, age
|
||||
FROM mnoskov.users
|
||||
WHERE age > 25 AND country = 'RU';
|
||||
-- Выводят они один и тот же результат, потому что условия у них одинаковые
|
||||
-- По скорости выполнения судя по планировщику тоже одинаковоы
|
||||
|
||||
|
||||
-- Задание 2
|
||||
-- Вариант 1
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
u.name,
|
||||
u.email,
|
||||
o.order_date::date,
|
||||
o.status
|
||||
FROM
|
||||
mnoskov.users u
|
||||
JOIN
|
||||
mnoskov.orders o ON u.id = o.user_id
|
||||
WHERE o.status = 'refunded';
|
||||
|
||||
-- Вариант 2
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
u.name,
|
||||
u.email,
|
||||
o.order_date::date,
|
||||
o.status
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
JOIN
|
||||
mnoskov.users u ON o.user_id = u.id
|
||||
WHERE o.status = 'refunded';
|
||||
-- Между первой и второй таблицей результаты одинаковые. Если в запросе нет никаких условий связанных с таблицами, то первой оптимизатор смотреть users, но если если условие по orders.status, то сначала он сканирует orders
|
||||
|
||||
|
||||
-- Задание 3
|
||||
EXPLAIN ANALYZE
|
||||
SELECT name, email
|
||||
FROM mnoskov.users
|
||||
WHERE country = 'RU';
|
||||
-- Тип сканирования: Seq Scan
|
||||
-- Количество прочитанных строк: 1
|
||||
-- Время выполнения примерно: 0.023 ms(+-5)
|
||||
|
||||
|
||||
-- Задание 4
|
||||
-- Запрос А
|
||||
EXPLAIN ANALYZE
|
||||
SELECT *
|
||||
FROM mnoskov.users
|
||||
WHERE country = 'RU';
|
||||
|
||||
-- Запрос Б
|
||||
EXPLAIN ANALYZE
|
||||
SELECT name, email
|
||||
FROM mnoskov.users
|
||||
WHERE country = 'RU';
|
||||
-- Поскольку данных в таблице мало у меня время выполнения одинаковое, но могу предположить, что второй выполнился быстрее, тк у него в селекте два поля, а не вся таблица
|
||||
|
||||
|
||||
-- Задание 5
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
u.name, o.id, o.order_date
|
||||
FROM
|
||||
mnoskov.users u
|
||||
JOIN
|
||||
mnoskov.orders o ON u.id = o.user_id
|
||||
WHERE
|
||||
u.country = 'RU';
|
||||
-- Читать начала с users, тк мы в where пишем условие для users, и быстрее сначала отсеять ненужных юзеров
|
||||
|
||||
|
||||
-- Задание 6
|
||||
SELECT u.name, COUNT(o.id) AS orders_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.country = 'Россия'
|
||||
AND LOWER(u.name) LIKE '%алекс%'
|
||||
GROUP BY u.name;
|
||||
-- Его замедляет LOWER(не надо использовать функции), и думаю что LIKE(лучше использовать просто =), и ещэ group by по имени - это дольше чем по первичному ключу(надо группировать по ПК)
|
||||
|
||||
|
||||
-- Задание 7
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, p.name AS product_name, oi.quantity
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE u.country = 'RU';
|
||||
-- users -> orders -> order_items -> products - читает в таком порядке, потому что в where условие по users
|
||||
-- Если убрать where, то будет - order_items -> users -> orders -> products
|
||||
-- Как ускорить не знаю, может индексов добавть, но это пока не проходил
|
||||
|
||||
-- Задание 8
|
||||
-- Вариант А
|
||||
EXPLAIN ANALYZE
|
||||
SELECT name, price
|
||||
FROM mnoskov.products
|
||||
WHERE category = 'Электроника'
|
||||
AND price > 50000;
|
||||
|
||||
-- Вариант Б
|
||||
EXPLAIN ANALYZE
|
||||
SELECT *
|
||||
FROM mnoskov.products
|
||||
WHERE category = 'Электроника'
|
||||
AND price > 50000;
|
||||
-- Данных в таблицах мало, врем выполнения не оценить, но смотря просто со стороны понятно что выриант А будет быстрее, потому что у него пару колонок в селекте, а не вся таблица как в Б
|
||||
|
||||
|
||||
-- Задание 9
|
||||
-- Вариант коллег
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE EXTRACT(YEAR FROM created_at) = 2026
|
||||
AND LOWER(country) = 'россия'
|
||||
AND age + 10 > 35;
|
||||
|
||||
-- Мой вариант
|
||||
SELECT
|
||||
id, name, age, is_active
|
||||
FROM
|
||||
mnoskov.users
|
||||
WHERE
|
||||
created_at BETWEEN '2026-01-01' AND '2027-01-02' AND
|
||||
country = 'RU' AND
|
||||
age > 45;
|
||||
-- Мой вариант лучше потому что:
|
||||
-- нет функций и мат. вычислений в where, все приведено к самому легкочитаемому формату
|
||||
-- нет звездочки в select, а только то, что нужно
|
||||
|
||||
|
||||
-- Задание 10
|
||||
INSERT INTO users (id, name, age, email, country, created_at)
|
||||
SELECT
|
||||
g AS id,
|
||||
'user_' || g AS name,
|
||||
27 AS age,
|
||||
'user_' || g || '@example.com' AS email,
|
||||
'RU' AS country,
|
||||
'2026-02-03' AS created_at
|
||||
FROM generate_series(20001, 50000) AS g; -- GPT сделал запрос мне
|
||||
|
||||
SELECT * FROM mnoskov.users WHERE age IS NOT NULL;
|
||||
|
||||
-- Запрос 1
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM users WHERE age > 25;
|
||||
|
||||
-- Запрос 2
|
||||
EXPLAIN ANALYZE
|
||||
SELECT name, email FROM users WHERE age > 25;
|
||||
-- Ну чето все равно не видна разница во времени выполнения, но по логике должен быть быстрее второй запрос
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
-- Задание 1
|
||||
EXPLAIN
|
||||
SELECT
|
||||
name, email
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
id = 1;
|
||||
-- Использует index scan
|
||||
-- Index Cond: (id = 1) - это значит что условие id = 1, и оно выполняется по индексу, и что id - индекс
|
||||
-- База использует индекс, потому что у нас по полю id создан индекс, и база выбирает наиболее быстрый пусть ввыполнения запроса - через индекс
|
||||
|
||||
|
||||
-- Задание 2
|
||||
-- Запрос А
|
||||
EXPLAIN
|
||||
SELECT * FROM users WHERE id = 2;
|
||||
|
||||
-- Запрос Б
|
||||
EXPLAIN
|
||||
SELECT * FROM users WHERE age > 25;
|
||||
-- В первом запрос index scan, потому что у нас уловие по индексу - id, во втором запросы seq scan, потому что у поля age не индексов, и таблица сканируется медленее
|
||||
|
||||
|
||||
-- Задание 3
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
name, price
|
||||
FROM
|
||||
products
|
||||
WHERE
|
||||
category = 'Электроника';
|
||||
-- Execution Time: 0.020 ms
|
||||
-- Тип сканирования: Seq Scan
|
||||
-- Ожидалось вернуть 1, а вернул 3
|
||||
|
||||
|
||||
-- Задание 4
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM users WHERE country = 'RU';
|
||||
-- Без индекса - Execution Time: 6.769 ms
|
||||
CREATE INDEX idx_country ON users(country);
|
||||
-- С индексом - Execution Time: 2.076 ms
|
||||
-- Сначала идет Bitmap Heap Scan on users - как я понял на этом этапе 'создается' таблица где country = 'RU', а потом Bitmap INdex Scan on idx_country - тут уже мы читам информацию из таблицы где все country = 'RU'.
|
||||
-- Запрос стал быстрее примерно в три раза
|
||||
-- Индекс помог, потому что база искала не среди всех данных, а среди 'проиндексированных'
|
||||
|
||||
|
||||
-- Задание 5
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
u.name, o.order_date, o.status
|
||||
FROM
|
||||
users u
|
||||
JOIN
|
||||
orders o ON u.id = o.user_id
|
||||
WHERE
|
||||
u.country = 'RU';
|
||||
-- База исполщовала Nested Loop
|
||||
-- Чтение началось с orders
|
||||
-- Execution Time: 0.075 ms
|
||||
|
||||
|
||||
-- Задание 6
|
||||
CREATE INDEX idx_email ON users(email);
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
lower(email) = 'user_28@example.com';
|
||||
-- База не использует индекс пока в where есть функция lower
|
||||
-- Потому что в условии функция, а с ними индексы не в ладах как я понял
|
||||
-- Чтобы индекс заработал, надо убрать функцию lower, и сравнивать email как есть
|
||||
-- Если убрать lower, то Execution Time намного быстрее, прям очень намного быстрее
|
||||
|
||||
|
||||
-- Задание 7
|
||||
CREATE INDEX idx_age ON users(age);
|
||||
-- Запрос А
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM users WHERE age + 5 > 30;
|
||||
|
||||
-- Запрос Б
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM users WHERE age > 25;
|
||||
-- Быстрее работает запрос Б, но че то он тоже не хочет индекс использовать, хотя я его создал
|
||||
-- Первый запрос не может использовать индекс, потому что там в условии математическое вычисление, а почему второй запрос не использует индек - я хз
|
||||
-- Чтобы исправить запрос надо убрать сложение, и просто написать age > 25, как в запросе Б
|
||||
|
||||
|
||||
-- Задание 8
|
||||
CREATE INDEX idx_created_at ON users(created_at);
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, COUNT(o.id) AS orders_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE EXTRACT(YEAR FROM u.created_at) = 2026
|
||||
GROUP BY u.name;
|
||||
-- Функция EXTRACT - это плохая идея, потому что это функция которой быть в where по возможности не должно
|
||||
-- WHERE i.crated_at BETWEEN '2026-01-01' AND '2027-01-01'
|
||||
-- Индекс не используется в текущеа виде
|
||||
|
||||
|
||||
-- Задание 9
|
||||
-- Запрос А
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM products WHERE price > 50000;
|
||||
|
||||
-- Запрос Б
|
||||
EXPLAIN ANALYZE
|
||||
SELECT name, price FROM products WHERE price > 50000;
|
||||
-- Если будет много данных в таблице, то быстрее будет работать второй запрос
|
||||
-- Второй эффективнее тк в селекте указаны определнные поля, а не все(*)
|
||||
-- Разница будет особо замета при больших объемах данных
|
||||
|
||||
|
||||
-- Задание 10
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, p.name AS product_name, oi.quantity
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE u.country = 'RU' AND p.category = 'Электроника';
|
||||
-- Порядок чтения таблиц: products -> order_items -> orders -> users
|
||||
-- Количество строк: products - 3, order_items - 12, orders - 1, users - 0
|
||||
-- Если у country и category будут индексы, то запрос ьудет выполняться побыстре, потому что будет Index Scan, а не Seq Scan
|
||||
|
||||
|
||||
-- Задание 11
|
||||
-- До
|
||||
EXPLAIN ANALYZE
|
||||
SELECT *
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE LOWER(u.name) LIKE '%иван%'
|
||||
AND EXTRACT(MONTH FROM o.order_date) = 1
|
||||
AND u.age + 10 > 35;
|
||||
-- Execution Time: 0.219 ms
|
||||
|
||||
-- После
|
||||
EXPLAIN ANALYZE
|
||||
SELECT *
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.name = 'иван' AND
|
||||
o.order_date BETWEEN '2024-01-01' AND '2024-02-01' AND
|
||||
O.order_date BETWEEN '2026-01-01' AND '2026-02-01' AND
|
||||
u.age > 25;
|
||||
-- Execution Time: 0.032 ms
|
||||
-- Мой вариант лучще, потому что у меня нет функций и вычисление мешаюзих использовать индексы в условии, и оптимизатор может успешно использовать индексы
|
||||
|
||||
|
||||
-- Задание 12
|
||||
-- Запрос 1
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
o.user_id,
|
||||
o.id AS order_id
|
||||
FROM
|
||||
users u
|
||||
JOIN
|
||||
orders o ON u.id = o.user_id AND u.country = 'RU';
|
||||
|
||||
-- Запрос 2
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
o.user_id,
|
||||
o.id AS order_id
|
||||
FROM
|
||||
orders o
|
||||
WHERE EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
users u
|
||||
WHERE
|
||||
o.user_id = u.id AND
|
||||
u.country = 'RU'
|
||||
);
|
||||
|
||||
-- Запрос 3
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
o.user_id,
|
||||
o.id AS order_id
|
||||
FROM
|
||||
orders o
|
||||
WHERE o.user_id IN (
|
||||
SELECT
|
||||
u.id
|
||||
FROM
|
||||
users u
|
||||
WHERE
|
||||
u.country = 'RU'
|
||||
);
|
||||
|
||||
-- Запрос 1: Execution Time: 0.055 ms
|
||||
-- Запрос 2: Execution Time: 0.051 ms
|
||||
-- Запрос 3: Execution Time: 0.073 ms
|
||||
-- Они все дют примерно одинаковый разультат, потому что мало данных в таблице. А планы выполнения у них у всех одинаковые, вот прям 1 в 1. Ну по логие 3 запрос самый медленный, а первые два шустрые
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
-- Задание 1
|
||||
EXPLAIN ANALYZE
|
||||
SELECT country, count(*) AS users_count
|
||||
FROM users
|
||||
WHERE is_active = TRUE
|
||||
GROUP BY country
|
||||
HAVING count(*) > 1
|
||||
ORDER BY users_count DESC;
|
||||
-- WHERE применится до GROUP BY
|
||||
-- HAVING применится после GROUP BY
|
||||
-- Использовать users_count в HAVING нельзя, а в ORDER BY можно
|
||||
|
||||
|
||||
-- Задание 2
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date, o.status
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.country = 'RU' AND o.status = 'paid'
|
||||
ORDER BY o.order_date DESC;
|
||||
-- Порядок выполнения по QUERY PLAN:
|
||||
-- 1) сначала сканирутся таблица orders и выполняется фильтрация o.status = 'paid',
|
||||
-- 2) потом сканируется таблица users по условию u.id = o.user_id, и выполняется вильтрация u.country = 'RU',
|
||||
-- 3) далее Nested Loop соединяет orders и users, которые уже прошли фильтры,
|
||||
-- 4) теперь выводится выполняется сортирвка o.order_date DESC
|
||||
|
||||
-- Порядок выполнения по логике:
|
||||
-- 1) сначала сканируем таблицу users и присоединяем orders по условию u.id = o.user_id
|
||||
-- 2) потом фильтруем соединенные таблицы по u.country = 'RU' и o.status = 'paid'
|
||||
-- 3) в SELECT выбираем поля которые хотим вывести - u.name, o.order_date, o.status
|
||||
-- 4) сортируем выбранные поля по o.order_date по убыванию
|
||||
|
||||
|
||||
-- Задание 3
|
||||
-- Запрос А
|
||||
SELECT country, AVG(age) AS avg_age
|
||||
FROM users
|
||||
GROUP BY country
|
||||
HAVING country = 'Россия';
|
||||
-- Работает медленно потому having фильтрет после группировки а не до
|
||||
|
||||
-- Запрос Б
|
||||
SELECT country, avg(age) AS avg_age
|
||||
FROM users
|
||||
WHERE country = 'RU'
|
||||
GROUP BY country;
|
||||
-- Этот вариант быстрее, тк он фильтрует строки до группировки, те группируется меньше строк а не все
|
||||
|
||||
|
||||
-- Задание 4
|
||||
-- Запрос А
|
||||
EXPLAIN ANALYZE
|
||||
SELECT category, AVG(price) AS avg_price
|
||||
FROM products
|
||||
GROUP BY category
|
||||
HAVING category = 'Электроника' AND AVG(price) > 20000;
|
||||
|
||||
-- Запрос Б
|
||||
EXPLAIN ANALYZE
|
||||
SELECT category, AVG(price) AS avg_price
|
||||
FROM products
|
||||
WHERE category = 'Электроника'
|
||||
GROUP BY category
|
||||
HAVING AVG(price) > 20000;
|
||||
-- Количество обработанных строк у запроса А: 4
|
||||
-- Количество обработанных строк у запроса Б: 4
|
||||
-- PostgreSQL походу оптимизирует даже хуйню написанную в запросе А, планы запросов А и Б одинаковые - 1 в 1, хотя по сути у запроса А должно быть больше строк
|
||||
|
||||
|
||||
-- Задание 5
|
||||
-- Изначальный запрос
|
||||
SELECT
|
||||
country,
|
||||
COUNT(*) AS users_count
|
||||
FROM users
|
||||
WHERE users_count > 1
|
||||
GROUP BY country
|
||||
ORDER BY country;
|
||||
|
||||
-- Исправленный запрос
|
||||
SELECT
|
||||
country,
|
||||
COUNT(*) AS users_count
|
||||
FROM users
|
||||
GROUP BY country
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY country;
|
||||
|
||||
-- Задание 6
|
||||
SELECT
|
||||
u.country,
|
||||
COUNT(o.id) AS orders_count,
|
||||
AVG(o.id) AS avg_order_id
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.is_active = true
|
||||
GROUP BY u.country
|
||||
HAVING COUNT(o.id) > 1
|
||||
ORDER BY orders_count DESC
|
||||
LIMIT 3;
|
||||
-- Полный порядок выполнения:
|
||||
-- 1) FROM/JOIN - сканируется таблица users, и к ней присоединяется таблица orders по условию u.id = o.user_id
|
||||
-- 2) WHERE - из двух соединенных таблиц фильтруются и остаются только строки где u.is_active = true
|
||||
-- 3) GROUP BY - происходится группировка по u.country, те все строки с одной и той же страной(записью в столбце) сжимаются в одну
|
||||
-- 4) HAVING - уже по сгруппированным строкам проводится фильтрация по COUNT(o.id) > 1, остются только те страны(строки) где кол-во пользователей больше одного
|
||||
-- 5) SELECT - выбираются столбцы которые будут выведены: u.country, COUNT(o.id) AS orders_count, AVG(o.id) AS avg_order_id
|
||||
-- 6) ORDER BY - выбранные в SELECT строки сортируются по orders_count по убыванию
|
||||
-- 7) LIMIT - из отсортированных строк остаются только первые 3
|
||||
|
||||
|
||||
-- Задание 7
|
||||
-- Запрос А
|
||||
EXPLAIN ANALYZE
|
||||
SELECT category, COUNT(*)
|
||||
FROM products
|
||||
GROUP BY category
|
||||
HAVING category = 'Электроника';
|
||||
|
||||
-- Запрос Б
|
||||
EXPLAIN ANALYZE
|
||||
SELECT category, COUNT(*)
|
||||
FROM products
|
||||
WHERE category = 'Электроника'
|
||||
GROUP BY category;
|
||||
-- по логике запрос Б должен быть быстрее тк он сначала филтрует строки а потом группирует уже отфильтрованное,
|
||||
-- НО PostgreSql может оптимизровать и запрос А, и получается что выполняются оба запроса одинаково, сначала фильтрация а потом группировка
|
||||
-- В обоих запросах на шаге 'Seq Scan on products' обрабатываются: 3 строки, а на 'GroupAggregate': 1 строка
|
||||
-- Execution Time одно и тоже
|
||||
|
||||
|
||||
-- Задание 8
|
||||
SELECT
|
||||
u.name,
|
||||
COUNT(o.id) AS orders_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.country = 'Россия'
|
||||
AND u.age > 25
|
||||
AND o.status = 'completed'
|
||||
GROUP BY u.name
|
||||
HAVING COUNT(o.id) >= 2
|
||||
ORDER BY orders_count DESC;
|
||||
-- WHERE применяется между FROM/JOIN и GROUP BY
|
||||
-- HAVING применяется между GROUP BY и SELECT
|
||||
-- фильтрация по o.status в WHERE происходит группировки строк, а фильтрация по COUNT(o.id) в HAVING происходит уже после группирациюю, по самим сгруппированным строкам
|
||||
|
||||
|
||||
-- Задание 9
|
||||
SELECT
|
||||
u.name,
|
||||
p.name AS product_name,
|
||||
p.price
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE u.country = 'Россия'
|
||||
ORDER BY p.price DESC
|
||||
LIMIT 5;
|
||||
-- ORDER BY применяется после SELECT, перед LIMIT/OFFSET(если есть)
|
||||
-- LIMIT идет после GROUP BY, тк сначала нужно отфильтровать строки чтобы взять их кусок(5 строк)
|
||||
-- если LIMIT(5) выполнить до сортировки, то это будет бесполезно, возьмутся просто первые 5 строк, и уже они будут сортироваться, и по итогу выйдет хуйня
|
||||
|
||||
|
||||
-- Задание 10
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
category,
|
||||
count(id) AS count_product_in_category,
|
||||
avg(price) AS avg_category_price
|
||||
FROM
|
||||
products
|
||||
GROUP BY
|
||||
category
|
||||
HAVING
|
||||
count(id) >= 2 OR avg(price) > 15000
|
||||
ORDER BY
|
||||
avg_category_price DESC
|
||||
LIMIT 3;
|
||||
-- 1) FROM - сканируем poducts
|
||||
-- 2) GROUP BY - группируем по category
|
||||
-- 3) HAVING - фильтруем по агрегированной функции count(id)
|
||||
-- 4) SELECT - делаем выборку по столбцам category, count_product_in_category, avg_category_price
|
||||
-- 5) ORDER BY - сортируем по avg_category_price по убыванию
|
||||
-- 6) LIMIT - выбираем первые 3 строки из отсортированных
|
||||
-- Ипсользовал HAVING тк по условию все фильтрации по агрегированным функциям
|
||||
-- Выполнил с EXPLAIN ANALYZE, сначала читаетя porducts, потом делается фильтрации по count(id) >= 2 и avg(price) > 15000, далее sort, а потом Limit
|
||||
|
||||
|
||||
-- Задание 11
|
||||
-- Запрос А
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
u.name,
|
||||
u.age,
|
||||
(SELECT COUNT(*)
|
||||
FROM orders o
|
||||
WHERE o.user_id = u.id) AS orders_count
|
||||
FROM users u
|
||||
WHERE u.country = 'RU'
|
||||
ORDER BY orders_count DESC;
|
||||
-- Подзапрос в SELECT выполняется самым первым, до начала остального запроса, и называется в планировщие - SubPlan 1
|
||||
-- orders_count нельзя использовать в WHERE
|
||||
-- orders_count можно использовать в ORDER BY
|
||||
|
||||
-- Запрос Б(исправленный)
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
u.name,
|
||||
u.age,
|
||||
count(o.id) AS orders_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.country = 'RU'
|
||||
GROUP BY u.name, u.age
|
||||
ORDER BY orders_count DESC;
|
||||
-- Запрос Б(исправленный) - быстрее, тк подзапрос в SELECT делается для каждого пользователя заново, а JOIN всего один раз
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
-- Задание 1
|
||||
-- Ситуация -- Какой JOIN выберет база? -- Почему?
|
||||
--------------------------------------------------------------------------------------
|
||||
-- 10 пользователей, 1 млн заказов,| Nested | Маленькая таблица и большая с индексом. |
|
||||
-- есть индекс на orders.user_id | Loop | База будет искать совпадения из |
|
||||
-- | | маленькой по индексу из большой. |
|
||||
--------------------------------------------------------------------------------------
|
||||
-- 100 000 пользователей, | Hash | Таблицы среднего размера, нет индексов. |
|
||||
-- 100 000 заказов, нет индексов | Join | База построит хэш-таблицу одной из них, |
|
||||
-- | | и будет искать совпадения в другой. |
|
||||
--------------------------------------------------------------------------------------
|
||||
-- Обе таблицы отсортированы по | Merge | Потому что таблицы отсортированы по |
|
||||
-- ключу, есть индексы | Join | ключу. Будет происходить соединение - |
|
||||
-- | | 'Одновременное&последовательное'. |
|
||||
--------------------------------------------------------------------------------------
|
||||
-- Маленькая таблица (5 строк) | Nested | База будет искать совпадения из |
|
||||
-- соединяется с большой (1 млн), | Loop | маленькой в большой. Но тк нет индексов |
|
||||
-- нет индекса | | - это будет медленно. |
|
||||
-- |--------| Я засомневался и разобрал с ГПТ: |
|
||||
-- | Hash | Nested Loop будет дорого(без индексов - |
|
||||
-- | Join | 5.000.000 проврок), поэтому скорее всего|
|
||||
-- | | Hash Join, просто построится хэш-таблица|
|
||||
-- | | по маленькой и 5 раз отсканирует большую|
|
||||
--------------------------------------------------------------------------------------
|
||||
-- Я заебался строить эту таблицу из пробелов. Хуй когда решусь на это снова. |
|
||||
--------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
-- Задание 2
|
||||
-- Маленькая таблица (10 строк)
|
||||
CREATE TABLE mnoskov.small_users AS
|
||||
SELECT generate_series(1, 10) AS id, 'User ' || generate_series(1, 10) AS name;
|
||||
|
||||
-- Большая таблица (100 000 строк)
|
||||
CREATE TABLE mnoskov.big_orders AS
|
||||
SELECT
|
||||
generate_series(1, 100000) AS id,
|
||||
(random() * 9 + 1)::int AS user_id,
|
||||
CURRENT_DATE - (random() * 365)::int AS order_date;
|
||||
|
||||
-- Вариант 1: Без индекса
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, COUNT(o.id)
|
||||
FROM small_users u
|
||||
JOIN big_orders o ON u.id = o.user_id
|
||||
GROUP BY u.name;
|
||||
|
||||
-- Вариант 2: С индексом
|
||||
CREATE INDEX idx_big_orders_user_id ON big_orders(user_id);
|
||||
--DROP INDEX idx_big_orders_user_id;
|
||||
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, COUNT(o.id)
|
||||
FROM small_users u
|
||||
JOIN big_orders o ON u.id = o.user_id
|
||||
GROUP BY u.name;
|
||||
-- Вариант 1: Merge Join - Потому что обе таблицы отсортированы с самого создания
|
||||
-- Варинат 2: Тоже Merge Join
|
||||
-- База могла бы выбрать Nested Loop, потому что у нас одна таблица маленькая а другая большая и с индексом, но выбрала Merge Join, потому что таблицы отсортированы
|
||||
-- С индексом Execution Time: 31.579 ms, а без примерно 80.000 ms
|
||||
|
||||
|
||||
-- Задание 3
|
||||
-- Вариант 1: Без ORDER BY
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id;
|
||||
|
||||
-- Вариант 2: С ORDER BY по ключу соединения
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
ORDER BY u.id, o.user_id;
|
||||
|
||||
-- Вариант 3: С ORDER BY по другому полю
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
ORDER BY o.order_date;
|
||||
-- Во втором варианте также как и в первом - Nested Loop
|
||||
-- Merge Join не появился
|
||||
-- ORDER BY может повлиять на выбор JOIN, тк если есть стортировка, то база может выбрать Merge Join
|
||||
|
||||
|
||||
-- Задание 4
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
u.name,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE u.country = 'RU';
|
||||
-- (u.id = o.user_id) - Nested Loop; (o.id = oi.order_id) - Nested Loop; (oi.product_id = p.id) - Hash Join
|
||||
-- Seq Scan на этапах сканирования orders и products
|
||||
-- Думаю стоит создать индекс на orders.user_id; и надо было бы на users.countyr, но я создавал уже в предидущих укроках; подумал, что надо ещё products.id, но там id и так первичный ключ, значит и индекс есть
|
||||
CREATE INDEX idx_orders_user_id ON orders(user_id); -- DROP INDEX idx_orders_user_id;
|
||||
-- После создания индекса типы JOIN не поменялись, план остался тем же, скорость выполнения запроса та же
|
||||
|
||||
|
||||
-- Задание 5
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
u.name,
|
||||
COUNT(DISTINCT o.id) AS orders_count,
|
||||
COUNT(DISTINCT oi.product_id) AS products_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
LEFT JOIN order_items oi ON o.id = oi.order_id
|
||||
WHERE u.country = 'RU'
|
||||
GROUP BY u.name;
|
||||
-- на этапе LEFT JOIN order_items oi ON o.id = oi.order_id - оптимизатор выполнил Hash Right Join по условию oi.order_id = o.id, то есть Hash Join
|
||||
-- на этапе LEFT JOIN orders o ON u.id = o.user_id - оптимизатор выполнил Hash Left Join по условию u.id = o.user_id, то есть тоже Hash Join
|
||||
-- Метод по сути один и тот же - Hash Join, но отличаются Right и Left - я думаю это выбрано для лучшей оптимизации
|
||||
-- Через Hash Right Join проходит - 12 строк
|
||||
-- Через Hash Left Join проходит - 11281 строк
|
||||
|
||||
|
||||
-- Задание 6
|
||||
-- Вариант A: INNER JOIN
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
INNER JOIN orders o ON u.id = o.user_id;
|
||||
|
||||
-- Вариант B: LEFT JOIN
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id;
|
||||
-- Разный тип используется, в варианте А - Nested Loop, в варианте B - Hash Left Join
|
||||
-- Да, у варанта B Execution Time в очень много раз больше
|
||||
-- LEFT JOIN медленне, потому что он не использует индекс, а делает Seq Scan, те читает таблицы полностью, потому что подругому не получится выполнить LEFT JOIN
|
||||
|
||||
|
||||
-- Задание 7
|
||||
-- Вариант 1: users JOIN orders
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id;
|
||||
|
||||
-- Вариант 2: orders JOIN users
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM orders o
|
||||
JOIN users u ON o.user_id = u.id;
|
||||
-- Тип JOIN не изменился
|
||||
-- Порядок чтения таблиц не изменился
|
||||
-- Execution Time одинаковое
|
||||
-- Оптимизатор выбрал один и тот же порядок потому что по сути это два одинаковых запроса, с одной и той же логикой, но написаны чуть чуть по разному, и оптимизатор в обоих случаях выполняет так, как быстрее. Почему порядок может быть разным, я не предполагаю.
|
||||
|
||||
|
||||
-- Задание 8
|
||||
-- Запрос 1
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
orders o
|
||||
JOIN
|
||||
order_items oi ON o.id = oi.order_id
|
||||
ORDER BY
|
||||
o.id, oi.order_id;
|
||||
-- Уверен такой запрос привел бы к Merge Join, если бы данных в таблицах было побольше
|
||||
|
||||
-- Запрос 2(из задания 2)
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, COUNT(o.id)
|
||||
FROM small_users u
|
||||
JOIN big_orders o ON u.id = o.user_id
|
||||
GROUP BY u.name;
|
||||
-- Потому что обе таблицы уже отсортированы в момент создания
|
||||
|
||||
-- Задание 9
|
||||
-- Nested Loop лучше Hash Join, когда есть индексы по полям фильтрации, когда одна таблица небольшая, а у второй есть индекс. Подглядел: когда нужно найти немного совпадений.
|
||||
-- Hash Join лучше чем Nested Loop, когда нет индексов, когда таблицы среднего размера и больше, и когдау условие фильтрации '='
|
||||
|
||||
-- Оба запроса из задания 6:
|
||||
-- Вариант A: INNER JOIN
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
INNER JOIN orders o ON u.id = o.user_id;
|
||||
-- тут nested loop быстрее, тк соеденияется все по индексам
|
||||
|
||||
-- Вариант B: LEFT JOIN
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id;
|
||||
-- а тут быстрее merge join, тк по индексам соединить не получится, тут нужны обе полные таблицы, и создается по одной из них хэш-таблица
|
||||
|
||||
|
||||
-- Задание 10
|
||||
-- Запрос A
|
||||
EXPLAIN ANALYZE
|
||||
--CREATE MATERIALIZED VIEW mv_category_revenue AS
|
||||
SELECT
|
||||
p.category,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS total_revenue
|
||||
FROM
|
||||
mnoskov.products p
|
||||
JOIN
|
||||
mnoskov.order_items oi ON p.id = oi.product_id
|
||||
JOIN
|
||||
mnoskov.orders o ON oi.order_id = o.id
|
||||
WHERE o.status = 'paid'
|
||||
GROUP BY
|
||||
p.category
|
||||
ORDER BY
|
||||
total_revenue desc;
|
||||
-- До: Execution Time: 0.309 ms
|
||||
CREATE INDEX idx_orders_status ON orders(status);
|
||||
-- После добавления индекса ничего не поменялось, план запроса остался тот же, скорее всего потому что мало данных, думаю будь их больше с индексом запрос был бы быстрее
|
||||
|
||||
-- Запрос B
|
||||
EXPLAIN ANALYZE
|
||||
--CREATE MATERIALIZED VIEW mv_user_activity AS
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.name AS user_name,
|
||||
count(*) AS orders_count
|
||||
FROM
|
||||
mnoskov.users u
|
||||
JOIN
|
||||
mnoskov.orders o ON u.id = o.user_id
|
||||
WHERE
|
||||
o.status = 'paid'
|
||||
GROUP BY
|
||||
u.id, u.name
|
||||
HAVING
|
||||
count(*) > 0
|
||||
ORDER BY
|
||||
orders_count desc;
|
||||
-- И тут тоже несмотря на то что индекс на orders статус я уже создал, таблица orders скаринруется как Seq Scan, я думаю это потому, что мало данных, будь их больше с индексом запрос был бы быстрее
|
||||
|
||||
-- Запрос C
|
||||
EXPLAIN ANALYZE
|
||||
--CREATE MATERIALIZED VIEW mv_daily_revenue AS
|
||||
SELECT
|
||||
o.order_date::date AS sales_date,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS daily_revenue
|
||||
FROM
|
||||
mnoskov.orders o
|
||||
JOIN
|
||||
mnoskov.order_items oi ON o.id = oi.order_id
|
||||
WHERE
|
||||
o.status = 'paid'
|
||||
GROUP BY
|
||||
o.order_date
|
||||
ORDER BY
|
||||
sales_date;
|
||||
-- Тут ситуация аналогичная и для первый двух запросов, нужные индексы уже созданы, в связи выполнения всех предидущих занятий и тем, и единственное что я мог сделать для улучшения времени выполнения - это добавить индекс на orders(status)
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
--DROP TABLE IF EXISTS users;
|
||||
CREATE TABLE users (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name varchar(100) NOT NULL,
|
||||
age SMALLINT,
|
||||
email varchar(100),
|
||||
registration_date timestamptz NOT NULL
|
||||
);
|
||||
|
||||
--DROP TABLE IF EXISTS courses;
|
||||
CREATE TABLE courses (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
title varchar(150) NOT NULL,
|
||||
category varchar(50),
|
||||
created_at timestamptz NOT NULL
|
||||
);
|
||||
|
||||
--DROP TABLE IF EXISTS lessons;
|
||||
CREATE TABLE lessons (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
course_id bigint NOT NULL,
|
||||
title varchar(150) NOT NULL,
|
||||
duration_min SMALLINT NOT NULL,
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id)
|
||||
);
|
||||
|
||||
--DROP TABLE IF EXISTS enrollments;
|
||||
CREATE TABLE enrollments (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id bigint NOT NULL,
|
||||
course_id bigint NOT NULL,
|
||||
enrolled_at timestamptz NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id)
|
||||
);
|
||||
|
||||
--DROP TABLE IF EXISTS lesson_views;
|
||||
CREATE TABLE lesson_views (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id bigint NOT NULL,
|
||||
lesson_id bigint NOT NULL,
|
||||
viewed_at timestamptz NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (lesson_id) REFERENCES lessons(id)
|
||||
);
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
INSERT INTO users (name, age, email, registration_date) VALUES
|
||||
('Alice',25,'alice@mail.com','2023-01-10'),
|
||||
('Bob',30,'bob@gmail.com','2023-02-05'),
|
||||
('Charlie',22,'charlie@mail.com','2023-02-20'),
|
||||
('Diana',28,'diana@mail.com','2023-03-01'),
|
||||
('Ethan',35,'ethan@gmail.com','2023-03-10'),
|
||||
('Frank',29,'frank@mail.com','2023-01-05'),
|
||||
('Grace',26,'grace@gmail.com','2023-01-12'),
|
||||
('Henry',32,'henry@mail.com','2023-01-20'),
|
||||
('Iris',24,'iris@gmail.com','2023-02-10'),
|
||||
('Jack',27,'jack@mail.com','2023-04-15'),
|
||||
('Kelly',23,'kelly@gmail.com','2023-04-20'),
|
||||
('Leo',31,'leo@mail.com','2023-01-15'),
|
||||
('Mia',26,'mia@gmail.com','2023-01-18'),
|
||||
('Nina',28,'nina@mail.com','2023-01-08'),
|
||||
('Oscar',33,'oscar@gmail.com','2023-01-10'),
|
||||
('Paul',29,'paul@mail.com','2023-02-15'),
|
||||
('Quinn',24,'quinn@gmail.com','2023-03-20'),
|
||||
('Rita',27,'rita@mail.com','2023-04-01'),
|
||||
('Sam',30,'sam@gmail.com','2023-04-10'),
|
||||
('Tina',25,'tina@mail.com','2023-04-18');
|
||||
|
||||
INSERT INTO courses (title, category, created_at) VALUES
|
||||
('SQL для начинающих','data','2023-01-01'),
|
||||
('Python для анализа данных','programming','2023-01-15'),
|
||||
('BI с нуля','business','2023-03-01'),
|
||||
('Excel для аналитиков','business','2023-02-01'),
|
||||
('Основы машинного обучения','data','2023-03-15');
|
||||
|
||||
INSERT INTO lessons (course_id, title, duration_min) VALUES
|
||||
(1,'SELECT и FROM',10),
|
||||
(1,'JOIN',15),
|
||||
(1,'WHERE и фильтрация',12),
|
||||
(1,'Агрегации и GROUP BY',18),
|
||||
(1,'Подзапросы',25),
|
||||
(2,'Pandas основы',20),
|
||||
(2,'NumPy',15),
|
||||
(2,'Визуализация данных',22),
|
||||
(3,'Основы BI',12),
|
||||
(3,'Метрики и дашборды',18),
|
||||
(3,'Power BI введение',20),
|
||||
(4,'Формулы в Excel',15),
|
||||
(4,'Сводные таблицы',18),
|
||||
(4,'Графики и диаграммы',14),
|
||||
(5,'Введение в ML',20),
|
||||
(5,'Линейная регрессия',30),
|
||||
(5,'Классификация',28),
|
||||
(5,'Кластеризация',25);
|
||||
|
||||
INSERT INTO enrollments (user_id, course_id, enrolled_at) VALUES
|
||||
(1,1,'2023-01-15'),
|
||||
(1,2,'2023-02-01'),
|
||||
(2,1,'2023-01-20'),
|
||||
(3,2,'2023-03-05'),
|
||||
(4,3,'2023-04-01'),
|
||||
(6,1,'2023-01-06'),
|
||||
(6,2,'2023-01-10'),
|
||||
(6,4,'2023-02-05'),
|
||||
(7,1,'2023-01-13'),
|
||||
(7,3,'2023-03-10'),
|
||||
(8,1,'2023-01-22'),
|
||||
(8,2,'2023-01-22'),
|
||||
(8,3,'2023-01-22'),
|
||||
(9,4,'2023-02-12'),
|
||||
(10,1,'2023-04-16'),
|
||||
(11,2,'2023-04-21'),
|
||||
(12,1,'2023-01-16'),
|
||||
(13,2,'2023-01-19'),
|
||||
(13,4,'2023-02-20'),
|
||||
(14,1,'2023-01-09'),
|
||||
(14,2,'2023-01-09'),
|
||||
(14,3,'2023-01-09'),
|
||||
(14,4,'2023-02-01'),
|
||||
(14,5,'2023-03-20'),
|
||||
(15,1,'2023-01-11'),
|
||||
(15,3,'2023-03-05'),
|
||||
(16,2,'2023-02-16'),
|
||||
(16,4,'2023-02-16'),
|
||||
(17,3,'2023-03-22'),
|
||||
(17,5,'2023-03-25'),
|
||||
(18,1,'2023-04-02'),
|
||||
(19,1,'2023-04-11'),
|
||||
(19,2,'2023-04-11'),
|
||||
(20,2,'2023-04-19'),
|
||||
(5,5,'2023-03-20'),
|
||||
(1,4,'2023-02-10'),
|
||||
(2,3,'2023-03-15'),
|
||||
(3,1,'2023-02-25'),
|
||||
(4,2,'2023-03-10'),
|
||||
(6,3,'2023-03-08'),
|
||||
(7,2,'2023-02-01'),
|
||||
(9,1,'2023-02-15'),
|
||||
(12,3,'2023-03-10'),
|
||||
(15,2,'2023-02-01'),
|
||||
(18,4,'2023-04-05');
|
||||
|
||||
INSERT INTO lesson_views (user_id, lesson_id, viewed_at) VALUES
|
||||
(1,1,'2023-01-16 10:00:00'),
|
||||
(1,2,'2023-01-16 10:15:00'),
|
||||
(1,3,'2023-01-17 09:00:00'),
|
||||
(1,4,'2023-01-17 09:30:00'),
|
||||
(1,4,'2023-01-17 11:00:00'),
|
||||
(1,4,'2023-01-18 10:00:00'),
|
||||
(1,6,'2023-02-02 14:00:00'),
|
||||
(1,12,'2023-02-15 10:00:00'),
|
||||
(1,13,'2023-02-15 10:30:00'),
|
||||
(2,1,'2023-01-21 09:00:00'),
|
||||
(2,9,'2023-03-16 10:00:00'),
|
||||
(2,10,'2023-03-16 10:30:00'),
|
||||
(3,6,'2023-03-06 12:00:00'),
|
||||
(3,1,'2023-02-26 11:00:00'),
|
||||
(3,2,'2023-02-26 11:20:00'),
|
||||
(4,9,'2023-04-02 10:00:00'),
|
||||
(4,10,'2023-04-03 10:00:00'),
|
||||
(4,6,'2023-03-12 14:00:00'),
|
||||
(4,7,'2023-03-13 09:00:00'),
|
||||
(5,15,'2023-03-21 10:00:00'),
|
||||
(5,16,'2023-03-22 11:00:00'),
|
||||
(6,1,'2023-01-07 10:00:00'),
|
||||
(6,2,'2023-01-07 10:15:00'),
|
||||
(6,3,'2023-01-08 09:00:00'),
|
||||
(6,4,'2023-01-08 09:30:00'),
|
||||
(6,5,'2023-01-09 10:00:00'),
|
||||
(6,6,'2023-01-11 14:00:00'),
|
||||
(6,7,'2023-01-12 10:00:00'),
|
||||
(6,8,'2023-01-13 11:00:00'),
|
||||
(6,12,'2023-02-06 09:00:00'),
|
||||
(6,13,'2023-02-06 09:30:00'),
|
||||
(6,14,'2023-02-07 10:00:00'),
|
||||
(6,9,'2023-03-09 11:00:00'),
|
||||
(7,1,'2023-01-14 10:00:00'),
|
||||
(7,2,'2023-01-14 10:20:00'),
|
||||
(7,3,'2023-01-15 09:00:00'),
|
||||
(7,9,'2023-03-11 14:00:00'),
|
||||
(7,10,'2023-03-11 14:30:00'),
|
||||
(9,12,'2023-02-13 10:00:00'),
|
||||
(9,13,'2023-02-13 10:30:00'),
|
||||
(9,1,'2023-02-16 11:00:00'),
|
||||
(12,1,'2023-01-17 10:00:00'),
|
||||
(12,2,'2023-01-17 10:20:00'),
|
||||
(12,1,'2023-04-10 11:00:00'),
|
||||
(12,3,'2023-04-10 11:30:00'),
|
||||
(12,9,'2023-03-12 10:00:00'),
|
||||
(13,6,'2023-01-20 14:00:00'),
|
||||
(13,7,'2023-01-21 10:00:00'),
|
||||
(13,12,'2023-02-21 09:00:00'),
|
||||
(14,1,'2023-01-09 08:00:00'),
|
||||
(14,2,'2023-01-09 08:15:00'),
|
||||
(14,3,'2023-01-10 10:00:00'),
|
||||
(14,4,'2023-01-10 10:30:00'),
|
||||
(14,5,'2023-01-11 09:00:00'),
|
||||
(14,6,'2023-01-10 14:00:00'),
|
||||
(14,7,'2023-01-11 10:00:00'),
|
||||
(14,8,'2023-01-12 11:00:00'),
|
||||
(14,9,'2023-01-10 08:00:00');
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
CREATE MATERIALIZED VIEW user_cohort_analysis AS
|
||||
WITH users_and_enrollments_results AS (
|
||||
SELECT
|
||||
date_trunc('month', u.registration_date)::date AS cohort_month,
|
||||
count(DISTINCT u.id) AS users_in_cohort,
|
||||
count(DISTINCT e.user_id) AS enrolled_users,
|
||||
count(DISTINCT e.course_id) courses_in_cohort
|
||||
FROM
|
||||
users u
|
||||
JOIN
|
||||
enrollments e ON u.id = e.user_id
|
||||
GROUP BY
|
||||
cohort_month
|
||||
),
|
||||
lesson_views_results AS (
|
||||
SELECT
|
||||
date_trunc('month', u.registration_date)::date AS cohort_month,
|
||||
count(DISTINCT lv.user_id) AS active_users,
|
||||
round(avg(lv.viewed_at::date - u.registration_date::date), 0) AS avg_time_to_first_view
|
||||
FROM
|
||||
users u
|
||||
LEFT JOIN
|
||||
lesson_views lv ON u.id = lv.user_id
|
||||
GROUP BY
|
||||
cohort_month
|
||||
)
|
||||
SELECT
|
||||
uer.cohort_month,
|
||||
uer.users_in_cohort,
|
||||
uer.enrolled_users,
|
||||
lvr.active_users,
|
||||
round((lvr.active_users / uer.users_in_cohort::numeric) * 100, 2) || '%' AS retention_rate,
|
||||
round(uer.courses_in_cohort / uer.users_in_cohort::numeric, 2) AS avg_courses_per_user,
|
||||
lvr.avg_time_to_first_view
|
||||
FROM
|
||||
users_and_enrollments_results uer
|
||||
JOIN
|
||||
lesson_views_results lvr ON uer.cohort_month = lvr.cohort_month
|
||||
ORDER BY
|
||||
uer.cohort_month;
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
CREATE MATERIALIZED VIEW conversion_funnel AS
|
||||
WITH stage_courses AS (
|
||||
-- registered
|
||||
(SELECT
|
||||
0 AS course_id,
|
||||
1 AS stage_id,
|
||||
'registered' AS stage_name,
|
||||
count(DISTINCT u.id) AS users_count
|
||||
FROM
|
||||
users u)
|
||||
UNION ALL
|
||||
-- enrolled
|
||||
(SELECT
|
||||
e.course_id,
|
||||
2 AS stage_id,
|
||||
'enrolled' AS stage_name,
|
||||
count(DISTINCT e.user_id) AS users_count
|
||||
FROM
|
||||
enrollments e
|
||||
GROUP BY
|
||||
e.course_id
|
||||
ORDER BY
|
||||
e.course_id)
|
||||
UNION ALL
|
||||
-- started
|
||||
(SELECT
|
||||
l.course_id,
|
||||
3 AS stage_id,
|
||||
'started' AS stage_name,
|
||||
count(DISTINCT lv.user_id) AS users_count
|
||||
FROM
|
||||
lessons l
|
||||
JOIN
|
||||
lesson_views lv ON l.id = lv.lesson_id
|
||||
GROUP BY
|
||||
l.course_id
|
||||
ORDER BY
|
||||
l.course_id)
|
||||
UNION ALL
|
||||
-- mid_progress
|
||||
(WITH lessons_mid AS (
|
||||
SELECT
|
||||
l.course_id,
|
||||
ceil(percentile_cont(0.5) WITHIN GROUP (ORDER BY l.id)) AS mid_in_lesson_id
|
||||
FROM
|
||||
lessons l
|
||||
GROUP BY
|
||||
l.course_id
|
||||
)
|
||||
SELECT
|
||||
lm.course_id,
|
||||
4 AS stage_id,
|
||||
'mid_progress' AS stage_name,
|
||||
count(lv.user_id) AS users_count
|
||||
FROM
|
||||
lessons_mid lm
|
||||
LEFT JOIN
|
||||
lesson_views lv ON lm.mid_in_lesson_id = lv.lesson_id
|
||||
GROUP BY
|
||||
lm.course_id, lm.mid_in_lesson_id
|
||||
ORDER BY
|
||||
lm.course_id)
|
||||
UNION ALL
|
||||
-- completed
|
||||
(WITH courses_lessons_id AS (
|
||||
SELECT
|
||||
l.course_id,
|
||||
l.id AS lesson_id
|
||||
FROM
|
||||
lessons l
|
||||
GROUP BY
|
||||
l.course_id, l.id
|
||||
),
|
||||
lessons_numbers AS (
|
||||
SELECT
|
||||
cli.course_id,
|
||||
cli.lesson_id,
|
||||
ROW_NUMBER() OVER(PARTITION BY cli.course_id ORDER BY cli.lesson_id DESC) AS lesson_number
|
||||
FROM
|
||||
courses_lessons_id cli
|
||||
)
|
||||
SELECT
|
||||
ln.course_id,
|
||||
5 AS stage_id,
|
||||
'completed' AS stage_name,
|
||||
count(lv.user_id) AS users_count
|
||||
FROM
|
||||
lessons_numbers ln
|
||||
LEFT JOIN
|
||||
lesson_views lv ON ln.lesson_id = lv.lesson_id
|
||||
WHERE
|
||||
ln.lesson_number = 1
|
||||
GROUP BY
|
||||
ln.course_id, ln.lesson_id
|
||||
ORDER BY
|
||||
ln.course_id)
|
||||
)
|
||||
SELECT
|
||||
sc.course_id,
|
||||
c.title AS course_title,
|
||||
sc.stage_name,
|
||||
sc.users_count,
|
||||
CASE
|
||||
WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) IS NULL THEN 'first stage'
|
||||
WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) = 0 THEN 0::text || '%'
|
||||
WHEN lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id) IS NOT NULL THEN round((sc.users_count / lag(sc.users_count) OVER (PARTITION BY sc.course_id ORDER BY sc.stage_id)::NUMERIC) * 100, 0)::text || '%'
|
||||
END AS conversion_from_previous,
|
||||
round((sc.users_count / FIRST_VALUE(sc.users_count) OVER (ORDER BY sc.stage_id)::NUMERIC) * 100, 0)::text || '%' AS conversion_from_start
|
||||
FROM
|
||||
courses c
|
||||
RIGHT JOIN
|
||||
stage_courses sc ON c.id = sc.course_id
|
||||
ORDER BY
|
||||
sc.course_id;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
CREATE MATERIALIZED VIEW course_summary AS
|
||||
WITH courses_unique_users AS (
|
||||
SELECT
|
||||
e.course_id,
|
||||
count(e.user_id) AS unique_users
|
||||
FROM
|
||||
enrollments e
|
||||
GROUP BY
|
||||
e.course_id
|
||||
),
|
||||
lessons_results AS (
|
||||
SELECT
|
||||
l.course_id,
|
||||
count(distinct l.id) AS lessons_count,
|
||||
count(lv.lesson_id) AS total_views,
|
||||
min(lv.viewed_at)::date AS first_viewed_at,
|
||||
max(lv.viewed_at)::date AS last_viewed_at
|
||||
FROM
|
||||
lessons l
|
||||
LEFT JOIN
|
||||
lesson_views lv ON l.id = lv.lesson_id
|
||||
GROUP BY
|
||||
l.course_id
|
||||
)
|
||||
SELECT
|
||||
c.id AS course_id,
|
||||
c.title AS course_title,
|
||||
cuu.unique_users,
|
||||
lr.lessons_count,
|
||||
lr.total_views,
|
||||
round(lr.total_views / cuu.unique_users::numeric, 2) AS avg_views_per_user,
|
||||
lr.first_viewed_at,
|
||||
lr.last_viewed_at
|
||||
FROM
|
||||
courses c
|
||||
JOIN
|
||||
courses_unique_users cuu ON c.id = cuu.course_id
|
||||
JOIN
|
||||
lessons_results lr ON c.id = lr.course_id
|
||||
ORDER BY
|
||||
c.id;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
CREATE MATERIALIZED VIEW lesson_popularity_ranking as
|
||||
WITH lesson_views_results AS (
|
||||
SELECT
|
||||
lv.lesson_id,
|
||||
count(lv.user_id) AS total_views,
|
||||
count(DISTINCT lv.user_id) AS unique_viewers
|
||||
FROM
|
||||
lesson_views lv
|
||||
GROUP BY
|
||||
lv.lesson_id
|
||||
),
|
||||
enrollments_results AS (
|
||||
SELECT
|
||||
e.course_id,
|
||||
count(e.user_id) AS enrolled_to_course
|
||||
FROM
|
||||
enrollments e
|
||||
GROUP BY
|
||||
e.course_id
|
||||
)
|
||||
SELECT
|
||||
l.id AS lesson_id,
|
||||
l.title AS lesson_title,
|
||||
c.title AS course_title,
|
||||
lvr.total_views,
|
||||
lvr.unique_viewers,
|
||||
round(lvr.total_views / lvr.unique_viewers::numeric, 2) AS avg_views_per_viewer,
|
||||
dense_rank() OVER (ORDER BY lvr.total_views desc) AS popularity_rank,
|
||||
round((lvr.unique_viewers / er.enrolled_to_course::numeric) * 100, 0) || '%' AS completion_rate
|
||||
FROM
|
||||
lessons l
|
||||
JOIN
|
||||
courses c ON l.course_id = c.id
|
||||
JOIN
|
||||
lesson_views_results lvr ON l.id = lvr.lesson_id
|
||||
JOIN
|
||||
enrollments_results er ON c.id = er.course_id
|
||||
ORDER BY
|
||||
popularity_rank;
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
CREATE MATERIALIZED VIEW platform_summary as
|
||||
WITH users_count AS (
|
||||
SELECT
|
||||
count(*) AS total_users
|
||||
FROM
|
||||
users
|
||||
),
|
||||
courses_count AS (
|
||||
SELECT
|
||||
count(*) AS total_courses
|
||||
FROM
|
||||
courses
|
||||
),
|
||||
lessons_count AS (
|
||||
SELECT
|
||||
count(*) AS total_lessons
|
||||
FROM
|
||||
lessons
|
||||
),
|
||||
users_with_views AS (
|
||||
SELECT
|
||||
count(distinct user_id) AS users_with_views
|
||||
FROM
|
||||
lesson_views
|
||||
),
|
||||
views_count AS (
|
||||
SELECT
|
||||
count(*) AS total_views
|
||||
FROM
|
||||
lesson_views lv
|
||||
)
|
||||
SELECT
|
||||
uc.total_users,
|
||||
cc.total_courses,
|
||||
lc.total_lessons,
|
||||
uwv.users_with_views,
|
||||
round(lc.total_lessons / cc.total_courses::NUMERIC, 2) AS avg_lessons_per_course,
|
||||
round(vc.total_views / lc.total_lessons::NUMERIC, 2) AS avg_views_per_lesson
|
||||
FROM
|
||||
users_count uc
|
||||
CROSS JOIN
|
||||
courses_count cc
|
||||
CROSS JOIN
|
||||
lessons_count lc
|
||||
CROSS JOIN
|
||||
users_with_views uwv
|
||||
CROSS JOIN
|
||||
views_count AS vc;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
CREATE MATERIALIZED VIEW user_activity_summary AS
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
u.name,
|
||||
count(DISTINCT e.course_id) AS enrollment_count,
|
||||
count(DISTINCT lv.lesson_id) AS viewed_lessons_count,
|
||||
max(lv.viewed_at)::date AS last_view,
|
||||
min(lv.viewed_at)::date AS first_view,
|
||||
((count(DISTINCT lv.lesson_id)::numeric(5,2) / count(DISTINCT l.id)) * 100)::numeric(5, 2) AS engagement_rate
|
||||
FROM
|
||||
users u
|
||||
JOIN
|
||||
enrollments e ON u.id = e.user_id
|
||||
JOIN
|
||||
courses c ON e.course_id = c.id
|
||||
JOIN
|
||||
lessons l ON c.id = l.course_id
|
||||
LEFT JOIN
|
||||
lesson_views lv ON u.id = lv.user_id -- сюда - 'AND l.id = lv.lesson_id'
|
||||
GROUP BY
|
||||
u.id, u.name
|
||||
ORDER BY
|
||||
u.id;
|
||||
-- Кинул его в нейронку на проверку, она сказала что надо добавить 'AND l.id = lv.lesson_id' при присоединении таблицы lesson_views(сделал пометку в коде), это нужно чтобы точно проверить, что урок который посмотрел пользователь точно принадлежит его курсу, чтобы избежать ошибок из-за некорректных данные если такие появятся в результате какого либо сбоя или ошибки.
|
||||
@@ -0,0 +1,51 @@
|
||||
-- Задание 1
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
conversion_funnel
|
||||
WHERE
|
||||
stage_name = 'started'
|
||||
ORDER BY
|
||||
conversion_from_previous;
|
||||
-- Первые 2 курса, гже отавлилось больше всего учеников между этапами enrolled → started
|
||||
|
||||
|
||||
-- Задание 2
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
lesson_popularity_ranking lpr
|
||||
WHERE
|
||||
lpr.avg_views_per_viewer > 1
|
||||
LIMIT 3;
|
||||
|
||||
|
||||
-- Задание 3
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
user_cohort_analysis uca
|
||||
ORDER BY
|
||||
retention_rate DESC;
|
||||
-- Не разобрался как можжно соеденить с другими таблицам
|
||||
-- А retention_rate такой, наверное потому что при запуске курсов была пиар компания большая, и делали рассылки с напоминанием позаниматься, а потом перестали, вот и удержание похерилось
|
||||
|
||||
|
||||
-- Задание 4
|
||||
-- Витрина - user_activity_summary
|
||||
-- Всяко разно попробовал подобавлять индексы, итог: без индексов быстрее. Просмотрел весь запрос, вроде ничего не нужно менять - лишнего ничего нет.
|
||||
|
||||
-- Витрина - course_summary
|
||||
-- Тут также как и в предидущем - и так и сяк попробовал, план запроса не меняется, время не улучшается
|
||||
|
||||
-- Витрина - platform_summary
|
||||
-- Тут тоже нечего менять - всё из CROSS JOIN. Попробовал вместо count(*) писать count(id) думал мол в ширину будет меньше читать, в итоге выяснил что и так и так одинаково читает
|
||||
|
||||
-- Витрина - user_cohort_analysis
|
||||
-- И тут че не делай, все одна фигня. План вроде хороший.
|
||||
|
||||
-- Витрина - lesson_popularity_ranking
|
||||
-- Тут тоже ничего не вышло - я начниаю чувствовать себя тупым от того что не могу улучшить нигде время запроса.
|
||||
|
||||
-- Витрина - conversion_funnel
|
||||
-- Здесь тоже ничего не вышло - со стороны план запроса выглядит хорошо. Ускорить время запроса не вышло.
|
||||
@@ -0,0 +1,111 @@
|
||||
/*Задание 1: По каким полям делать индексы(тут говорится смотреть на таблицы из пет проекта 'Онлайн магазины', надеюсь смотреть на таблицы а не на витрины):
|
||||
1) По каким полям, как тебе кажется, реальный продукт‑менеджер или аналитик будет чаще всего:
|
||||
where:
|
||||
users: age, email, registration_date,
|
||||
enrollments: enrolled_at
|
||||
courses: title, category
|
||||
lessons: title, duration_min
|
||||
lesson_views: viewed_at
|
||||
order by:
|
||||
users: id, age, registration_date
|
||||
enrollments: user_id, enrolled_at
|
||||
courses: id
|
||||
lessons: id, duration_min
|
||||
lesson_views: user_id, viewed_at
|
||||
group by:
|
||||
users: age, registration_date
|
||||
enrollments: user_id, course_id, enrolled_at
|
||||
courses: category
|
||||
lessons: course_id
|
||||
lesson_views: user_id, lesson_id
|
||||
join:
|
||||
users: id
|
||||
enrollments: user_id, course_id
|
||||
courses: id
|
||||
lessons: id, course_id
|
||||
lesson_views: user_id, lesson_id
|
||||
2) Для каждого поля, которое ты назвал, укажи: «индекс нужен / индекс не нужен» и коротко объясни почему (часто фильтруем, редко фильтруем, мало разных значений и т.п.).
|
||||
users:
|
||||
id(есть автоматически),
|
||||
age(не нужен, мало разнах значений),
|
||||
email(нужен, чтобы найти пользователя через where)
|
||||
registration_date(нужен, часто будет сравниваться в BETWEEN наприме, и сортироваться)
|
||||
enrollments:
|
||||
user_id(нужен, будет часто джоиниться),
|
||||
course_id(нужен, будет часто джоиниться),
|
||||
enrolled_at(нужен, часто будет сравниваться в BETWEEN например, и сортироваться)
|
||||
courses:
|
||||
id(есть автоматически),
|
||||
title(не нужен, мало разных значений),
|
||||
category(не нужен, мало разных значений)
|
||||
lessons:
|
||||
id(есть автоматически),
|
||||
course_id(нужен, будет часто джоиниться),
|
||||
title(не нужен, мало разных значений),
|
||||
duration_min(мне кажется не нужен, тк не очень часто будет использоваться)
|
||||
lesson_views:
|
||||
user_id(нужен, будет часто джоиниться),
|
||||
lesson_id(нужен, будет часто джоиниться)
|
||||
viewed_at(нужен, часто будет сравниваться в BETWEEN например, и сортироваться)*/
|
||||
|
||||
-- Задание 2: Свяжи запрос и индекс:
|
||||
-- 1)
|
||||
SELECT * FROM users WHERE email = 'какой-то email'; -- индекс на email ускорит where
|
||||
-- 2)
|
||||
SELECT * FROM users WHERE is_active = true AND created_at >= CURRENT_DATE - INTERVAL '1 year'; -- индекс на created_at ускоряет where
|
||||
-- 3)
|
||||
SELECT * FROM products WHERE category = 'Электроника' ORDER BY price; -- индекс по price ускорит order by
|
||||
-- 4)
|
||||
SELECT * FROM orders WHERE user_id = 1 AND order_date >= CURRENT_DATE - INTERVAL '30 days'; -- индекс на user_id и order_date ускорят where
|
||||
-- 5)
|
||||
SELECT order_id, product_id FROM order_items; -- на у тут по идее составной создается сразу, так как тут pkey составной. Ну и они ускоряют select
|
||||
|
||||
/* Задание 3. Порядок столбцов в индексе
|
||||
1) Я бы сделал orders(order_date, user_id).
|
||||
2) Для первых двух будет работать четко, а для третьего хуже. Тк у третьего нет фильрации по order date, и он стоит вторым в индексе
|
||||
3)
|
||||
* Когда один индекс - хорошо с фильтрацией по нескольким полям, и когда первое поле указанное в индексе используется часто, а второе нет.
|
||||
* Когда индекса два - хорошо когда оба поля фильтруются по отдельности*/
|
||||
|
||||
/* Задание 4. Лишние индексы в наших таблицах
|
||||
!!! Я тут не буду писать индексы которые создаются автоматически из за pkey
|
||||
users:
|
||||
Скорее нужен индекс:
|
||||
email, created_at(нужны для where, и order by)
|
||||
Скорее не нужен индекс:
|
||||
name, age, country, is_active(либо мало рахных значений, либо не очень востребованы эти поля)
|
||||
products:
|
||||
Скорее нужен индекс:
|
||||
price (для order by и where)
|
||||
Скорее не нужен индекс:
|
||||
name, ctegory, in_stock(либо мало разных значений, либо не очень востребованы эти поля)
|
||||
orders:
|
||||
Скорее нужен индекс:
|
||||
user_id(для join), price(для order by и where)
|
||||
Скорее не нужен индекс:
|
||||
name, category, in_stock(либо мало разных значений, либо не очень востребованы эти поля)
|
||||
order_items:
|
||||
Скорее нужен индекс:
|
||||
price_at_purchase (для order by и where)
|
||||
Скорее не нужен индекс:
|
||||
quantity(мало разных значения)
|
||||
|
||||
Много индексов не есть хорошо, потому что они тоже занимают помять и их тоже нужно считать. Если их бездумно везде понатыкать то запрос может выполняться дольше.*/
|
||||
|
||||
/* Задание 5. Что должно быть уникальным
|
||||
1) id, email
|
||||
2) id, article_number(придумал вот такое)
|
||||
3)
|
||||
* Если не будет уникаьности по id, то возникнут проблемы бд, что у разных пользователей будет один id и это жопа, ничего невозможно фильтровать и тд.
|
||||
* Если email будет повторяться, то как вообще пользователи будут входить в аккаунт, и как делать персонализированную рассылку? Никак.
|
||||
* Если article_number будет повторяться, то те же проблемы что и с id, будет нарушение логики работы бд.
|
||||
|
||||
* Уникальный индекс обезопасит бд от сбоев и некорректной работы.*/
|
||||
|
||||
-- Задание 6. Где индексы помогут, а где нет
|
||||
SELECT * FROM users WHERE id = 100; -- индекс уже создан тк это pkey
|
||||
SELECT * FROM users WHERE email = 'user@example.com'; -- нужен, поможет с where, быстрее будет рабоать фильтрация
|
||||
SELECT * FROM products WHERE price BETWEEN 100 AND 500; -- нужн, поможет с where, быстрее будте фильтрация
|
||||
SELECT * FROM products WHERE name LIKE '%phone%'; -- не нужен, индекс не рвботает с like
|
||||
SELECT * FROM orders WHERE status = 'DELIVERED'; -- не нужен, в status мало разных значений
|
||||
SELECT * FROM order_items WHERE order_id = 123; -- нужен, поможет с where, быстрее будет рабоать фильтрация
|
||||
@@ -0,0 +1,189 @@
|
||||
/* Задание 1. Какой вид партиционирования выбрать
|
||||
1)
|
||||
* range по полю order_date
|
||||
* запросы с between в год
|
||||
1. where order_date >= '2022-01-01' and order_date < '2023-01-01'
|
||||
2. where extract('year' from order_date) = '2025'
|
||||
2)
|
||||
* тут два типа партиционирования подходят. range по created_at или list по country.
|
||||
* если выбирать list по country то плюс - это при фильтрах по стране будут отсекаться лишние партиции, а минус, что год/месяц регистрации не будет разделен по партициям.
|
||||
3)
|
||||
* логично range по created_at - чтобы разделить по месяцам/годам, а hash по user_id - почему так, точно сказать не могу, но в теории сказано что hash используется когда нет диапазона или списка и это частый сценарий для логов
|
||||
* с range выиграют запросы с фильтрами по created_at, а с hash выигают запросы с фильтрами по user_id, а все что не указаны как поле для партиций, не почувствуют разницы.*/
|
||||
|
||||
|
||||
/* Задание 2. Размер партиций
|
||||
2) количество партициый за 10 лет по:
|
||||
* годам - 10, тут вообще все четко, и удалять не сложно, и поддержка проще, и запросы будут хорошо делаться
|
||||
* месяцам - 120, тут с удалением тарых данных труднее, поддержка тяжелая, но скорость запросов будет лучше, если фильтрация будет по месяцам
|
||||
* дням - примерно 3650, а это я чувствую пиздец, с удалением и поддержкой жопа, и со скоростью запросов тоже жопа - данные будут читаться из кучи партиций что не есть хорошо
|
||||
3)
|
||||
Я думаю прям для очень большого магазина оптимальнее будет партиционирование по месяцам - тк очень много покупателей и большинство статистик будет по месяцам, по годам тоэе конечно, но думаю приоритетнее по месяцам*/
|
||||
|
||||
|
||||
-- Задание 3. RANGE‑партиции по дате заказов
|
||||
DROP TABLE IF EXISTS orders_part;
|
||||
CREATE TABLE orders_part (
|
||||
id bigint,
|
||||
user_id bigint,
|
||||
order_date timestamptz NOT NULL,
|
||||
status varchar(20),
|
||||
PRIMARY KEY (id, order_date),
|
||||
FOREIGN KEY (user_id) REFERENCES users_1(id)
|
||||
) PARTITION BY RANGE (order_date);
|
||||
|
||||
CREATE TABLE orders_2024 PARTITION OF orders_part
|
||||
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
|
||||
|
||||
CREATE TABLE orders_2025 PARTITION OF orders_part
|
||||
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
|
||||
|
||||
CREATE TABLE orders_2026 PARTITION OF orders_part
|
||||
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
|
||||
|
||||
-- сделал insert который попросил сгенерировать нейронку
|
||||
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
orders_part
|
||||
WHERE
|
||||
order_date BETWEEN '2025-01-01' AND '2025-12-31 23:59:59'; -- пришлось сделать так, тк дата 2026-01-01 тоже есть в таблице
|
||||
-- в этом запросе за год - читатется только одна партиция - orders_2025, НО если в условии все же написать AND '2026-01-01', то читаются две партиции orders_2025 и orders_2026 и время выполнения запроса намного больше тк выполняется full scan обеих таблиц
|
||||
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
orders_part
|
||||
WHERE
|
||||
EXTRACT('year' FROM order_date) IN ('2024', '2025'); -- так читаются все три партиции, 2026 почему-то тоже захватывается, но по итогу после сканирование у таблицы orders_2026 - rows = 0
|
||||
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
orders_part;
|
||||
-- так просто сканируются все три партиции, но без фильтров, как было в предидущем варианте
|
||||
|
||||
|
||||
-- Задание 4. LIST‑партиции по стране пользователей
|
||||
DROP TABLE IF EXISTS users_part;
|
||||
CREATE TABLE users_part (
|
||||
id int8 NOT NULL,
|
||||
"name" varchar(100) NOT NULL,
|
||||
age int2 NULL,
|
||||
email varchar(100) NULL,
|
||||
country bpchar(2) NULL,
|
||||
is_active bool NULL,
|
||||
created_at timestamptz NOT NULL,
|
||||
PRIMARY KEY (id, country)
|
||||
) PARTITION BY LIST (country);
|
||||
|
||||
CREATE TABLE users_ru PARTITION OF users_part
|
||||
FOR VALUES IN ('RU');
|
||||
CREATE TABLE users_us PARTITION OF users_part
|
||||
FOR VALUES IN ('US');
|
||||
CREATE TABLE users_gb PARTITION OF users_part
|
||||
FOR VALUES IN ('GB');
|
||||
CREATE TABLE users_de PARTITION OF users_part
|
||||
FOR VALUES IN ('DE');
|
||||
CREATE TABLE users_fr PARTITION OF users_part
|
||||
FOR VALUES IN ('FR');
|
||||
CREATE TABLE users_jp PARTITION OF users_part
|
||||
FOR VALUES IN ('JP');
|
||||
CREATE TABLE users_uk PARTITION OF users_part
|
||||
FOR VALUES IN ('UK');
|
||||
CREATE TABLE users_kz PARTITION OF users_part
|
||||
FOR VALUES IN ('KZ');
|
||||
|
||||
-- сделал insert который попросил сгенерировать нейронку
|
||||
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
users_part
|
||||
WHERE
|
||||
country = 'RU';
|
||||
-- запрос скаинровал только партицию users_ru
|
||||
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
users_part
|
||||
WHERE
|
||||
country IN ('RU', 'KZ');
|
||||
-- запрос сканировал только партиции users_ru и users_kz
|
||||
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
users_part
|
||||
WHERE
|
||||
age BETWEEN 44 AND 66;
|
||||
-- сканировал не просто users_part, а каждую партицию по отдельности
|
||||
|
||||
|
||||
-- Задание 5. HASH‑партиции по user_id в логах
|
||||
DROP TABLE IF EXISTS user_logs_part;
|
||||
CREATE TABLE user_logs_part (
|
||||
id int8,
|
||||
user_id bigint NOT NULL,
|
||||
created_at timestamp NOT null,
|
||||
event_type text NOT NULL,
|
||||
payload jsonb,
|
||||
PRIMARY KEY (id, user_id)
|
||||
) PARTITION BY hash (user_id);
|
||||
|
||||
CREATE TABLE user_logs_p1 PARTITION OF user_logs_part
|
||||
FOR VALUES WITH (modulus 4, remainder 0);
|
||||
|
||||
CREATE TABLE user_logs_p2 PARTITION OF user_logs_part
|
||||
FOR VALUES WITH (modulus 4, remainder 1);
|
||||
|
||||
CREATE TABLE user_logs_p3 PARTITION OF user_logs_part
|
||||
FOR VALUES WITH (modulus 4, remainder 2);
|
||||
|
||||
CREATE TABLE user_logs_p4 PARTITION OF user_logs_part
|
||||
FOR VALUES WITH (modulus 4, remainder 3);
|
||||
|
||||
-- сделал insert который попросил сгенерировать нейронку
|
||||
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM user_logs_part WHERE user_id = 228;
|
||||
-- читает только user_logs_p2
|
||||
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM user_logs_part WHERE created_at >= current_date - INTERVAL '7 days';
|
||||
-- читает все партиции
|
||||
|
||||
-- Задание 6
|
||||
-- email, чтобы искать пользователя быстрее, created_at для сортировки и where
|
||||
CREATE UNIQUE INDEX idx_users_ru_email ON users_ru(email);
|
||||
CREATE INDEX idx_users_ru_created_at ON users_ru(created_at);
|
||||
|
||||
CREATE UNIQUE INDEX idx_users_us_email ON users_us(email);
|
||||
CREATE INDEX idx_users_us_created_at ON users_us(created_at);
|
||||
|
||||
CREATE UNIQUE INDEX idx_users_gb_email ON users_gb(email);
|
||||
CREATE INDEX idx_users_gb_created_at ON users_gb(created_at);
|
||||
|
||||
CREATE UNIQUE INDEX idx_users_de_email ON users_de(email);
|
||||
CREATE INDEX idx_users_de_created_at ON users_de(created_at);
|
||||
|
||||
CREATE UNIQUE INDEX idx_users_fr_email ON users_fr(email);
|
||||
CREATE INDEX idx_users_fr_created_at ON users_fr(created_at);
|
||||
|
||||
CREATE UNIQUE INDEX idx_users_jp_email ON users_jp(email);
|
||||
CREATE INDEX idx_users_jp_created_at ON users_jp(created_at);
|
||||
|
||||
CREATE UNIQUE INDEX idx_users_uk_email ON users_uk(email);
|
||||
CREATE INDEX idx_users_uk_created_at ON users_uk(created_at);
|
||||
|
||||
CREATE UNIQUE INDEX idx_users_kz_email ON users_kz(email);
|
||||
CREATE INDEX idx_users_kz_created_at ON users_kz(created_at);
|
||||
-- должны ускориться запросы с поиском пользователей по email, и запросы с сортировкой по дате или с фильтрацией по интервалам
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Задание 1. Выбор ключа шардирования
|
||||
-- Ну я тут чето нихера не понял
|
||||
-- Вот users лучше всего шардировать по id, а с orders непонятно. Я не до конца понял логику шардирования, если orders и users на разных шардах, то наверное лучше шардировать по user_id. Но в тоже время логично было бы сделать по order_date.
|
||||
-- Короче я очень плохо понял шардирование, сейчас поищу объяснение на ютубчике и напишу новый ответ.
|
||||
-- В нашем случае мы получается обсуждаем горизонтальное шардирование
|
||||
-- Как я понял, если шардируются две таблицы, то они шардирются по одному полю у нас это users(id) и orders(user_id).
|
||||
-- Я выбрал users(id), тк это уникальное поле и есть join по нему в orders - user_id
|
||||
-- Шардирование будет происходить по хэшу. Быстрее будут выполняться запросы в пределах одного шарда, а запросы где данные берутся из разных шардов да ещё и join - явно станут сложнее.
|
||||
|
||||
-- Задание 2. Добавление нового шарда
|
||||
-- Проблемы: мне кажется временная приостановка записи и чтения таблиц на время 'перешардирования'.
|
||||
-- План: 1. остановить запись и чтение данных; 2. создать 8 новых шардов; 3. перераспределить данные из старых четырех в восемь новых шардов; 4. удалить старые 4 шарда; 5. поменять логику изменения и чтение в бэк энде например 6. разрешить запись и чтение. конец
|
||||
|
||||
-- Задание 3. Шардинг против партиционирования
|
||||
-- При партицировании таблицы данные хранятся на одной бд, внутри одной таблицы, но эта таблица внутри себя разделена на партиции для ускорения запросов к ней.
|
||||
-- При шардировании таблицы данные хранятся на разных серверах, то есть таблица делится на части(при горизонтальном шардировании), и каждая часть таблицы отправляется на разные сервер.
|
||||
|
||||
-- Пример партицирования без шардирования: когда сервер не переполнен, но таблица слишком большая, и и запросы к ней замедляются, тогда таблица делится на партиции, для более быстрого выполнения запросов к ней.
|
||||
-- Пример когда без шардирования не обойтись: это когда сервер перегружен, и физически невозможно добавлять новые данные и чтение таблиц очень медленное, тк для этого чтения сервер не может выделять должное количество ресурсов.
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
-- Задание 1. Где и зачем понадобится репликация
|
||||
-- На примере онлайн-магазина
|
||||
-- Пример 1: мастер сервер перестал работать(по любым причинам), и чтобы не останавливать работу магазина - делаем реплику мастером. Если бы небыло репликации работа магазина приостановилась бы на какое время.
|
||||
-- Пример 2: на мастер сервере даже без чтения высокая нагрузка с инсертами, апдейтами, а чтение происходит с реплики, например отчеты для аналитиков читаются с реплики чтобы не перегружать мастер. Если убрать репликацию, то сервер будет один и может перегрузиться и упасть.
|
||||
-- Пример 3: я сдеал первыве два примера настоллько обобщенными, что они покрываются все более конкретные примеры, и я не могу придумать пример 3.
|
||||
|
||||
|
||||
-- Задание 2. Сравнение синхронной и асинхронной репликации
|
||||
-- * как ведёт себя запись при нормальной работе;
|
||||
-- * синхронная репликация - мастер ждет пока реплика завершит копирование, наверное блокирует строку в таблице как в тарнзакции строка блокируется пока транзакция не завершится.
|
||||
-- * асинхронная репликация - мастер ничего не ждет - его главная задача, что он отправил даные в реплику, и реплика уже пусть сама разбирается и сохраняетс всё.
|
||||
-- * что происходит при падении мастера;
|
||||
-- * я думаю при обоих видах репликаций - реплика становится мастером.
|
||||
-- * какие риски потери/отставания данных есть.
|
||||
-- * с синхронной - рисков с отставанием и потерями вроде нет.
|
||||
-- * в случае с асинхронной - реплика может не успеть сохранить что-то одно, как придет уже что-то второе в туже строку например, или может произойти какой-то сбой, и данные прийдут некорректно - получается транзакция просто не откатится, а будет как будет.
|
||||
|
||||
-- платежному сервису нужен синхронный вид репликации, тк ошибки и неточность недопустимы.
|
||||
-- аналитической витрины нужен асинхронный вид репликации, тк он быстрее, и небольшая неточность не критична.
|
||||
|
||||
|
||||
-- Задание 3. Логическая и физическая репликация
|
||||
-- * думаю логическая репликация это больше про то, что мы видим и делаем сами, по типу sql запрос написать, а физическая репликация - это про то, что происходит на уровне субд/сервера, что данные перетекают байтах, WAL-журналов(я погуглил что-это - это грубо говоря план изменений, но пока не сами изменения)
|
||||
-- Оказывается про логические физические репликации я тоже ничего не понял, сейчас прогуглю, и напишу ответ заново.
|
||||
-- * у логической репликации изменения передаются как sql запросы, для отдельных талиц например, а физическаая репликация изменяется побайтово создавая полную копию мастера
|
||||
-- * у логической репликации высокая гибкость, можно выбирать что обновлять, а что нет, а у физической репликации нулевая гибкость - создается просто копия мастера 1 в 1
|
||||
-- * как я понял для миграция лучше всего логическая репликация - тк высокая гибкость, а физическая подходит для резервной копии
|
||||
|
||||
-- * для физической репликации - банковская система, тк физичская быстрее и копия 1 в 1 делается, а для логическрй репликации я не могу ничего придумать.
|
||||
|
||||
|
||||
-- Задание 4. Схема один мастер — несколько реплик для чтения
|
||||
-- * на мастер отправлять строго unsrt update delete только с изменениями, а на реплики select, только с чтением
|
||||
-- * проблемы из-за отсавания: 1. пользователь обновил фото профиля, а изменилось оно на странице толькко через 1-2 минуты; 2. один пользователь купил последний товар, а реплика не успела обновиться, и другой пользователь тоже попытался заказать этот товар, и у него вылезла ошибка, получается что товар отображается в наличии а купить нельзя; 3. в банках если будут отставать то там вообще жопа будет со счетами, но там получается синхронная репликация.
|
||||
-- * наверное можно закрыть доступ на изменения для строк которые ещё не до конца реплицировались
|
||||
|
||||
-- Задание 5. Мульти‑мастер: когда это оправдано
|
||||
-- * главные плюсы: 1. распределение нагрузки между серверами 2. каждая мастер репликация отвечает только за свою зону ответсвенности
|
||||
-- * сложности: 1. конфликты, что на разных мульти репликах могут быть разные записи по одному и тому же значению и будет не ясно что важнее оставить; 2. согласованность - кадая мульти реплика должна работать только в пределах своей зоны ответственности; 3. сложность настройки - я думаю имеется ввиду защита от конфликтов и поддержка согласованности.
|
||||
-- * у меня в голове только мысль о разделении отвественности по хэшу, что одна мастер реплика отвечает за один кусок, другая за другой, по типу шардирования, только без деления таблиц, они как бы и дальше между собой реплицируются, но каждая изменят обновляет и делает записи только на своем куске. Наверное такой способ поможет при больших таблицах с частыми изменениями(хотя по суть шардирование лучше испольщовать тогда, а не мульти-мастер). Неэффективно будет на таблицах с редкими изменениями, и не очень больших таблицах.
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
-- Задание 1
|
||||
-- 1. будет
|
||||
-- 2. не будет. можно изменить запрос - убрать lower
|
||||
-- 3. не будет. нужно убрать like
|
||||
-- 4. не будет. нужно убрать like
|
||||
-- 5. будет
|
||||
-- 6. не будет, тк фильтрация по полю без индекса
|
||||
|
||||
|
||||
-- Задание 2
|
||||
-- не понял задание, сначала нужно каждому запросу сказать что индекс не нужен/бсполезен, а потом создать индекс?
|
||||
-- 1. по идее есть смысл создать два индекса - по user_id и created_at, или состваной.
|
||||
CREATE INDEX idx_orders_user_id ON orders(user_id);
|
||||
CREATE INDEX idx_orders_created_at ON orders(created_at);
|
||||
-- 2. тут никакой индекс не нужен
|
||||
-- 3. тут тоже индекс не нужен, тк у status всего 4 значения
|
||||
|
||||
|
||||
-- Задание 3
|
||||
-- 1. тут hash, тк равенство
|
||||
-- 2. здесь индекс и не нужен, тк у status всего 4 значения
|
||||
-- 3. тут B-tree, тк интервал(<,>)
|
||||
-- 4. конкретно в этом запросе лучше hash, тк равенство, но в целом думаю на user_id лучше B-tree, тк часто сортируют по нему
|
||||
|
||||
-- * hash иммет смысл где просто равенство
|
||||
-- * B-tree обязательно там где интервалы, > или <, и где order by
|
||||
-- * в варианте 2, где у столбца всего 4 уникальных значения
|
||||
|
||||
|
||||
-- Задание 4
|
||||
-- * первый составной нужен для запросов с фильтрацией по нескольким полям
|
||||
-- * второй не нужен, тк user_id будет рабоать эффективно и в первом случае
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Задание 1. Интернет‑банк
|
||||
-- * в этом случае важна атомарность, и первый запрос должен был откатиться обратно, и не дублироваться как я понимаю.
|
||||
-- * если не будеь атомарноти, то после сбоев могут произойте несостыковки, напрмер у пользователя деньги списались, а заказ не оформился, или наоборот
|
||||
|
||||
|
||||
-- Задание 2. Лента новостей и лайки
|
||||
-- * эта система ближе к BASE, тк согласование узлов происходит не сразу, и у кого то 22 лайка, а у кого-то 24.
|
||||
|
||||
|
||||
-- Задание 3. Заказы в интернет‑магазине
|
||||
-- * у первого случая принципы ACID - это нужно чтбы критическа важная информация обновлялась без ошибок, и чтобы небыло несостыковок
|
||||
-- * во втором случае принципы BASE - можно немного потерпеть неточность, тк это некритично
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
-- Задание 1: Разбери ситуацию
|
||||
-- 1) 200
|
||||
-- 2) 100
|
||||
-- 3) Результаты разные тк на уровне Read Committed разрешено неповторяемое чтение, а на уровне Repeatable Read неповторяемое чтенине запрщено, и мы видим то, что было на момент начала транщакции.
|
||||
|
||||
-- Задание 2: Практика
|
||||
-- При Repeatable Read, я увидел во втором select значение - 100, потому что изменение произошли до того как завершилась транзакция с чтением, и зафиксировалось то значение которые было на момент начала транзакции с чтением
|
||||
-- При Read Committed, во втором select было значение - 200, тк этот уровень изолированности подразумевает чтение закоммиченных(другой транзакцией) данных даже если текущая транзакция ещё не завершена
|
||||
|
||||
-- Задание 3: Найди ошибку
|
||||
-- 1) Так произошло потому, что выбран некорректный уровень изоляции.
|
||||
-- 2) Тут подойдет serializavle.
|
||||
-- 3) Решить эту проблему без смены изоляции можно с помощью проверки баланса перед списанием, и если денег на балансе будет недостаточно, то откатить транзакцию. Такое решение подойдет только с уровнем изоляции Read Commited, для Repeatable Read такой способ уже не подойдет.
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Задание 1: Определи тип базы
|
||||
-- 1) CP
|
||||
-- 2) AP
|
||||
-- 3) CA
|
||||
-- 4) AP
|
||||
|
||||
|
||||
-- Задание 2: Что выбрать?
|
||||
-- CP - должна быть согласованность, чтобы последний билет достался только одному, у двух людей не может быть одинаковый билет
|
||||
-- Если выбрать CP, то все будет четко, и билет достентся только одному пользователю
|
||||
-- Если выбрать AP, то билет может достаться и тому и тому, что недопустимо
|
||||
@@ -0,0 +1,38 @@
|
||||
CREATE TABLE users (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
age SMALLINT CHECK (age > 0),
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
country VARCHAR(50),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
-- age - для возраста не нужны болшие числа
|
||||
-- country - можно было бы использовать VARCHAR(2), я бы тут спросил у тимлида как будет удобней использовать поле)
|
||||
-- created_at - нужно знать не только дату но и время создания/регистрации
|
||||
-- id - если не прописать not null в primary key, то not null ставится автоматически?
|
||||
|
||||
CREATE TABLE products (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
category VARCHAR(50),
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
in_stock BOOLEAN DEFAULT true,
|
||||
);
|
||||
|
||||
CREATE TABLE orders(
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
user_id INT,
|
||||
order_date TIMESTAMPTZ NOT NULL,
|
||||
status VARCHAR(20),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
-- status - лучше прописать словами статус, так понятней будет. Я бы сюда поставил ENUM
|
||||
|
||||
CREATE TABLE order_items(
|
||||
order_id INT NOT NULL,
|
||||
product_id INT NOT NULL,
|
||||
quantity INT NOT NULL,
|
||||
price_at_purchase DECIMAL(10, 2) NOT NULL,
|
||||
PRIMARY KEY (order_id, product_id)
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
INSERT INTO users (name, age, email, country, is_active, created_at)
|
||||
VALUES ('Alice', 25, LOWER('alice@example.com'), 'US', true, '2024-01-10 10:00:00'),
|
||||
('Bob', 30, LOWER('bob@example.com'), 'UK', true, '2024-02-05 09:30:00'),
|
||||
('Carol', 27, LOWER('carol@example.com'), 'US', false, '2024-03-01 15:20:00'),
|
||||
('Diana', 22, LOWER('diana@GMAIL.com'), 'US', true, '2024-01-12 08:15:00'),
|
||||
('Evan', 35, LOWER('EVAN@example.com'), 'UK', true, '2024-01-18 19:40:00'),
|
||||
('Farida', 29, LOWER('farida@example.com'), 'KZ', true, '2024-02-20 11:05:00'),
|
||||
('George', 31, LOWER('george+test@example.com'), 'US', true, '2024-03-05 16:45:00'),
|
||||
('Ivan', 26, LOWER('ivan@example.com'), 'RU', true, '2024-01-11 14:30:00'),
|
||||
('Julia', 28, LOWER('julia@example.com'), 'US', true, '2024-03-07 18:05:00');
|
||||
|
||||
INSERT INTO products (name, category, price, in_stock)
|
||||
VALUES ('iPhone 15', 'Электроника', 80000, true),
|
||||
('Kindle', 'Электроника', 15000, true),
|
||||
('Кроссовки Nike', 'Одежда', 9000, true),
|
||||
('Кроссовки NoName', 'Одежда', 3000, false),
|
||||
('Наушники Pro', 'Электроника', 12000, true),
|
||||
('Фитнес‑браслет', 'Гаджеты', 6000, true),
|
||||
('Подарочная карта', 'Сервисы', 1000, true);
|
||||
|
||||
INSERT INTO orders (user_id, order_date, status)
|
||||
VALUES (1, '2025-01-10 12:00:00', 'paid'),
|
||||
(3, '2025-01-12 09:30:00', 'cancelled'),
|
||||
(1, '2025-01-15 18:20:00', 'paid'),
|
||||
(2, '2025-01-20 11:05:00', 'created'),
|
||||
(4, '2025-02-01 17:45:00', 'paid'),
|
||||
(5, '2025-02-10 10:10:00', 'refunded'),
|
||||
(7, '2025-03-01 09:00:00', 'paid'),
|
||||
(9, '2025-03-05 19:30:00', 'paid');
|
||||
|
||||
INSERT INTO order_items (order_id, product_id, quantity, price_at_purchase)
|
||||
VALUES (1, 1, 1, 78000),
|
||||
(1, 3, 2, 8500),
|
||||
(2, 2, 1, 14000),
|
||||
(3, 5, 1, 11000),
|
||||
(3, 7, 1, 1000),
|
||||
(4, 2, 1, 15000),
|
||||
(5, 6, 1, 6000),
|
||||
(6, 1, 1, 82000),
|
||||
(7, 3, 1, 9000),
|
||||
(8, 4, 2, 2800);
|
||||
@@ -0,0 +1,56 @@
|
||||
-- Задача 1
|
||||
SELECT name, category, price,
|
||||
price * 1.3 AS price_with_markup,
|
||||
price * 1.5 AS price_with_vat
|
||||
FROM products
|
||||
WHERE (price BETWEEN 100 AND 1300)
|
||||
AND category NOT IN ('smartwatch')
|
||||
AND in_stock = TRUE;
|
||||
|
||||
-- Вопрос:
|
||||
-- Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему?
|
||||
-- Ответ:
|
||||
-- Для меня BETWEEN 100 AND 1300 читается лучше, сразу понятен срез от 100 и 1300.
|
||||
|
||||
|
||||
-- Задача 2
|
||||
SELECT name, price,
|
||||
price * 0.85 AS price_with_discount
|
||||
FROM products
|
||||
WHERE (price < 100 OR price > 1000);
|
||||
|
||||
-- Вопрос:
|
||||
-- Почему важно явно писать скобки в условии с OR, даже если кажется, что и так понятно?
|
||||
-- Ответ:
|
||||
-- Потому что условие может не правильно отобразить результат, AND выполняется раньше OR.
|
||||
-- Для надежности лучше скобками прописывать явно.
|
||||
|
||||
|
||||
-- Задача 3
|
||||
SELECT id, name, category, price
|
||||
FROM products
|
||||
WHERE price >= 100
|
||||
AND category IN ('tablet', 'smartphone', 'mouse')
|
||||
ORDER BY price DESC;
|
||||
|
||||
|
||||
-- Задача 4
|
||||
SELECT name, category, price
|
||||
FROM products
|
||||
WHERE name NOT LIKE '%iPhone%';
|
||||
|
||||
|
||||
-- Задача 5
|
||||
SELECT name, category, price
|
||||
FROM products
|
||||
WHERE name ILIKE '%pro%';
|
||||
|
||||
SELECT name, category, price
|
||||
FROM products
|
||||
WHERE name ILIKE '%mx%'
|
||||
ORDER BY price DESC;
|
||||
|
||||
-- Вопрос:
|
||||
-- В одном предложении объясни, почему для поисковой строки лучше ILIKE '%pro%', а не = 'Pro'.
|
||||
-- Ответ:
|
||||
-- В названии товаров могут быть буквы (в словах) с разными регистрами, при поиске клиенту можен не показать часть товаров только потому что слово выглядить так - PRO а не так - Pro.
|
||||
@@ -0,0 +1,144 @@
|
||||
-- Задание 1
|
||||
SELECT o.id AS order_id,
|
||||
o.order_date,
|
||||
o.status,
|
||||
u.name AS user_name,
|
||||
u.country
|
||||
FROM orders o
|
||||
JOIN users u ON o.user_id = u.id;
|
||||
|
||||
-- Ответ на вопросы
|
||||
-- 1. Логичнее начать c orders, так как нам в первую очередь нужны все заказы,
|
||||
-- а потом только пользователи которые их сделали.
|
||||
|
||||
-- 2. Я думаю уместнее сделать LEFT.
|
||||
-- Я пробовал сделать с INNER и LEFT и у меня не пропали строки.
|
||||
-- Получается это из за того что у каждого заказа есть пользователь, не дуаю что должен создастся заказ без пользователя.
|
||||
|
||||
|
||||
-- Задание 2
|
||||
SELECT o.id AS order_id,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM orders o
|
||||
JOIN order_items oi ON oi.order_id = o.id
|
||||
JOIN products p ON p.id = oi.product_id
|
||||
WHERE o.id = 101;
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- Выдаст дублированный состав заказа с id 101,
|
||||
-- получается он будет обращаться к каждаму заказу, но отображать продукты заказа с id 101
|
||||
|
||||
|
||||
-- Задание 3
|
||||
SELECT o.id AS order_id,
|
||||
u.name AS name,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM orders o
|
||||
JOIN users u ON u.id = o.user_id
|
||||
JOIN order_items oi ON oi.order_id = o.id
|
||||
JOIN products p ON p.id = oi.product_id;
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- 1. Появляется дубликат заказов потому что у 3 заказа есть несколько товаров, join превращает(размножает) их в несколько строк.
|
||||
-- 2. Отличается тем что мы выводим все заказы с товарами а не один заказ.
|
||||
|
||||
|
||||
-- Задание 4
|
||||
SELECT u.name AS user_name,
|
||||
o.id AS order_id,
|
||||
o.status
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON o.user_id = u.id;
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- INNER JOIN не выдаст пользователей без заказа. Только тех у кого есть пара с orders.
|
||||
|
||||
|
||||
-- Задание 5
|
||||
SELECT u.id AS user_id,
|
||||
u.name AS user_name
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON o.user_id = u.id
|
||||
WHERE o.id IS NULL;
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- 1. Проверять IS NULL нужно по полю orders.id
|
||||
-- 2. Мы должны в запросе получить всех пользователей, а потом в фильтре "узнать" есть ли у них заказы.
|
||||
|
||||
|
||||
-- Задание 6
|
||||
SELECT o.id AS order_id,
|
||||
u.name AS user_name,
|
||||
p.name AS product_name,
|
||||
oi.quantity
|
||||
FROM orders o
|
||||
JOIN users u ON u.id = o.user_id
|
||||
JOIN order_items oi ON oi.order_id = o.id
|
||||
JOIN products p ON p.id = oi.product_id
|
||||
WHERE p.name ILIKE '%kindle%';
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- В заказе 2 есть только один товар с названием Kindle.
|
||||
|
||||
|
||||
-- Задание 7
|
||||
SELECT p.name AS product_name,
|
||||
oi.order_id,
|
||||
oi.quantity
|
||||
FROM products p
|
||||
LEFT JOIN order_items oi ON oi.product_id = p.id;
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- 1. order_id будет NULL
|
||||
-- 2. Если использовать INNER JOIN то он не покажет товар который не заказывали.
|
||||
|
||||
|
||||
-- Задание 8
|
||||
SELECT o.id AS order_id,
|
||||
u.name AS user_name,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM products p
|
||||
JOIN order_items oi ON oi.product_id = p.id
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
JOIN users u ON u.id = o.user_id
|
||||
WHERE o.status = 'paid';
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- У меня получился тот же результат если написать вот так - JOIN orders o ON o.id = oi.order_id and o.status = 'paid'
|
||||
|
||||
|
||||
-- Задание 9
|
||||
SELECT o.id AS order_id,
|
||||
o.order_date,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
oi.price_at_purchase
|
||||
FROM products p
|
||||
JOIN order_items oi ON oi.product_id = p.id
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
JOIN users u ON u.id = o.user_id
|
||||
WHERE u.name = 'Alice';
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- Если у пользователя нет ни одного заказа, то этот запрос вернет пустую строку.
|
||||
-- Не вернет потому что нечего отобразить таблице, если нет заказов то и нет купленных товаров.
|
||||
-- Но если прописать RIGHT JOIN users u ON u.id = o.user_id то отобразит все строки NULL.
|
||||
|
||||
|
||||
-- Задание 10
|
||||
SELECT o.id AS order_id,
|
||||
o.user_id,
|
||||
o.status
|
||||
FROM orders o
|
||||
LEFT JOIN order_items oi ON oi.order_id = o.id
|
||||
WHERE oi.order_id IS NULL;
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- Такой запрос выдаст пустые строки.
|
||||
-- INNER JOIN должен выдать строки только если есть "пара".
|
||||
@@ -0,0 +1,73 @@
|
||||
-- Задание 1
|
||||
SELECT id, name, country, created_at, email
|
||||
FROM users
|
||||
ORDER BY created_at DESC, name;
|
||||
|
||||
-- Почему выбрал именно эти столбцы и зачем такая сортировка?
|
||||
-- Ответ на вопрос:
|
||||
-- Эти столбцы нужны для анализа пользователей, для тех же аналитиков.
|
||||
-- Сортировка по дате и имени, я так предпологаю, нужня для удобства.
|
||||
|
||||
|
||||
-- Задание 2
|
||||
-- Запрос 1
|
||||
SELECT id, name, country
|
||||
FROM users
|
||||
WHERE country = 'US'
|
||||
ORDER BY name;
|
||||
|
||||
-- Запрос 2
|
||||
SELECT id, name, country
|
||||
FROM users
|
||||
WHERE country = 'UK'
|
||||
ORDER BY name;
|
||||
|
||||
-- Какой из двух запросов маркетологу полезнее и почему?
|
||||
-- Ответ на вопрос:
|
||||
-- Я так понимаю маркетологу нужно понять какое кол-во пользователей в каждой из стран зарегестрировалось в приложении.
|
||||
-- Возможно для понимания продвижения продукта и насколько успешна маркетинговая стратегия. И в дальнейшем её корректировка.
|
||||
|
||||
|
||||
-- Задание 3
|
||||
-- Запрос 1
|
||||
SELECT id, name, email
|
||||
FROM users
|
||||
WHERE email IS NOT NULL -- я знаю что email и так должен быть NOT NULL по DDL, но на всякий прописал
|
||||
ORDER BY name;
|
||||
|
||||
-- Запрос 2
|
||||
SELECT DISTINCT LOWER(email) as email_lower
|
||||
FROM users
|
||||
WHERE email IS NOT NULL;
|
||||
|
||||
-- Зачем вообще приводить email к одному регистру и убирать дубликаты на стороне базы?
|
||||
-- Ответ на вопрос:
|
||||
-- Приводить email к нижнему регистру необходимо для того чтобы небыло дубликатов почты.
|
||||
-- После гугления я узнал что в почтовых адресах могут допускаться верхние регистры букв, только потом создать email с теми же буквами но в нижнем регистре нельзя будет, они будут считаться индентичными.
|
||||
-- Получается несколько пользователей могут залогинится под одним и тем же email адресом но с разными регистрами. А такое нельзя допускать.
|
||||
-- Для рассылки дубликаты нужно убирать что-бы не приходила рассылка дважды в один и тот же email.
|
||||
|
||||
-- Задание 4
|
||||
-- Запрос 1
|
||||
SELECT id, name, created_at
|
||||
FROM users
|
||||
WHERE created_at > '2024-01-14'
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- Запрос 2
|
||||
SELECT id, name, created_at
|
||||
FROM users
|
||||
WHERE created_at > '2024-01-14'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 3;
|
||||
|
||||
-- Запрос 3
|
||||
SELECT id, name, created_at
|
||||
FROM users
|
||||
WHERE created_at > '2024-01-14'
|
||||
ORDER BY created_at DESC
|
||||
OFFSET 1 LIMIT 2;
|
||||
|
||||
-- В каком реальном сценарии тебе пригодится OFFSET?
|
||||
-- Ответ на вопрос:
|
||||
-- Не знаю, возможно понадобится посмотреть несколько записей из середины списка.
|
||||
@@ -0,0 +1,49 @@
|
||||
-- Задание 1
|
||||
SELECT id, name, 'active' AS user_status
|
||||
FROM users
|
||||
WHERE is_active = TRUE
|
||||
UNION ALL
|
||||
SELECT id, name, 'inactive' AS user_status
|
||||
FROM users
|
||||
WHERE is_active = FALSE
|
||||
ORDER BY name;
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- 1. Количество строк не изменилось
|
||||
-- 2. Предположу, что подача информации в таком виде более информативней(понятней).
|
||||
|
||||
|
||||
-- Задание 2
|
||||
SELECT id AS order_id, user_id, order_date, 'old' AS period
|
||||
FROM orders
|
||||
WHERE order_date < '2024-01-01'
|
||||
UNION ALL
|
||||
SELECT id AS order_id, user_id, order_date, 'new' AS period
|
||||
FROM orders
|
||||
WHERE order_date >= '2024-01-01'
|
||||
ORDER BY order_date, order_id;
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- 1. В dbeaver в самой левой колонке отображается номерация строк, а снизу общее количество строк который выдал запрос.
|
||||
-- На последней строке с old можно увидеть число в самой левой колонке, это и есть количество строк c old. Далее отнимаем общее кол-во строк от old и получаем кол-во new.
|
||||
-- 2. Тяжело ответить на такой вопрос, не знаю. Если у меня были бы сомнения, я бы у новичка спросил.
|
||||
|
||||
|
||||
-- Задание 3
|
||||
SELECT DISTINCT p.id AS product_id,
|
||||
p.name AS product_name,
|
||||
'ordered' AS product_status
|
||||
FROM products p
|
||||
JOIN order_items oi ON oi.product_id = p.id
|
||||
UNION ALL
|
||||
SELECT DISTINCT p.id AS product_id,
|
||||
p.name AS product_name,
|
||||
'never_ordered' AS product_status
|
||||
FROM products p
|
||||
LEFT JOIN order_items oi ON oi.product_id = p.id
|
||||
WHERE oi.product_id IS NULL
|
||||
ORDER BY product_name;
|
||||
|
||||
-- Ответ на вопрос
|
||||
-- 1. Нам нужно чтобы товар(products) ни разу не заказывали(order_items), получается что мы проверяем есть ли ссылка в каком нибудь заказе на продукт.
|
||||
-- 2. Дает сигнал бизнесу что товар не хотят покупать, возможно на него нужно сделать скидку или прорекламировать товар у блогера какого нибудь.
|
||||
Reference in New Issue
Block a user