homework, pet project DE Dmitry_Medvedev #1

Open
dmitry_medvedev wants to merge 266 commits from md_branch into main
94 changed files with 10227 additions and 9 deletions
Showing only changes of commit af820f2324 - Show all commits
@@ -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

@@ -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;
+38
View File
@@ -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)
);
+59
View File
@@ -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%';
@@ -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
1 course_id title category created_at
2 1 SQL для начинающих data 2023-01-01 00:00:00.000
3 5 Основы машинного обучения data 2023-03-15 00:00:00.000
4 2 Python для анализа данных programming 2023-01-15 00:00:00.000
5 3 BI с нуля business 2023-03-01 00:00:00.000
6 4 Excel для аналитиков business 2023-02-01 00:00:00.000
@@ -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
1 lesson_id title duration_min course_id
2 2 JOIN 15 1
3 3 WHERE и фильтрация 12 1
4 4 Агрегации и GROUP BY 18 1
5 6 Pandas основы 20 2
6 7 NumPy 15 2
7 8 Визуализация данных 22 2
8 9 Основы BI 12 3
9 10 Метрики и дашборды 18 3
10 13 Сводные таблицы 18 4
11 16 Линейная регрессия 30 5
12 18 Кластеризация 25 5
13 1 SELECT и FROM 10 1
14 5 Подзапросы 25 1
15 11 Power BI введение 20 3
16 12 Формулы в Excel 15 4
17 14 Графики и диаграммы 14 4
18 15 Введение в ML 20 5
19 17 Классификация 28 5
@@ -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
1 user_id name age email registration_date
2 2 Bob 30 bob@gmail.com 2023-02-05 00:00:00.000
3 3 Charlie 22 charlie@mail.com 2023-02-20 00:00:00.000
4 4 Diana 28 diana@mail.com 2023-03-01 00:00:00.000
5 6 Frank 29 frank@mail.com 2023-01-05 00:00:00.000
6 7 Grace 26 grace@gmail.com 2023-01-12 00:00:00.000
7 8 Henry 32 henry@mail.com 2023-01-20 00:00:00.000
8 9 Iris 24 iris@gmail.com 2023-02-10 00:00:00.000
9 10 Jack 27 jack@mail.com 2023-04-15 00:00:00.000
10 13 Mia 26 mia@gmail.com 2023-01-18 00:00:00.000
11 16 Paul 29 paul@mail.com 2023-02-15 00:00:00.000
12 18 Rita 27 rita@mail.com 2023-04-01 00:00:00.000
13 19 Sam 30 sam@gmail.com 2023-04-10 00:00:00.000
14 1 Alice 25 alice@mail.com 2023-01-10 00:00:00.000
15 5 Ethan 35 ethan@gmail.com 2023-03-10 00:00:00.000
16 11 Kelly 23 kelly@gmail.com 2023-04-20 00:00:00.000
17 12 Leo 31 leo@mail.com 2023-01-15 00:00:00.000
18 14 Nina 28 nina@mail.com 2023-01-08 00:00:00.000
19 15 Oscar 33 oscar@gmail.com 2023-01-10 00:00:00.000
20 17 Quinn 24 quinn@gmail.com 2023-03-20 00:00:00.000
21 20 Tina 25 tina@mail.com 2023-04-18 00:00:00.000
@@ -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
1 user_id enrolled_at course_id
2 2 2023-01-20 00:00:00.000 1
3 3 2023-03-05 00:00:00.000 2
4 6 2023-01-06 00:00:00.000 1
5 6 2023-01-10 00:00:00.000 2
6 6 2023-02-05 00:00:00.000 4
7 7 2023-01-13 00:00:00.000 1
8 7 2023-03-10 00:00:00.000 3
9 8 2023-01-22 00:00:00.000 3
10 13 2023-01-19 00:00:00.000 2
11 13 2023-02-20 00:00:00.000 4
12 16 2023-02-16 00:00:00.000 2
13 16 2023-02-16 00:00:00.000 4
14 19 2023-04-11 00:00:00.000 1
15 19 2023-04-11 00:00:00.000 2
16 2 2023-03-15 00:00:00.000 3
17 4 2023-03-10 00:00:00.000 2
18 7 2023-02-01 00:00:00.000 2
19 9 2023-02-15 00:00:00.000 1
20 18 2023-04-05 00:00:00.000 4
21 4 2023-04-01 00:00:00.000 3
22 8 2023-01-22 00:00:00.000 1
23 8 2023-01-22 00:00:00.000 2
24 9 2023-02-12 00:00:00.000 4
25 10 2023-04-16 00:00:00.000 1
26 18 2023-04-02 00:00:00.000 1
27 3 2023-02-25 00:00:00.000 1
28 6 2023-03-08 00:00:00.000 3
29 1 2023-02-01 00:00:00.000 2
30 11 2023-04-21 00:00:00.000 2
31 14 2023-01-09 00:00:00.000 2
32 14 2023-01-09 00:00:00.000 3
33 14 2023-03-20 00:00:00.000 5
34 17 2023-03-22 00:00:00.000 3
35 20 2023-04-19 00:00:00.000 2
36 12 2023-03-10 00:00:00.000 3
37 1 2023-01-15 00:00:00.000 1
38 12 2023-01-16 00:00:00.000 1
39 14 2023-01-09 00:00:00.000 1
40 14 2023-02-01 00:00:00.000 4
41 15 2023-01-11 00:00:00.000 1
42 15 2023-03-05 00:00:00.000 3
43 17 2023-03-25 00:00:00.000 5
44 5 2023-03-20 00:00:00.000 5
45 1 2023-02-10 00:00:00.000 4
46 15 2023-02-01 00:00:00.000 2
@@ -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
1 user_id lesson_id course_id viewed_at
2 3 6 2 2023-03-06 12:00:00.000
3 4 9 3 2023-04-02 10:00:00.000
4 4 6 2 2023-03-12 14:00:00.000
5 4 7 2 2023-03-13 09:00:00.000
6 6 3 1 2023-01-08 09:00:00.000
7 6 6 2 2023-01-11 14:00:00.000
8 6 7 2 2023-01-12 10:00:00.000
9 6 8 2 2023-01-13 11:00:00.000
10 6 9 3 2023-03-09 11:00:00.000
11 7 9 3 2023-03-11 14:00:00.000
12 2 9 3 2023-03-16 10:00:00.000
13 2 10 3 2023-03-16 10:30:00.000
14 3 2 1 2023-02-26 11:20:00.000
15 4 10 3 2023-04-03 10:00:00.000
16 6 2 1 2023-01-07 10:15:00.000
17 6 4 1 2023-01-08 09:30:00.000
18 6 13 4 2023-02-06 09:30:00.000
19 7 2 1 2023-01-14 10:20:00.000
20 7 3 1 2023-01-15 09:00:00.000
21 7 10 3 2023-03-11 14:30:00.000
22 9 13 4 2023-02-13 10:30:00.000
23 13 6 2 2023-01-20 14:00:00.000
24 13 7 2 2023-01-21 10:00:00.000
25 2 1 1 2023-01-21 09:00:00.000
26 6 1 1 2023-01-07 10:00:00.000
27 6 14 4 2023-02-07 10:00:00.000
28 7 1 1 2023-01-14 10:00:00.000
29 9 12 4 2023-02-13 10:00:00.000
30 9 1 1 2023-02-16 11:00:00.000
31 3 1 1 2023-02-26 11:00:00.000
32 6 5 1 2023-01-09 10:00:00.000
33 6 12 4 2023-02-06 09:00:00.000
34 13 12 4 2023-02-21 09:00:00.000
35 1 2 1 2023-01-16 10:15:00.000
36 1 3 1 2023-01-17 09:00:00.000
37 1 4 1 2023-01-17 09:30:00.000
38 1 4 1 2023-01-18 10:00:00.000
39 1 6 2 2023-02-02 14:00:00.000
40 1 13 4 2023-02-15 10:30:00.000
41 5 16 5 2023-03-22 11:00:00.000
42 12 2 1 2023-01-17 10:20:00.000
43 12 3 1 2023-04-10 11:30:00.000
44 14 2 1 2023-01-09 08:15:00.000
45 14 4 1 2023-01-10 10:30:00.000
46 14 6 2 2023-01-10 14:00:00.000
47 14 9 3 2023-01-10 08:00:00.000
48 1 4 1 2023-01-17 11:00:00.000
49 12 9 3 2023-03-12 10:00:00.000
50 14 3 1 2023-01-10 10:00:00.000
51 14 7 2 2023-01-11 10:00:00.000
52 14 8 2 2023-01-12 11:00:00.000
53 1 12 4 2023-02-15 10:00:00.000
54 12 1 1 2023-01-17 10:00:00.000
55 14 5 1 2023-01-11 09:00:00.000
56 1 1 1 2023-01-16 10:00:00.000
57 5 15 5 2023-03-21 10:00:00.000
58 12 1 1 2023-04-10 11:00:00.000
59 14 1 1 2023-01-09 08:00:00.000
@@ -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);
@@ -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');
@@ -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');
@@ -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');
@@ -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;
@@ -0,0 +1,2 @@
Разница есть только в таблицах mv_lesson_popularity_summary
Где в Greenplum - NULL, в clickhouse - 0. Потому что clickhouser сам подставляет 0 вместо NULL.
@@ -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;
-- Че то кликхаус странно себя ведет, то таблицы сами очищаются, то удаленные таблицы воскресают.
@@ -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;
@@ -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, тк нужно вывести имя для каждого лога а не юзера.
@@ -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;
@@ -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');
@@ -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 Индекс не будет использоваться.
@@ -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))
@@ -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');
@@ -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;
-- Че то кликхаус странно себя ведет, то таблицы сами очищаются, то удаленные таблицы воскресают.
@@ -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;
@@ -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
1 id title category created_at
2 1 SQL для начинающих data 2023-01-01 00:00:00.000 +0300
3 2 Python для анализа данных programming 2023-01-15 00:00:00.000 +0300
4 3 BI с нуля business 2023-03-01 00:00:00.000 +0300
5 4 Excel для аналитиков business 2023-02-01 00:00:00.000 +0300
6 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
1 id user_id course_id enrolled_at
2 1 1 1 2023-01-15 00:00:00.000 +0300
3 2 1 2 2023-02-01 00:00:00.000 +0300
4 3 2 1 2023-01-20 00:00:00.000 +0300
5 4 3 2 2023-03-05 00:00:00.000 +0300
6 5 4 3 2023-04-01 00:00:00.000 +0300
7 6 6 1 2023-01-06 00:00:00.000 +0300
8 7 6 2 2023-01-10 00:00:00.000 +0300
9 8 6 4 2023-02-05 00:00:00.000 +0300
10 9 7 1 2023-01-13 00:00:00.000 +0300
11 10 7 3 2023-03-10 00:00:00.000 +0300
12 11 8 1 2023-01-22 00:00:00.000 +0300
13 12 8 2 2023-01-22 00:00:00.000 +0300
14 13 8 3 2023-01-22 00:00:00.000 +0300
15 14 9 4 2023-02-12 00:00:00.000 +0300
16 15 10 1 2023-04-16 00:00:00.000 +0300
17 16 11 2 2023-04-21 00:00:00.000 +0300
18 17 12 1 2023-01-16 00:00:00.000 +0300
19 18 13 2 2023-01-19 00:00:00.000 +0300
20 19 13 4 2023-02-20 00:00:00.000 +0300
21 20 14 1 2023-01-09 00:00:00.000 +0300
22 21 14 2 2023-01-09 00:00:00.000 +0300
23 22 14 3 2023-01-09 00:00:00.000 +0300
24 23 14 4 2023-02-01 00:00:00.000 +0300
25 24 14 5 2023-03-20 00:00:00.000 +0300
26 25 15 1 2023-01-11 00:00:00.000 +0300
27 26 15 3 2023-03-05 00:00:00.000 +0300
28 27 16 2 2023-02-16 00:00:00.000 +0300
29 28 16 4 2023-02-16 00:00:00.000 +0300
30 29 17 3 2023-03-22 00:00:00.000 +0300
31 30 17 5 2023-03-25 00:00:00.000 +0300
32 31 18 1 2023-04-02 00:00:00.000 +0300
33 32 19 1 2023-04-11 00:00:00.000 +0300
34 33 19 2 2023-04-11 00:00:00.000 +0300
35 34 20 2 2023-04-19 00:00:00.000 +0300
36 35 5 5 2023-03-20 00:00:00.000 +0300
37 36 1 4 2023-02-10 00:00:00.000 +0300
38 37 2 3 2023-03-15 00:00:00.000 +0300
39 38 3 1 2023-02-25 00:00:00.000 +0300
40 39 4 2 2023-03-10 00:00:00.000 +0300
41 40 6 3 2023-03-08 00:00:00.000 +0300
42 41 7 2 2023-02-01 00:00:00.000 +0300
43 42 9 1 2023-02-15 00:00:00.000 +0300
44 43 12 3 2023-03-10 00:00:00.000 +0300
45 44 15 2 2023-02-01 00:00:00.000 +0300
46 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
1 id user_id lesson_id viewed_at
2 1 1 1 2023-01-16 10:00:00.000 +0300
3 2 1 2 2023-01-16 10:15:00.000 +0300
4 3 1 3 2023-01-17 09:00:00.000 +0300
5 4 1 4 2023-01-17 09:30:00.000 +0300
6 5 1 4 2023-01-17 11:00:00.000 +0300
7 6 1 4 2023-01-18 10:00:00.000 +0300
8 7 1 6 2023-02-02 14:00:00.000 +0300
9 8 1 12 2023-02-15 10:00:00.000 +0300
10 9 1 13 2023-02-15 10:30:00.000 +0300
11 10 2 1 2023-01-21 09:00:00.000 +0300
12 11 2 9 2023-03-16 10:00:00.000 +0300
13 12 2 10 2023-03-16 10:30:00.000 +0300
14 13 3 6 2023-03-06 12:00:00.000 +0300
15 14 3 1 2023-02-26 11:00:00.000 +0300
16 15 3 2 2023-02-26 11:20:00.000 +0300
17 16 4 9 2023-04-02 10:00:00.000 +0300
18 17 4 10 2023-04-03 10:00:00.000 +0300
19 18 4 6 2023-03-12 14:00:00.000 +0300
20 19 4 7 2023-03-13 09:00:00.000 +0300
21 20 5 15 2023-03-21 10:00:00.000 +0300
22 21 5 16 2023-03-22 11:00:00.000 +0300
23 22 6 1 2023-01-07 10:00:00.000 +0300
24 23 6 2 2023-01-07 10:15:00.000 +0300
25 24 6 3 2023-01-08 09:00:00.000 +0300
26 25 6 4 2023-01-08 09:30:00.000 +0300
27 26 6 5 2023-01-09 10:00:00.000 +0300
28 27 6 6 2023-01-11 14:00:00.000 +0300
29 28 6 7 2023-01-12 10:00:00.000 +0300
30 29 6 8 2023-01-13 11:00:00.000 +0300
31 30 6 12 2023-02-06 09:00:00.000 +0300
32 31 6 13 2023-02-06 09:30:00.000 +0300
33 32 6 14 2023-02-07 10:00:00.000 +0300
34 33 6 9 2023-03-09 11:00:00.000 +0300
35 34 7 1 2023-01-14 10:00:00.000 +0300
36 35 7 2 2023-01-14 10:20:00.000 +0300
37 36 7 3 2023-01-15 09:00:00.000 +0300
38 37 7 9 2023-03-11 14:00:00.000 +0300
39 38 7 10 2023-03-11 14:30:00.000 +0300
40 39 9 12 2023-02-13 10:00:00.000 +0300
41 40 9 13 2023-02-13 10:30:00.000 +0300
42 41 9 1 2023-02-16 11:00:00.000 +0300
43 42 12 1 2023-01-17 10:00:00.000 +0300
44 43 12 2 2023-01-17 10:20:00.000 +0300
45 44 12 1 2023-04-10 11:00:00.000 +0300
46 45 12 3 2023-04-10 11:30:00.000 +0300
47 46 12 9 2023-03-12 10:00:00.000 +0300
48 47 13 6 2023-01-20 14:00:00.000 +0300
49 48 13 7 2023-01-21 10:00:00.000 +0300
50 49 13 12 2023-02-21 09:00:00.000 +0300
51 50 14 1 2023-01-09 08:00:00.000 +0300
52 51 14 2 2023-01-09 08:15:00.000 +0300
53 52 14 3 2023-01-10 10:00:00.000 +0300
54 53 14 4 2023-01-10 10:30:00.000 +0300
55 54 14 5 2023-01-11 09:00:00.000 +0300
56 55 14 6 2023-01-10 14:00:00.000 +0300
57 56 14 7 2023-01-11 10:00:00.000 +0300
58 57 14 8 2023-01-12 11:00:00.000 +0300
59 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
1 id course_id title duration_min
2 1 1 SELECT и FROM 10
3 2 1 JOIN 15
4 3 1 WHERE и фильтрация 12
5 4 1 Агрегации и GROUP BY 18
6 5 1 Подзапросы 25
7 6 2 Pandas основы 20
8 7 2 NumPy 15
9 8 2 Визуализация данных 22
10 9 3 Основы BI 12
11 10 3 Метрики и дашборды 18
12 11 3 Power BI введение 20
13 12 4 Формулы в Excel 15
14 13 4 Сводные таблицы 18
15 14 4 Графики и диаграммы 14
16 15 5 Введение в ML 20
17 16 5 Линейная регрессия 30
18 17 5 Классификация 28
19 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
1 id name age email registration_date
2 1 Alice 25 alice@mail.com 2023-01-10 00:00:00.000 +0300
3 2 Bob 30 bob@gmail.com 2023-02-05 00:00:00.000 +0300
4 3 Charlie 22 charlie@mail.com 2023-02-20 00:00:00.000 +0300
5 4 Diana 28 diana@mail.com 2023-03-01 00:00:00.000 +0300
6 5 Ethan 35 ethan@gmail.com 2023-03-10 00:00:00.000 +0300
7 6 Frank 29 frank@mail.com 2023-01-05 00:00:00.000 +0300
8 7 Grace 26 grace@gmail.com 2023-01-12 00:00:00.000 +0300
9 8 Henry 32 henry@mail.com 2023-01-20 00:00:00.000 +0300
10 9 Iris 24 iris@gmail.com 2023-02-10 00:00:00.000 +0300
11 10 Jack 27 jack@mail.com 2023-04-15 00:00:00.000 +0300
12 11 Kelly 23 kelly@gmail.com 2023-04-20 00:00:00.000 +0300
13 12 Leo 31 leo@mail.com 2023-01-15 00:00:00.000 +0300
14 13 Mia 26 mia@gmail.com 2023-01-18 00:00:00.000 +0300
15 14 Nina 28 nina@mail.com 2023-01-08 00:00:00.000 +0300
16 15 Oscar 33 oscar@gmail.com 2023-01-10 00:00:00.000 +0300
17 16 Paul 29 paul@mail.com 2023-02-15 00:00:00.000 +0300
18 17 Quinn 24 quinn@gmail.com 2023-03-20 00:00:00.000 +0300
19 18 Rita 27 rita@mail.com 2023-04-01 00:00:00.000 +0300
20 19 Sam 30 sam@gmail.com 2023-04-10 00:00:00.000 +0300
21 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 - собрать на мастер.
@@ -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)
);
@@ -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) снять с продажи
@@ -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 гипотезы)? Ответ: «доставка пробников/тестовой партии», «использование купонов/бонусов», «технический тест витрины»
-- Проще увидеть глазами отфильтрованные микро-заказы
@@ -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;
-- По такому графику мы можем видеть РОСТ выручки по дням например, а не просто сколько в какой день принесла категория
@@ -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 сделать, и вызывать когда надо
@@ -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
-- Дедлок возникает потому что каждая сессия заблокировал то, что нужно другой сессии. База отменит одну из сессий, чтобы прошла хоть какая-то
-- Чтобы дедлок не возникал, нужно обновления все производить в одном порядке
-- Если при массовых обновлениях не соблюдать порядок строк, то будет возникать жопа из дедлоков
@@ -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;
-- Ну чето все равно не видна разница во времени выполнения, но по логике должен быть быстрее второй запрос
@@ -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 запрос самый медленный, а первые два шустрые
@@ -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 всего один раз
@@ -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)
@@ -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)
);
@@ -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');
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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. Шардинг против партиционирования
-- При партицировании таблицы данные хранятся на одной бд, внутри одной таблицы, но эта таблица внутри себя разделена на партиции для ускорения запросов к ней.
-- При шардировании таблицы данные хранятся на разных серверах, то есть таблица делится на части(при горизонтальном шардировании), и каждая часть таблицы отправляется на разные сервер.
-- Пример партицирования без шардирования: когда сервер не переполнен, но таблица слишком большая, и и запросы к ней замедляются, тогда таблица делится на партиции, для более быстрого выполнения запросов к ней.
-- Пример когда без шардирования не обойтись: это когда сервер перегружен, и физически невозможно добавлять новые данные и чтение таблиц очень медленное, тк для этого чтения сервер не может выделять должное количество ресурсов.
@@ -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. сложность настройки - я думаю имеется ввиду защита от конфликтов и поддержка согласованности.
-- * у меня в голове только мысль о разделении отвественности по хэшу, что одна мастер реплика отвечает за один кусок, другая за другой, по типу шардирования, только без деления таблиц, они как бы и дальше между собой реплицируются, но каждая изменят обновляет и делает записи только на своем куске. Наверное такой способ поможет при больших таблицах с частыми изменениями(хотя по суть шардирование лучше испольщовать тогда, а не мульти-мастер). Неэффективно будет на таблицах с редкими изменениями, и не очень больших таблицах.
@@ -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 - можно немного потерпеть неточность, тк это некритично
@@ -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, то билет может достаться и тому и тому, что недопустимо
+38
View File
@@ -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)
);
+41
View File
@@ -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);
+56
View File
@@ -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.
+144
View File
@@ -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 должен выдать строки только если есть "пара".
+73
View File
@@ -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?
-- Ответ на вопрос:
-- Не знаю, возможно понадобится посмотреть несколько записей из середины списка.
+49
View File
@@ -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. Дает сигнал бизнесу что товар не хотят покупать, возможно на него нужно сделать скидку или прорекламировать товар у блогера какого нибудь.