Merge branch 'dmezentsev_postgresql_DEIN-2902' of https://gitea.de-infra.servehttp.com/de-infrastructure/DWH into dmezentsev_postgresql_DEIN-2902
This commit is contained in:
@@ -0,0 +1,117 @@
|
|||||||
|
--Типы данных
|
||||||
|
|
||||||
|
--Задача 1
|
||||||
|
CREATE TABLE raw_data
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
user_id String,
|
||||||
|
balance String,
|
||||||
|
created_at String
|
||||||
|
) ENGINE = MergeTree ORDER BY tuple();
|
||||||
|
|
||||||
|
SELECT DISTINCT
|
||||||
|
toDecimal64OrNull(balance, 2) AS balance_decimal,
|
||||||
|
toDateTime64OrNull(created_at, 3) AS created_at_date
|
||||||
|
FROM raw_data
|
||||||
|
WHERE
|
||||||
|
toDecimal64OrNull(balance, 2) IS NOT NULL
|
||||||
|
AND toDateTime64OrNull(created_at, 3) IS NOT NULL
|
||||||
|
|
||||||
|
/*
|
||||||
|
CAST(balance AS Decimal(10, 2)) - этом случае будет ошибка, так как данный метод не поддеживает NULL
|
||||||
|
|
||||||
|
Следующие два метода по сути своей преобразование типа данных с условием ИЛИ, то есть:
|
||||||
|
toDecimal64OrNull(balance, 2) - в этом методе на выходе будут корректные данные, в противном случае NULL
|
||||||
|
toDecimal64OrZero(balance, 2) - аналогично с NULL, только будет ноль.
|
||||||
|
*/
|
||||||
|
|
||||||
|
--Задача 2
|
||||||
|
CREATE TABLE events
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
event_id String,
|
||||||
|
event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid"
|
||||||
|
event_time String -- форматы: "12:30:45", "25:99:99"
|
||||||
|
)
|
||||||
|
ENGINE = MergeTree
|
||||||
|
ORDER BY event_id;
|
||||||
|
|
||||||
|
--Заполнил данными для проверки
|
||||||
|
INSERT INTO events (event_id, event_date, event_time) VALUES
|
||||||
|
('1', '2023-01-01', '12:30:45'),
|
||||||
|
('2', '01.02.2023', '08:15:00'),
|
||||||
|
('3', 'invalid', '25:99:99'),
|
||||||
|
('4', '2023-05-20', 'invalid'),
|
||||||
|
('5', '31.12.2023', '23:59:59');
|
||||||
|
|
||||||
|
SELECT * FROM events;
|
||||||
|
|
||||||
|
SELECT DISTINCT
|
||||||
|
toDate(DateTime_Valid) AS Event_Date,
|
||||||
|
toHour(DateTime_Valid) AS Event_Hour,
|
||||||
|
DateTime_Valid
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS DateTime_Valid
|
||||||
|
FROM events
|
||||||
|
)
|
||||||
|
WHERE DateTime_Valid IS NOT NULL;
|
||||||
|
|
||||||
|
--Задача 3
|
||||||
|
DROP TABLE IF EXISTS user_activity ON CLUSTER clickhouse_cluster;
|
||||||
|
|
||||||
|
CREATE TABLE user_activity
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
user_id UInt32,
|
||||||
|
actions String -- JSON: ["login", "purchase", "logout"]
|
||||||
|
) ENGINE = MergeTree ORDER BY tuple();
|
||||||
|
|
||||||
|
--Тест данные
|
||||||
|
INSERT INTO user_activity (user_id, actions) VALUES
|
||||||
|
(1, '["login", "purchase", "logout"]'),
|
||||||
|
(2, '["login", "logout"]'),
|
||||||
|
(3, '["login"]'),
|
||||||
|
(4, '["login","purchase"]');
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
-- Сначала реобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Потом считваем длину массива и дальше суммируем длину всех массивов действий для каждого пользователя
|
||||||
|
sum(length(JSONExtract(actions, 'Array(String)'))) AS count_actions
|
||||||
|
FROM user_activity
|
||||||
|
WHERE
|
||||||
|
-- Проверяем наличие 'purchase' в массиве
|
||||||
|
has(JSONExtract(actions, 'Array(String)'), 'purchase')
|
||||||
|
GROUP BY
|
||||||
|
user_id;
|
||||||
|
/*
|
||||||
|
Преобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Нашел пользователей в новом массиве Array(String), совершивших действие "purchase".
|
||||||
|
Просто написать actions = 'purchase' не получится, так как изначально actions просто (String) и соответсвенно "["login", "purchase", "logout"]" нельзя сравнить с "purchase". Поэтому сначала преобразовываем в массив, а потом с функцией has(array, elem) ищем то, что нужно.
|
||||||
|
То есть менять бы не пришлось, если бы в actions было изначально разные значения в каждой строке ("login", "purchase" или "logout")
|
||||||
|
*/
|
||||||
|
|
||||||
|
--Задача 4
|
||||||
|
DROP TABLE IF EXISTS sensor_data ON CLUSTER clickhouse_cluster;
|
||||||
|
|
||||||
|
CREATE TABLE sensor_data
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
sensor_id UInt32,
|
||||||
|
value Float64,
|
||||||
|
status Enum('ok' = 1, 'error' = 2)
|
||||||
|
) ORDER BY sensor_id;
|
||||||
|
|
||||||
|
--Тест данные
|
||||||
|
INSERT INTO sensor_data (sensor_id, value, status) VALUES
|
||||||
|
(1, 25.5, 'ok'),
|
||||||
|
(2, 120.75, 'error'),
|
||||||
|
(3, 15.0, 1),
|
||||||
|
(4, 9999999.99, 'ok'),
|
||||||
|
(5, 0.001, 'ok');
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
toString(status) AS status_string,
|
||||||
|
avg(value) AS avg_value --Среднее значение value для каждого status
|
||||||
|
FROM sensor_data
|
||||||
|
WHERE accurateCastOrNull(value, 'Decimal(10,2)') IS NOT NULL --Фильтруем строки, где value нельзя преобразовать в Decimal(10,2)
|
||||||
|
GROUP BY status_string;
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
-- Задачи на отработку Запросы - JOIN --
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.products ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.prices ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.products
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
product_id UInt32,
|
||||||
|
name String
|
||||||
|
) ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/products',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY product_id;
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.products VALUES
|
||||||
|
(101, 'iPhone 14'),
|
||||||
|
(102, 'Galaxy S22'),
|
||||||
|
(103, 'Pixel 7');
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.prices
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
product_id UInt32,
|
||||||
|
price UInt32,
|
||||||
|
region String
|
||||||
|
) ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/prices',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY product_id;
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.prices VALUES
|
||||||
|
(101, 999, 'US'),
|
||||||
|
(101, 1049, 'EU'),
|
||||||
|
(102, 850, 'US'),
|
||||||
|
(102, 899, 'CA'),
|
||||||
|
(103, 799, 'JP');
|
||||||
|
|
||||||
|
|
||||||
|
--№1 Сделай ANY LEFT JOIN, чтобы получить цену любой из регионов.
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
p.product_id,
|
||||||
|
p.name,
|
||||||
|
pr.price,
|
||||||
|
pr.region
|
||||||
|
FROM dsolovianov.products AS p
|
||||||
|
ANY LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id;
|
||||||
|
|
||||||
|
--№2 Что будет, если в таблице `prices` несколько цен на один `product_id`?
|
||||||
|
|
||||||
|
/* Для одного product_id будет выбрана только первая ЛЮБАЯ (ANY) подходящая в поиске.*/
|
||||||
|
|
||||||
|
--№3 Как повлияет `ALL LEFT JOIN`? Почему в ClickHouse его избегают?
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
p.product_id,
|
||||||
|
p.name,
|
||||||
|
pr.price,
|
||||||
|
pr.region
|
||||||
|
FROM dsolovianov.products AS p
|
||||||
|
ALL LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id;
|
||||||
|
|
||||||
|
/* `ALL LEFT JOIN` в Clickhouse ведет себя также, как LEFT JOIN в SQL, то есть собирает все совпадения.
|
||||||
|
Начинается перегон данных между таблицами. Из-за этого растет нагрузка на ОЗУ и CPU.
|
||||||
|
короче, по сути ALL LEFT JOIN создает декартово произведение*/
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
/* Запросы - Синтаксис*/
|
||||||
|
|
||||||
|
--! Задача 1 !--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE users
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
user_id UInt32,
|
||||||
|
name String,
|
||||||
|
age UInt8,
|
||||||
|
country String,
|
||||||
|
is_active Bool
|
||||||
|
) ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/users_v2',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY user_id;
|
||||||
|
|
||||||
|
INSERT INTO users VALUES
|
||||||
|
(1, 'Ivan', 30, 'RU', 1),
|
||||||
|
(2, 'Anna', 25, 'RU', 1),
|
||||||
|
(3, 'Petr', 45, 'RU', 0),
|
||||||
|
(4, 'Olga', 35, 'RU', 1),
|
||||||
|
(5, 'John', 55, 'US', 1),
|
||||||
|
(6, 'Alice', 42, 'US', 0),
|
||||||
|
(7, 'Tom', 38, 'US', 1),
|
||||||
|
(8, 'Lucas', 28, 'BR', 1),
|
||||||
|
(9, 'Maria', 60, 'BR', 1),
|
||||||
|
(10, 'João', 27, 'BR', 0),
|
||||||
|
(11, 'Kenji', 33, 'JP', 1),
|
||||||
|
(12, 'Yuki', 29, 'JP', 0),
|
||||||
|
(13, 'Chen', 40, 'CN', 1),
|
||||||
|
(14, 'Li', 48, 'CN', 1),
|
||||||
|
(15, 'Fatima', 31, 'AE', 1),
|
||||||
|
(16, 'Ahmed', 36, 'AE', 1),
|
||||||
|
(17, 'George', 24, 'US', 1),
|
||||||
|
(18, 'Nina', 34, 'RU', 1),
|
||||||
|
(19, 'Max', 50, 'DE', 1),
|
||||||
|
(20, 'Sophie', 22, 'DE', 0);
|
||||||
|
|
||||||
|
--Получи 10 самых старших активных пользователей.
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
age,
|
||||||
|
is_active
|
||||||
|
FROM users
|
||||||
|
WHERE is_active = true
|
||||||
|
ORDER BY age DESC
|
||||||
|
LIMIT 10;
|
||||||
|
|
||||||
|
--Выведи пользователей из country = 'RU', отсортированных по имени.
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
country
|
||||||
|
FROM users
|
||||||
|
WHERE country = 'RU'
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
--Что произойдёт, если написать LIMIT 10 без ORDER BY? Просто выведет 10 случайных активных прользователей без какой либо сортировки.
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
age
|
||||||
|
FROM users
|
||||||
|
WHERE is_active = true
|
||||||
|
LIMIT 10;
|
||||||
|
|
||||||
|
--Посчитай количество пользователей по странам.
|
||||||
|
SELECT
|
||||||
|
country,
|
||||||
|
COUNT(*) as user_country
|
||||||
|
FROM users
|
||||||
|
GROUP BY country;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
country,
|
||||||
|
uniqExact(country) as unique_countries --возвращает точное количество уникальных значений в столбце country
|
||||||
|
FROM users
|
||||||
|
GROUP BY country;
|
||||||
|
|
||||||
|
--Для каждой страны выведи минимальный и максимальный возраст.
|
||||||
|
SELECT
|
||||||
|
country,
|
||||||
|
MIN(age) as min_age,
|
||||||
|
MAX(age) as max_age
|
||||||
|
FROM users
|
||||||
|
GROUP BY country
|
||||||
|
ORDER BY country;
|
||||||
|
|
||||||
|
--Отсортируй страны по убыванию количества пользователей.
|
||||||
|
SELECT
|
||||||
|
country,
|
||||||
|
COUNT(*) as user_country
|
||||||
|
FROM users
|
||||||
|
GROUP BY country
|
||||||
|
ORDER BY user_country DESC;
|
||||||
|
|
||||||
|
--Всё вместе
|
||||||
|
SELECT
|
||||||
|
country,
|
||||||
|
COUNT(*) as user_country,
|
||||||
|
SUM(is_active) as active_users,
|
||||||
|
MIN(age) as min_age,
|
||||||
|
MAX(age) as max_age,
|
||||||
|
uniqExact(country) as unique_countries
|
||||||
|
FROM users
|
||||||
|
GROUP BY country
|
||||||
|
ORDER BY country;
|
||||||
|
|
||||||
|
--! Задача 2 !--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.events ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
user_id UInt32,
|
||||||
|
event_time DateTime,
|
||||||
|
event_type String
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/events_v2',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY (user_id, event_time);
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.events (user_id, event_time, event_type) VALUES
|
||||||
|
(1, '2025-07-17 09:00:00', 'click'),
|
||||||
|
(1, '2025-07-17 09:05:00', 'view'),
|
||||||
|
(1, '2025-07-17 09:10:00', 'purchase'),
|
||||||
|
(2, '2025-07-17 10:00:00', 'click'),
|
||||||
|
(2, '2025-07-17 10:05:00', 'click'),
|
||||||
|
(2, '2025-07-17 11:00:00', 'view'),
|
||||||
|
(3, '2025-07-16 12:00:00', 'view'),
|
||||||
|
(3, '2025-07-16 12:30:00', 'click'),
|
||||||
|
(3, '2025-07-17 08:00:00', 'purchase'),
|
||||||
|
(4, '2025-07-17 13:00:00', 'click'),
|
||||||
|
(4, '2025-07-17 14:00:00', 'view'),
|
||||||
|
(4, '2025-07-17 15:00:00', 'view'),
|
||||||
|
(5, '2025-07-16 20:00:00', 'view'),
|
||||||
|
(5, '2025-07-17 01:00:00', 'click'),
|
||||||
|
(5, '2025-07-17 06:00:00', 'purchase'),
|
||||||
|
(6, '2025-07-17 07:30:00', 'view'),
|
||||||
|
(6, '2025-07-17 08:45:00', 'click'),
|
||||||
|
(7, '2025-07-16 23:59:59', 'click'),
|
||||||
|
(7, '2025-07-17 00:01:00', 'purchase'),
|
||||||
|
(7, '2025-07-17 10:10:00', 'view');
|
||||||
|
|
||||||
|
--1. Добавь колонку с кумулятивным количеством событий по `user_id` (используй `count() OVER ...`).
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
event_time,
|
||||||
|
event_type,
|
||||||
|
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time) AS cumulative_events
|
||||||
|
FROM dsolovianov.events
|
||||||
|
ORDER BY user_id, event_time;
|
||||||
|
|
||||||
|
--2. Сделай аналог ранжирования событий внутри пользователя: `row_number() OVER (PARTITION BY user_id ORDER BY event_time)`
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
event_time,
|
||||||
|
event_type,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time) AS event_rank
|
||||||
|
FROM dsolovianov.events
|
||||||
|
ORDER BY user_id, event_rank;
|
||||||
|
|
||||||
|
--3. Что будет, если ты явно задашь `ROWS BETWEEN`? Поддерживается ли?
|
||||||
|
--Да, `ROWS BETWEEN` поддерживается:
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
event_time,
|
||||||
|
event_type,
|
||||||
|
-- кумулятивное окно (все предыдущие строки)
|
||||||
|
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time
|
||||||
|
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_events,
|
||||||
|
-- скользящее окно: текущая + 2 предыдущих события
|
||||||
|
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time
|
||||||
|
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS last_3_events,
|
||||||
|
-- окно: предыдущая + текущая + следующая
|
||||||
|
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time
|
||||||
|
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS prev_curr_next
|
||||||
|
FROM dsolovianov.events
|
||||||
|
ORDER BY user_id, event_time;
|
||||||
|
|
||||||
|
--4. Сделай `WITH recent_events AS (...)` — все события за последние 24 часа.
|
||||||
|
WITH recent_events AS (
|
||||||
|
SELECT *
|
||||||
|
FROM dsolovianov.events
|
||||||
|
WHERE event_time >= '2025-07-16 15:00:00'
|
||||||
|
)
|
||||||
|
SELECT * FROM recent_events
|
||||||
|
ORDER BY event_time DESC;
|
||||||
|
|
||||||
|
--5. Используй CTE, чтобы посчитать топ-5 пользователей по числу этих событий.
|
||||||
|
WITH recent_events AS (
|
||||||
|
SELECT *
|
||||||
|
FROM dsolovianov.events
|
||||||
|
WHERE event_time >= '2025-07-16 15:00:00'
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
COUNT(*) AS event_count
|
||||||
|
FROM recent_events
|
||||||
|
GROUP BY user_id
|
||||||
|
ORDER BY event_count DESC
|
||||||
|
LIMIT 5;
|
||||||
|
|
||||||
|
--6. Добавь второй `WITH` — для маппинга `event_type → category` и используй его в основном запросе.
|
||||||
|
WITH
|
||||||
|
event_categories AS (
|
||||||
|
SELECT
|
||||||
|
event_type,
|
||||||
|
CASE
|
||||||
|
WHEN event_type = 'click' THEN 'engagement'
|
||||||
|
WHEN event_type = 'view' THEN 'engagement'
|
||||||
|
WHEN event_type = 'purchase' THEN 'conversion'
|
||||||
|
ELSE 'other'
|
||||||
|
END AS category
|
||||||
|
FROM (SELECT DISTINCT event_type FROM dsolovianov.events)
|
||||||
|
),
|
||||||
|
recent_events AS (
|
||||||
|
SELECT *
|
||||||
|
FROM dsolovianov.events
|
||||||
|
WHERE event_time >= '2025-07-16 15:00:00'
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
re.user_id,
|
||||||
|
re.event_time,
|
||||||
|
re.event_type,
|
||||||
|
ec.category,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS event_rank,
|
||||||
|
COUNT(*) OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS cumulative_events
|
||||||
|
FROM recent_events re
|
||||||
|
LEFT JOIN event_categories ec ON re.event_type = ec.event_type
|
||||||
|
ORDER BY re.user_id, re.event_time;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Вопрос: поддерживает ли ClickHouse многоуровневые или рекурсивные CTE?
|
||||||
|
многоуровневые CTE - да
|
||||||
|
рекурсивные CTE - нет в классическом понимании, а скорее в итеративном смысле.
|
||||||
|
*/
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
-- Индексы и сжатие - Индексы --
|
||||||
|
|
||||||
|
-- #1
|
||||||
|
/*
|
||||||
|
3. Посмотри, сколько блоков было пропущено (в `system.query_log`, `rows_read`, `rows`).
|
||||||
|
Не посмотрю, так как нет доступа system.query_log, rows_read, rows (ACCESS_DENIED)
|
||||||
|
4. Что будет, если `ORDER BY log_id`, а не `ts`?
|
||||||
|
ORDER BY log_id → данные физически отсортированы по log_id. Значения ts расположены хаотично относительно физической сортировки.
|
||||||
|
Индекс minmax по ts будет почти бесполезен. Minmax индекс будет иметь широкие диапазоны
|
||||||
|
*/
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.logs
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
log_id UInt64,
|
||||||
|
ts DateTime
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/logs',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY ts
|
||||||
|
PARTITION BY toYYYYMM(ts)
|
||||||
|
PRIMARY KEY (ts);
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.logs (log_id, ts) VALUES
|
||||||
|
(1, '2024-06-01 10:00:00'),
|
||||||
|
(2, '2024-06-01 11:00:00'),
|
||||||
|
(3, '2024-06-02 12:00:00'),
|
||||||
|
(4, '2024-06-03 13:00:00'),
|
||||||
|
(5, '2024-06-04 14:00:00');
|
||||||
|
|
||||||
|
-- 1. Добавляем minmax индекс:
|
||||||
|
ALTER TABLE dsolovianov.logs ADD INDEX idx_ts ts TYPE minmax GRANULARITY 2;
|
||||||
|
|
||||||
|
-- 2. Сделай запрос: `SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03'`
|
||||||
|
SELECT *
|
||||||
|
FROM dsolovianov.logs
|
||||||
|
WHERE ts
|
||||||
|
BETWEEN '2024-06-02' AND '2024-06-03';
|
||||||
|
|
||||||
|
-- Раз нет доступа, но можно предположить, что вот такой запрос сработает:
|
||||||
|
SELECT query, read_rows, result_rows
|
||||||
|
FROM system.query_log
|
||||||
|
WHERE query LIKE '%logs%'
|
||||||
|
ORDER BY event_time DESC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
|
||||||
|
-- #2
|
||||||
|
/*
|
||||||
|
1. Создай таблицу с `ORDER BY id`, добавь индекс:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
3. Посмотри `EXPLAIN` — индекс используется?
|
||||||
|
Данные отсортированы по id, а не по country. Значения country распределены хаотически относительно физического порядка.
|
||||||
|
4. Как будет вести себя индекс, если `country` имеет 10M уникальных значений?
|
||||||
|
Будет создано 10M партиций, что убьёт производительность.
|
||||||
|
**/
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.clicks ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.clicks
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
id UInt64,
|
||||||
|
country String
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/clicks',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY id
|
||||||
|
PARTITION BY country
|
||||||
|
PRIMARY KEY (id);
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.clicks (id, country) VALUES
|
||||||
|
(1, 'RU'),
|
||||||
|
(2, 'US'),
|
||||||
|
(3, 'RU'),
|
||||||
|
(4, 'DE'),
|
||||||
|
(5, 'RU');
|
||||||
|
|
||||||
|
ALTER TABLE dsolovianov.clicks ON CLUSTER clickhouse_cluster
|
||||||
|
ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2;
|
||||||
|
|
||||||
|
-- 2. Выполни запрос `SELECT * FROM clicks WHERE country = 'RU'`.
|
||||||
|
EXPLAIN indexes = 1
|
||||||
|
SELECT * FROM dsolovianov.clicks WHERE country = 'RU';
|
||||||
|
|
||||||
|
EXPLAIN
|
||||||
|
SELECT * FROM dsolovianov.clicks WHERE country = 'RU';
|
||||||
|
|
||||||
|
/*
|
||||||
|
Expression ((Project names + Projection))
|
||||||
|
Expression ((WHERE + Change column names to column identifiers))
|
||||||
|
ReadFromMergeTree (dsolovianov.clicks)
|
||||||
|
Indexes:
|
||||||
|
MinMax
|
||||||
|
Keys:
|
||||||
|
country
|
||||||
|
Condition: (country in ['RU', 'RU'])
|
||||||
|
Parts: 1/3
|
||||||
|
Granules: 1/3
|
||||||
|
Partition
|
||||||
|
Keys:
|
||||||
|
country
|
||||||
|
Condition: (country in ['RU', 'RU'])
|
||||||
|
Parts: 1/1
|
||||||
|
Granules: 1/1
|
||||||
|
PrimaryKey
|
||||||
|
Condition: true
|
||||||
|
Parts: 1/1
|
||||||
|
Granules: 1/1
|
||||||
|
Skip
|
||||||
|
Name: idx_country
|
||||||
|
Description: set GRANULARITY 2
|
||||||
|
Parts: 1/1
|
||||||
|
Granules: 1/1
|
||||||
|
Ranges: 1
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- #3
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.users
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
id UInt64,
|
||||||
|
email String
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/users',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY id
|
||||||
|
PRIMARY KEY (id);
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.users (id, email) VALUES
|
||||||
|
(1, 'alice@mail.com'),
|
||||||
|
(2, 'bob@mail.com'),
|
||||||
|
(3, 'charlie@mail.com'),
|
||||||
|
(4, 'dave@mail.com'),
|
||||||
|
(5, 'eve@example.com');
|
||||||
|
|
||||||
|
ALTER TABLE dsolovianov.users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2;
|
||||||
|
|
||||||
|
EXPLAIN indexes = 1
|
||||||
|
SELECT * FROM users WHERE email = 'alice@mail.com';
|
||||||
|
-- Bloom filter может пропустить некоторые гранулы, но не все, ибо данные не отсортированы по email и индексные блоки покрывают случайные email'ы
|
||||||
|
|
||||||
|
EXPLAIN indexes = 1
|
||||||
|
SELECT * FROM users WHERE email LIKE '%mail%';
|
||||||
|
|
||||||
|
EXPLAIN indexes = 1
|
||||||
|
SELECT * FROM users WHERE email LIKE 'alice%';
|
||||||
|
/*
|
||||||
|
Bloom filter не поддерживает частичные совпадения (LIKE '..%', LIKE '%..', LIKE '%..%'). Из-за этого ClickHouse не может пропустить гранулы.
|
||||||
|
Клику становится сложно вычислить хэш для проверки, если подходящих значений по LIKE будет больше одного. Нет гарантии, что данные на выходе будут корректные.
|
||||||
|
*/
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
-- Индексы и сжатие - Партиции --
|
||||||
|
|
||||||
|
-- #1
|
||||||
|
/*
|
||||||
|
1. Создай таблицу sales с `PARTITION BY toYYYYMM(sale_date)`.
|
||||||
|
2. Выполни `ALTER TABLE ... DROP PARTITION '202405'`.
|
||||||
|
3. Что произойдет, если не указывать `PARTITION BY`? <- удалит все данные из таблицы. партиция то одна :)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.sales ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.sales
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
id UInt64,
|
||||||
|
region String,
|
||||||
|
sale_date Date,
|
||||||
|
amount Float64
|
||||||
|
) ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/sales',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
PARTITION BY toYYYYMM(sale_date)
|
||||||
|
ORDER BY (region, sale_date);
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.sales (id, region, sale_date, amount) VALUES
|
||||||
|
(1, 'RU', '2024-05-01', '100.0'),
|
||||||
|
(2, 'RU', '2024-05-15', '120.0'),
|
||||||
|
(3, 'US', '2024-06-01', '90.0'),
|
||||||
|
(4, 'US', '2024-07-03', '150.0'),
|
||||||
|
(5, 'DE', '2024-07-04', '160.0');
|
||||||
|
|
||||||
|
ALTER TABLE dsolovianov.sales DROP PARTITION '202405';
|
||||||
|
|
||||||
|
SELECT *
|
||||||
|
FROM dsolovianov.sales;
|
||||||
|
|
||||||
|
-- #2
|
||||||
|
/*
|
||||||
|
1. Создай таблицу с `PARTITION BY event_type`.
|
||||||
|
2. Удали все события `click` (подсказка: это `ALTER TABLE DROP PARTITION 'click'`).
|
||||||
|
3. Чем такой подход может быть опасен (подумай про кардинальность значений)?
|
||||||
|
При большом количестве уникальных event_type создастся много мелких партиций.
|
||||||
|
*/
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.events
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
id UInt64,
|
||||||
|
event_type String,
|
||||||
|
event_date Date
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/events',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
PARTITION BY event_type
|
||||||
|
ORDER BY (event_date, id);
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.events (id, event_type, event_date) VALUES
|
||||||
|
(1, 'login', '2024-06-01'),
|
||||||
|
(2, 'click', '2024-06-01'),
|
||||||
|
(3, 'click', '2024-06-02'),
|
||||||
|
(4, 'purchase', '2024-06-03'),
|
||||||
|
(5, 'login', '2024-06-04');
|
||||||
|
|
||||||
|
ALTER TABLE dsolovianov.events DROP PARTITION 'click';
|
||||||
|
|
||||||
|
-- #3
|
||||||
|
/*
|
||||||
|
1. Создай таблицу с `PARTITION BY cityHash64(user_id) % 100`.
|
||||||
|
2. Что ты выиграешь с точки зрения баланса?
|
||||||
|
Данные распределятся примерно поровну между 100 партициями. Упрощается управление удалением старых данных (можно дропать целые партиции)
|
||||||
|
3. Почему такой тип `PARTITION` не ускорит `SELECT * WHERE user_id = 456`?
|
||||||
|
Потому что партиции определяются хэшем, а не прямым значением user_id и к тому же сортировка в партициях по log_time, а не user_id.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.logs
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
id UInt64,
|
||||||
|
user_id UInt64,
|
||||||
|
log_time DateTime
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/logs',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
PARTITION BY cityHash64(user_id) % 100 -- 100 партиций по хэшу user_id
|
||||||
|
ORDER BY (log_time, id);
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.logs (id, user_id, log_time) VALUES
|
||||||
|
(1, 123, '2024-07-17 10:00:00'),
|
||||||
|
(2, 456, '2024-07-17 10:01:00'),
|
||||||
|
(3, 789, '2024-07-17 10:02:00');
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
cityHash64(user_id) % 100 AS partition_number
|
||||||
|
FROM dsolovianov.logs
|
||||||
|
ORDER BY id;
|
||||||
|
|
||||||
|
SELECT * FROM logs WHERE user_id = 456;
|
||||||
|
|
||||||
|
|
||||||
|
-- #4
|
||||||
|
/*
|
||||||
|
1. Создай таблицу с `PARTITION BY (region, toYYYYMM(visit_date))`.
|
||||||
|
2. Как будет выглядеть имя партиции?
|
||||||
|
Имена партиций будут в формате: ('значение_region', 'YYYYMM'). Пример: ('RU', '202406')
|
||||||
|
3. Сможешь ли ты эффективно удалить **все июльские данные по `RU`**? Да. Удаляется целая партиция без сканирования и нагрузки на систему.
|
||||||
|
*/
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.visits ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.visits
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
id UInt64,
|
||||||
|
region String,
|
||||||
|
visit_date Date
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/visits',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
PARTITION BY (region, toYYYYMM(visit_date))
|
||||||
|
ORDER BY (region, visit_date, id);
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.visits (id, region, visit_date) VALUES
|
||||||
|
(1, 'RU', '2024-06-01'),
|
||||||
|
(2, 'RU', '2024-06-15'),
|
||||||
|
(3, 'US', '2024-06-03'),
|
||||||
|
(4, 'US', '2024-07-01'),
|
||||||
|
(5, 'RU', '2024-07-05');
|
||||||
|
|
||||||
|
ALTER TABLE dsolovianov.visits ON CLUSTER clickhouse_cluster
|
||||||
|
DROP PARTITION ('RU', '202407');
|
||||||
|
|
||||||
|
SELECT * FROM dsolovianov.visits;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
-- Индексы и сжатие - Сжатие --
|
||||||
|
|
||||||
|
-- #1 Создай таблицу logs с колонкой message CODEC(ZSTD(3)).
|
||||||
|
-- #2 Добавь created_at CODEC(Delta, ZSTD) — это улучшит хранение по времени.
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.logs
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
id UInt64,
|
||||||
|
level String,
|
||||||
|
message String CODEC(ZSTD(3)),
|
||||||
|
created_at DateTime CODEC(Delta, ZSTD)
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/logs',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY (created_at, id)
|
||||||
|
PARTITION BY toYYYYMM(created_at);
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.logs (id, level, message, created_at) VALUES
|
||||||
|
(1, 'INFO', 'User logged in', '2024-07-01 10:00:00'),
|
||||||
|
(2, 'WARN', 'Request took too long', '2024-07-01 10:05:00'),
|
||||||
|
(3, 'ERROR', 'Unable to connect to server', '2024-07-01 10:10:00');
|
||||||
|
|
||||||
|
-- #3 Посмотри статистику сжатия:
|
||||||
|
SELECT
|
||||||
|
column,
|
||||||
|
compression_codec
|
||||||
|
FROM system.columns
|
||||||
|
WHERE table = 'logs';
|
||||||
|
|
||||||
|
-- Стата вот так выглядит:
|
||||||
|
column | compression_codec
|
||||||
|
id
|
||||||
|
level
|
||||||
|
message CODEC(ZSTD(3))
|
||||||
|
created_at CODEC(Delta(4), ZSTD(1))
|
||||||
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
+18
-9
@@ -1,6 +1,9 @@
|
|||||||
--Задача 1: Создай таблицу page_views
|
--Задача 1: Создай таблицу page_views
|
||||||
|
DROP TABLE page_views;
|
||||||
|
|
||||||
CREATE TABLE page_views (
|
CREATE TABLE page_views
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
user_id UInt64,
|
user_id UInt64,
|
||||||
url String,
|
url String,
|
||||||
view_time DateTime
|
view_time DateTime
|
||||||
@@ -11,7 +14,9 @@ ORDER BY (user_id, view_time);
|
|||||||
|
|
||||||
--Задача 2: Создай таблицу users
|
--Задача 2: Создай таблицу users
|
||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE users
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
id UInt64,
|
id UInt64,
|
||||||
name String,
|
name String,
|
||||||
updated_at DateTime
|
updated_at DateTime
|
||||||
@@ -21,7 +26,9 @@ ORDER BY id;
|
|||||||
|
|
||||||
-- Задача 3: Создай таблицу region_sales
|
-- Задача 3: Создай таблицу region_sales
|
||||||
|
|
||||||
CREATE TABLE region_sales (
|
CREATE TABLE region_sales
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
region String,
|
region String,
|
||||||
date Date,
|
date Date,
|
||||||
sales UInt64
|
sales UInt64
|
||||||
@@ -30,17 +37,19 @@ ENGINE = SummingMergeTree()
|
|||||||
PARTITION BY toYYYYMM(date)
|
PARTITION BY toYYYYMM(date)
|
||||||
ORDER BY (region, 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,
|
region String,
|
||||||
avg_age_state AggregateFunction(avg, UInt8),
|
avg_age_state AggregateFunction(avg, UInt8),
|
||||||
active_users_state AggregateFunction(uniq, UInt64)
|
active_users_state AggregateFunction(uniq, UInt64)
|
||||||
)
|
)
|
||||||
ENGINE = AggregatingMergeTree()
|
ENGINE = AggregatingMergeTree() --Подзадача 4.2
|
||||||
ORDER BY region;
|
ORDER BY region; --Подзадача 4.3
|
||||||
|
|
||||||
--Задача 4.3: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()`
|
--Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()`
|
||||||
|
|
||||||
INSERT INTO user_metrics
|
INSERT INTO user_metrics
|
||||||
SELECT
|
SELECT
|
||||||
@@ -61,7 +70,7 @@ FROM
|
|||||||
)
|
)
|
||||||
GROUP BY region;
|
GROUP BY region;
|
||||||
|
|
||||||
--Задача 4.4: - Читай их через `avgMerge()` и `uniqMerge()`
|
--Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()`
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
region,
|
region,
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
DROP TABLE IF EXISTS dsolovianov.core_fact_lesson_views ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.core_dim_user ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.core_dim_course ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.core_dim_lesson ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.core_dim_date ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.core_enrollments ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.core_dim_user
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
user_id Int32,
|
||||||
|
name String,
|
||||||
|
age Int32,
|
||||||
|
email String,
|
||||||
|
registration_date Date
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/core_dim_user',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY user_id;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.core_dim_course
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
course_id Int32,
|
||||||
|
title String,
|
||||||
|
category String
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/core_dim_course',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY course_id;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.core_dim_lesson
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
lesson_id Int32,
|
||||||
|
title String,
|
||||||
|
duration_min Int32,
|
||||||
|
course_id Int32
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/core_dim_lesson',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY lesson_id;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.core_dim_date
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
date Date,
|
||||||
|
year Int32,
|
||||||
|
month Int32,
|
||||||
|
day Int32,
|
||||||
|
weekday String
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/core_dim_date',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY date;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.core_enrollments
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
user_id Int32,
|
||||||
|
course_id Int32,
|
||||||
|
enrolled_at DateTime64(3)
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/core_enrollments',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY user_id;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.core_fact_lesson_views
|
||||||
|
ON CLUSTER clickhouse_cluster
|
||||||
|
(
|
||||||
|
user_id Int32,
|
||||||
|
lesson_id Int32,
|
||||||
|
course_id Int32,
|
||||||
|
viewed_at DateTime64(3)
|
||||||
|
)
|
||||||
|
ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/core_fact_lesson_views',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY user_id;
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
-- Этап 2. Создание витринных таблиц --
|
||||||
|
|
||||||
|
-- Витрина lesson_popularity_summary
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.lesson_popularity_summary ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.lesson_popularity_summary ON CLUSTER clickhouse_cluster (
|
||||||
|
lesson_id UInt32,
|
||||||
|
lesson_title String,
|
||||||
|
course_id UInt32,
|
||||||
|
course_title String,
|
||||||
|
total_views UInt64,
|
||||||
|
unique_users UInt64,
|
||||||
|
first_view DateTime,
|
||||||
|
last_view DateTime
|
||||||
|
) ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/lesson_popularity_summary',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY (course_id, lesson_id);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.lesson_popularity_summary_mv ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW dsolovianov.lesson_popularity_summary_mv ON CLUSTER clickhouse_cluster
|
||||||
|
TO dsolovianov.lesson_popularity_summary
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
l.lesson_id AS lesson_id,
|
||||||
|
l.title AS lesson_title,
|
||||||
|
l.course_id AS course_id,
|
||||||
|
c.title AS course_title,
|
||||||
|
COUNT(v.viewed_at) AS total_views,
|
||||||
|
COUNT(DISTINCT v.user_id) AS unique_users,
|
||||||
|
MIN(v.viewed_at) AS first_view,
|
||||||
|
MAX(v.viewed_at) AS last_view
|
||||||
|
FROM dsolovianov.core_fact_lesson_views v
|
||||||
|
LEFT JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||||
|
LEFT JOIN dsolovianov.core_dim_course c ON l.course_id = c.course_id
|
||||||
|
GROUP BY l.lesson_id, l.title, l.course_id, c.title;
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.lesson_popularity_summary
|
||||||
|
SELECT
|
||||||
|
l.lesson_id AS lesson_id,
|
||||||
|
l.title AS lesson_title,
|
||||||
|
l.course_id AS course_id,
|
||||||
|
c.title AS course_title,
|
||||||
|
COUNT(v.viewed_at) AS total_views,
|
||||||
|
COUNT(DISTINCT v.user_id) AS unique_users,
|
||||||
|
MIN(v.viewed_at) AS first_view,
|
||||||
|
MAX(v.viewed_at) AS last_view
|
||||||
|
FROM dsolovianov.core_dim_lesson l
|
||||||
|
JOIN dsolovianov.core_dim_course c ON l.course_id = c.course_id
|
||||||
|
LEFT JOIN dsolovianov.core_fact_lesson_views v ON l.lesson_id = v.lesson_id
|
||||||
|
GROUP BY l.lesson_id, l.title, l.course_id, c.title;
|
||||||
|
|
||||||
|
-- Витрину inactive_users_summary
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster (
|
||||||
|
user_id UInt32,
|
||||||
|
name String,
|
||||||
|
email String,
|
||||||
|
age UInt32,
|
||||||
|
registered_courses_count UInt32,
|
||||||
|
registration_date Date
|
||||||
|
) ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/inactive_users_summary',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY user_id;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.inactive_users_summary_mv ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW dsolovianov.inactive_users_summary_mv ON CLUSTER clickhouse_cluster
|
||||||
|
TO dsolovianov.inactive_users_summary
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
u.user_id AS user_id,
|
||||||
|
u.name AS name,
|
||||||
|
u.email AS email,
|
||||||
|
u.age AS age,
|
||||||
|
COUNT(DISTINCT e.course_id) AS registered_courses_count,
|
||||||
|
u.registration_date AS registration_date
|
||||||
|
FROM dsolovianov.core_dim_user u
|
||||||
|
LEFT JOIN dsolovianov.core_enrollments e ON u.user_id = e.user_id
|
||||||
|
WHERE u.user_id NOT IN (SELECT DISTINCT user_id FROM dsolovianov.core_fact_lesson_views)
|
||||||
|
GROUP BY u.user_id, u.name, u.email, u.age, u.registration_date;
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.inactive_users_summary
|
||||||
|
SELECT
|
||||||
|
u.user_id AS user_id,
|
||||||
|
u.name AS name,
|
||||||
|
u.email AS email,
|
||||||
|
u.age AS age,
|
||||||
|
COUNT(DISTINCT e.course_id) AS registered_courses_count,
|
||||||
|
u.registration_date AS registration_date
|
||||||
|
FROM dsolovianov.core_dim_user u
|
||||||
|
LEFT JOIN dsolovianov.core_enrollments e ON u.user_id = e.user_id
|
||||||
|
WHERE u.user_id NOT IN (SELECT DISTINCT user_id FROM dsolovianov.core_fact_lesson_views)
|
||||||
|
GROUP BY u.user_id, u.name, u.email, u.age, u.registration_date;
|
||||||
|
|
||||||
|
-- Витрина course_completion_rate
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.course_completion_rate ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.course_completion_rate ON CLUSTER clickhouse_cluster (
|
||||||
|
user_id UInt32,
|
||||||
|
user_name String,
|
||||||
|
course_id UInt32,
|
||||||
|
course_title String,
|
||||||
|
lessons_in_course UInt32,
|
||||||
|
lessons_viewed UInt32,
|
||||||
|
completion_rate Float32
|
||||||
|
) ENGINE = ReplicatedMergeTree(
|
||||||
|
'/clickhouse/tables/{shard}/dsolovianov/course_completion_rate',
|
||||||
|
'{replica}'
|
||||||
|
)
|
||||||
|
ORDER BY (user_id, course_id);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS dsolovianov.course_completion_rate_mv ON CLUSTER clickhouse_cluster SYNC;
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW dsolovianov.course_completion_rate_mv ON CLUSTER clickhouse_cluster
|
||||||
|
TO dsolovianov.course_completion_rate
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
e.user_id AS user_id,
|
||||||
|
u.name AS user_name,
|
||||||
|
e.course_id AS course_id,
|
||||||
|
c.title AS course_title,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM dsolovianov.core_dim_lesson l
|
||||||
|
WHERE l.course_id = e.course_id
|
||||||
|
) AS lessons_in_course,
|
||||||
|
(
|
||||||
|
SELECT COUNT(DISTINCT v.lesson_id)
|
||||||
|
FROM dsolovianov.core_fact_lesson_views v
|
||||||
|
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||||
|
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||||
|
) AS lessons_viewed,
|
||||||
|
(
|
||||||
|
SELECT COUNT(DISTINCT v.lesson_id)
|
||||||
|
FROM dsolovianov.core_fact_lesson_views v
|
||||||
|
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||||
|
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||||
|
) / (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM dsolovianov.core_dim_lesson l
|
||||||
|
WHERE l.course_id = e.course_id
|
||||||
|
) AS completion_rate
|
||||||
|
FROM dsolovianov.core_enrollments e
|
||||||
|
JOIN dsolovianov.core_dim_user u ON e.user_id = u.user_id
|
||||||
|
JOIN dsolovianov.core_dim_course c ON e.course_id = c.course_id
|
||||||
|
GROUP BY e.user_id, u.name, e.course_id, c.title;
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.course_completion_rate
|
||||||
|
SELECT
|
||||||
|
e.user_id AS user_id,
|
||||||
|
u.name AS user_name,
|
||||||
|
e.course_id AS course_id,
|
||||||
|
c.title AS course_title,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM dsolovianov.core_dim_lesson l
|
||||||
|
WHERE l.course_id = e.course_id
|
||||||
|
) AS lessons_in_course,
|
||||||
|
COALESCE(
|
||||||
|
(
|
||||||
|
SELECT COUNT(DISTINCT v.lesson_id)
|
||||||
|
FROM dsolovianov.core_fact_lesson_views v
|
||||||
|
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||||
|
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||||
|
), 0
|
||||||
|
) AS lessons_viewed,
|
||||||
|
COALESCE(
|
||||||
|
(
|
||||||
|
SELECT COUNT(DISTINCT v.lesson_id)
|
||||||
|
FROM dsolovianov.core_fact_lesson_views v
|
||||||
|
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||||
|
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||||
|
), 0
|
||||||
|
) / (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM dsolovianov.core_dim_lesson l
|
||||||
|
WHERE l.course_id = e.course_id
|
||||||
|
) AS completion_rate
|
||||||
|
FROM dsolovianov.core_enrollments e
|
||||||
|
JOIN dsolovianov.core_dim_user u ON e.user_id = u.user_id
|
||||||
|
JOIN dsolovianov.core_dim_course c ON e.course_id = c.course_id
|
||||||
|
GROUP BY e.user_id, u.name, e.course_id, c.title;
|
||||||
|
|
||||||
|
SELECT * FROM dsolovianov.course_completion_rate ORDER BY user_id;
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
-- Этап 3. Анализ и запросы - Напиши SQL-запросы к витринам
|
||||||
|
-- Напиши свои вариации для практики--
|
||||||
|
|
||||||
|
|
||||||
|
-- Топ-5 самых просматриваемых уроков
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
lesson_title,
|
||||||
|
course_title,
|
||||||
|
total_views
|
||||||
|
FROM dsolovianov.lesson_popularity_summary
|
||||||
|
ORDER BY total_views DESC
|
||||||
|
LIMIT 5;
|
||||||
|
|
||||||
|
|
||||||
|
-- Неактивные пользователи, записавшиеся на курсы
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
registered_courses_count
|
||||||
|
FROM dsolovianov.inactive_users_summary
|
||||||
|
WHERE registered_courses_count > 0
|
||||||
|
ORDER BY registration_date DESC;
|
||||||
|
|
||||||
|
-- Курсы с самым высоким процентом завершения
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
course_title,
|
||||||
|
AVG(completion_rate) AS avg_completion
|
||||||
|
FROM dsolovianov.course_completion_rate
|
||||||
|
GROUP BY course_title
|
||||||
|
ORDER BY avg_completion DESC
|
||||||
|
LIMIT 5;
|
||||||
|
|
||||||
|
-- Мои вариации --
|
||||||
|
|
||||||
|
-- Среднее количество учеников на урок по каждому курсу
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
course_title,
|
||||||
|
ROUND(AVG(unique_users), 2) AS avg_users_lesson,
|
||||||
|
SUM(total_views) AS total_course_views
|
||||||
|
FROM dsolovianov.lesson_popularity_summary
|
||||||
|
GROUP BY course_title
|
||||||
|
ORDER BY avg_users_lesson DESC;
|
||||||
|
|
||||||
|
-- Возрастное распределение неактивных пользователей
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN age < 25 THEN 'до 25'
|
||||||
|
WHEN age BETWEEN 25 AND 30 THEN '25-30'
|
||||||
|
WHEN age BETWEEN 31 AND 35 THEN '31-35'
|
||||||
|
ELSE '35+'
|
||||||
|
END AS age_group,
|
||||||
|
COUNT(*) AS inactive_users_count,
|
||||||
|
AVG(registered_courses_count) AS avg_courses_enrolled
|
||||||
|
FROM dsolovianov.inactive_users_summary
|
||||||
|
GROUP BY age_group
|
||||||
|
ORDER BY age_group;
|
||||||
|
|
||||||
|
-- Пользователи, которые начали, но не закончили курс (completion_rate между 0 и 1)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
user_name,
|
||||||
|
course_title,
|
||||||
|
lessons_viewed,
|
||||||
|
lessons_in_course,
|
||||||
|
ROUND(completion_rate * 100, 0) AS no_completion,
|
||||||
|
(lessons_in_course - lessons_viewed) AS remaining_lessons
|
||||||
|
FROM dsolovianov.course_completion_rate
|
||||||
|
WHERE completion_rate BETWEEN 0.01 AND 0.99
|
||||||
|
ORDER BY completion_rate DESC, user_name;
|
||||||
|
|
||||||
|
-- Прогресс каждого пользователя по курсам (с указанием статуса)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
user_name,
|
||||||
|
course_title,
|
||||||
|
lessons_viewed,
|
||||||
|
lessons_in_course,
|
||||||
|
ROUND(completion_rate * 100, 0) AS progress_percent,
|
||||||
|
CASE
|
||||||
|
WHEN completion_rate = 1.0 THEN 'Пройден'
|
||||||
|
WHEN completion_rate = 0 THEN 'Не начат'
|
||||||
|
WHEN completion_rate >= 0.5 THEN 'В процессе (>50%)'
|
||||||
|
ELSE 'В процессе (<50%)'
|
||||||
|
END AS status
|
||||||
|
FROM dsolovianov.course_completion_rate
|
||||||
|
ORDER BY user_name, completion_rate DESC;
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
CREATE TABLE dsolovianov.users (
|
||||||
|
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
age INT,
|
||||||
|
email VARCHAR(100),
|
||||||
|
country VARCHAR(100),
|
||||||
|
is_active BOOLEAN,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.products (
|
||||||
|
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
category VARCHAR(50),
|
||||||
|
price DECIMAL(10,2) NOT NULL,
|
||||||
|
in_stock BOOLEAN
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.orders (
|
||||||
|
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
user_id INT,
|
||||||
|
order_date TIMESTAMPTZ NOT NULL,
|
||||||
|
status VARCHAR(20),
|
||||||
|
CONSTRAINT orders_user_fk
|
||||||
|
FOREIGN KEY (user_id) REFERENCES dsolovianov.users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE dsolovianov.order_items (
|
||||||
|
order_id INT NOT NULL,
|
||||||
|
product_id INT NOT NULL,
|
||||||
|
quantity INT NOT NULL CHECK (quantity > 0),
|
||||||
|
price_at_purchase DECIMAL(10,2) NOT NULL CHECK (price_at_purchase > 0),
|
||||||
|
CONSTRAINT order_items_pk PRIMARY KEY (order_id, product_id),
|
||||||
|
CONSTRAINT order_items_order_fk
|
||||||
|
FOREIGN KEY (order_id) REFERENCES dsolovianov.orders(id),
|
||||||
|
CONSTRAINT order_items_product_fk
|
||||||
|
FOREIGN KEY (product_id) REFERENCES dsolovianov.products(id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
INSERT INTO dsolovianov.users (name, age, email, country, is_active, created_at) VALUES
|
||||||
|
('Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00+00'),
|
||||||
|
('Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00+00'),
|
||||||
|
('Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00+00'),
|
||||||
|
('Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00+00'),
|
||||||
|
('Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00+00'),
|
||||||
|
('Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00+00'),
|
||||||
|
('George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00+00'),
|
||||||
|
('Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00+00'),
|
||||||
|
('Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00+00'),
|
||||||
|
('Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00+00');
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.products (name, category, price, in_stock) VALUES
|
||||||
|
('iPhone 15', 'Электроника', 80000, true),
|
||||||
|
('Kindle', 'Электроника', 15000, true),
|
||||||
|
('Кроссовки Nike', 'Одежда', 9000, true),
|
||||||
|
('Кроссовки NoName', 'Одежда', 3000, false),
|
||||||
|
('Наушники Pro', 'Электроника', 12000, true),
|
||||||
|
('Фитнес-браслет', 'Гаджеты', 6000, true),
|
||||||
|
('Подарочная карта', 'Сервисы', 1000, true);
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.orders (user_id, order_date, status) VALUES
|
||||||
|
(1, '2025-01-10 12:00:00+00', 'paid'),
|
||||||
|
(3, '2025-01-12 09:30:00+00', 'cancelled'),
|
||||||
|
(1, '2025-01-15 18:20:00+00', 'paid'),
|
||||||
|
(2, '2025-01-20 11:05:00+00', 'created'),
|
||||||
|
(4, '2025-02-01 17:45:00+00', 'paid'),
|
||||||
|
(5, '2025-02-10 10:10:00+00', 'refunded'),
|
||||||
|
(7, '2025-03-01 09:00:00+00', 'paid'),
|
||||||
|
(9, '2025-03-05 19:30:00+00', 'paid');
|
||||||
|
|
||||||
|
INSERT INTO dsolovianov.order_items (order_id, product_id, quantity, price_at_purchase) VALUES
|
||||||
|
-- Заказ #1: iPhone 15 (1 шт) + Наушники Pro (2 шт)
|
||||||
|
(1, 1, 1, 80000.00),
|
||||||
|
(1, 5, 2, 12000.00),
|
||||||
|
|
||||||
|
-- Заказ #2 (cancelled): Kindle (1 шт)
|
||||||
|
(2, 2, 1, 15000.00),
|
||||||
|
|
||||||
|
-- Заказ #3: Кроссовки Nike (2 шт)
|
||||||
|
(3, 3, 2, 9000.00),
|
||||||
|
|
||||||
|
-- Заказ #4 (created): Фитнес-браслет (1 шт)
|
||||||
|
(4, 6, 1, 6000.00),
|
||||||
|
|
||||||
|
-- Заказ #5: iPhone 15 (1 шт) + Подарочная карта (3 шт)
|
||||||
|
(5, 1, 1, 80000.00),
|
||||||
|
(5, 7, 3, 1000.00),
|
||||||
|
|
||||||
|
-- Заказ #6 (refunded): Наушники Pro (1 шт)
|
||||||
|
(6, 5, 1, 12000.00),
|
||||||
|
|
||||||
|
-- Заказ #7: Kindle (1 шт) + Фитнес-браслет (2 шт)
|
||||||
|
(7, 2, 1, 15000.00),
|
||||||
|
(7, 6, 2, 6000.00),
|
||||||
|
|
||||||
|
-- Заказ #8: iPhone 15 (1 шт) + Кроссовки Nike (1 шт)
|
||||||
|
(8, 1, 1, 80000.00),
|
||||||
|
(8, 3, 1, 9000.00);
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
- Задание 1 — Спроектируй «карточку пользователя»
|
||||||
|
|
||||||
|
Ты делаешь экран “Пользователи” в админке и решаешь, какие поля там важны.
|
||||||
|
|
||||||
|
Работаем с таблицей **`users`**
|
||||||
|
|
||||||
|
**✅ Требования:**
|
||||||
|
|
||||||
|
1. Напиши запрос, который вернёт в результате только столбцы: **`id`**, **`name`**, **`country`**, **`created_at`**, **`email`**
|
||||||
|
2. Потом измени запрос так, чтобы:
|
||||||
|
- сначала отсортировать по **`created_at`** (новые сверху)
|
||||||
|
- потом по **`name`** по алфавиту внутри одной даты
|
||||||
|
|
||||||
|
- Задание 2 — Сегментация по странам (с выбором)
|
||||||
|
|
||||||
|
Маркетолог хочет понять, какая страна сейчас интереснее.
|
||||||
|
|
||||||
|
Работаем с таблицей **`users`**
|
||||||
|
|
||||||
|
**✅ Требования:**
|
||||||
|
|
||||||
|
1. Выбери страну **`US`**и напиши запрос, который:
|
||||||
|
- вернёт **`id`**, **`name`**, **`country`**;
|
||||||
|
- покажет только пользователей из выбранной страны;
|
||||||
|
- отсортирует их по имени.
|
||||||
|
2. Напиши второй запрос, который вернёт пользователей из страны **`UK`**.
|
||||||
|
|
||||||
|
- Задание 3 — Контакты для рассылки (думать про качество)
|
||||||
|
|
||||||
|
Нужно сделать список e‑mail для рассылки так, чтобы его не стыдно было отдавать маркетологу.
|
||||||
|
|
||||||
|
**✅ Требования:**
|
||||||
|
|
||||||
|
1. Напиши один запрос, который:
|
||||||
|
- вернёт только пользователей, у которых email **есть**;
|
||||||
|
- вернёт столбцы **`id`**, **`name`**, **`email`**;
|
||||||
|
- отсортирует их по имени.
|
||||||
|
2. Напиши второй запрос, который подготовит «чистый» вид email:
|
||||||
|
- вернёт только **`email`** в нижнем регистре (как **`email_lower`**);
|
||||||
|
- уберёт дубликаты email (нужно использовать **`DISTINCT`**).
|
||||||
|
- Задание 4 — Новички и активность
|
||||||
|
|
||||||
|
«Тебе нужно посмотреть, кто пришёл недавно и кого стоит “подогреть”.»
|
||||||
|
|
||||||
|
**✅ Требования:**
|
||||||
|
|
||||||
|
1. Напиши запрос, который:
|
||||||
|
- выберет пользователей, зарегистрированных **после** даты, которую ты сам выберешь (например, **`2024‑01‑11`**);
|
||||||
|
- вернёт **`id`**, **`name`**, **`created_at`**;
|
||||||
|
- отсортирует по **`created_at`** от новых к старым.
|
||||||
|
2. Ограничь результат так, чтобы увидеть только 3 самых новых пользователя.
|
||||||
|
3. Измени запрос так, чтобы пропустить самого нового и показать следующих двух (используй **`LIMIT`** и **`OFFSET`**).
|
||||||
|
*/
|
||||||
|
SELECT id,
|
||||||
|
name,
|
||||||
|
country,
|
||||||
|
created_at,
|
||||||
|
email
|
||||||
|
FROM dsolovianov.users
|
||||||
|
ORDER BY created_at DESC, name ASC;
|
||||||
|
|
||||||
|
SELECT id,
|
||||||
|
name,
|
||||||
|
country
|
||||||
|
FROM dsolovianov.users
|
||||||
|
WHERE country LIKE 'US'
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
SELECT id,
|
||||||
|
name,
|
||||||
|
country
|
||||||
|
FROM dsolovianov.users
|
||||||
|
WHERE country LIKE 'UK'
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
SELECT id,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
LOWER(email) AS email_lower
|
||||||
|
FROM dsolovianov.users
|
||||||
|
WHERE email IS NOT NULL
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
SELECT id,
|
||||||
|
name,
|
||||||
|
created_at
|
||||||
|
FROM dsolovianov.users
|
||||||
|
WHERE created_at > '2024-01-11'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 3 OFFSET 1;
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/*INSERT INTO dsolovianov.products (name, category, price, in_stock) VALUES
|
||||||
|
('Mi Band 7', 'smartwatch', 49.00, true),
|
||||||
|
('Surface Pro', 'tablet', 1299.00, true),
|
||||||
|
('iPhone 15', 'smartphone', 999.00, true),
|
||||||
|
('Logitech MX Master', 'mouse', 120.00, true);*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
- Задача 1 — Товары для продажи
|
||||||
|
|
||||||
|
Ты настраиваешь главную витрину магазина. Нужно показывать только товары, которые сейчас реально готовы продавать.
|
||||||
|
|
||||||
|
**✅ Условия:**
|
||||||
|
|
||||||
|
- товар **в наличии**
|
||||||
|
- категория **НЕ smartwatch**
|
||||||
|
- цена **от 100 до 1300**
|
||||||
|
- отдельно хотим видеть цену:
|
||||||
|
- с наценкой 30% ( price + 30%)
|
||||||
|
- с «виртуальным НДС» 50% (price + 50%)
|
||||||
|
|
||||||
|
**✅ Вывести:**
|
||||||
|
|
||||||
|
- `name`, `category`, `price`
|
||||||
|
- `price_with_markup` = price + 30%
|
||||||
|
- `price_with_vat` = price + 50%
|
||||||
|
|
||||||
|
- Задача 2 — Скидочная корзина: дешёвые или дорогие
|
||||||
|
|
||||||
|
Нужно собрать товары под акцию
|
||||||
|
|
||||||
|
**✅ Условия:**
|
||||||
|
|
||||||
|
- участвуют товары с ценой **меньше 100** или **больше 1000**
|
||||||
|
- скидка 15% (price - 15%)
|
||||||
|
|
||||||
|
**✅ Вывести:**
|
||||||
|
|
||||||
|
- `name`, `price`
|
||||||
|
- `price_with_discount` = price - 15%
|
||||||
|
|
||||||
|
- Задача 3 — Категории для рекламы
|
||||||
|
|
||||||
|
Маркетолог просит товары только из нужных категорий:
|
||||||
|
|
||||||
|
**✅ Условия:**
|
||||||
|
|
||||||
|
- category в списке: `tablet`, `smartphone`, `mouse`
|
||||||
|
- цена **не меньше 100**
|
||||||
|
|
||||||
|
**✅ Вывести:**
|
||||||
|
|
||||||
|
- `id`, `name`, `category`, `price`
|
||||||
|
- сортировка по цене по убыванию
|
||||||
|
- Задача 4 — Убрать iPhone из выборки
|
||||||
|
|
||||||
|
Собираем список товаров, **кроме iPhone**, чтобы сделать подборку “не Apple”.
|
||||||
|
|
||||||
|
**✅ Условия:**
|
||||||
|
|
||||||
|
- название **НЕ содержит** `iPhone`
|
||||||
|
|
||||||
|
**✅ Вывести:**
|
||||||
|
|
||||||
|
- `name`, `category`, `price`
|
||||||
|
- Задача 5 — Поиск товаров по названию
|
||||||
|
|
||||||
|
Представь, что у пользователя есть строка поиска на сайте. Нужно сделать **2 запроса**, которые помогут искать товары по названию.
|
||||||
|
|
||||||
|
- 1) Найти товары, где в названии есть `"pro"` (без учёта регистра)
|
||||||
|
|
||||||
|
📌 Условия:
|
||||||
|
|
||||||
|
- поиск должен сработать на `pro`, `PRO`, `Pro`
|
||||||
|
- вывести: `name`, `category`, `price`
|
||||||
|
- 2) Найти товары, где в названии есть `"mx"` (без учёта регистра)
|
||||||
|
|
||||||
|
📌 Условия:
|
||||||
|
|
||||||
|
- вывести: `name`, `category`, `price`
|
||||||
|
- отсортировать по `price DESC`
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
SELECT name,
|
||||||
|
category,
|
||||||
|
price,
|
||||||
|
(price * 0.30) + price AS price_with_markup,
|
||||||
|
(price * 0.50) + price AS price_with_vat
|
||||||
|
FROM dsolovianov.products
|
||||||
|
WHERE category NOT IN ('smartwatch')
|
||||||
|
AND (price >= 100 AND price <= 1300);
|
||||||
|
|
||||||
|
SELECT name,
|
||||||
|
price,
|
||||||
|
price - (price * 0.15) AS price_with_discount
|
||||||
|
FROM dsolovianov.products
|
||||||
|
WHERE price < 100 OR price > 1000;
|
||||||
|
|
||||||
|
SELECT id,
|
||||||
|
name,
|
||||||
|
category,
|
||||||
|
price
|
||||||
|
FROM dsolovianov.products
|
||||||
|
WHERE category IN ('tablet', 'smartphone', 'mouse') AND price >= 100
|
||||||
|
ORDER BY price DESC;
|
||||||
|
|
||||||
|
SELECT name,
|
||||||
|
category,
|
||||||
|
price
|
||||||
|
FROM dsolovianov.products
|
||||||
|
WHERE category NOT LIKE '%iPhone%';
|
||||||
|
|
||||||
|
SELECT name,
|
||||||
|
category,
|
||||||
|
price
|
||||||
|
FROM dsolovianov.products
|
||||||
|
WHERE name ILIKE '%pro%';
|
||||||
|
|
||||||
|
SELECT name,
|
||||||
|
category,
|
||||||
|
price
|
||||||
|
FROM dsolovianov.products
|
||||||
|
WHERE name ILIKE '%mx%';
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
CREATE TABLE users (
|
||||||
|
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
age SMALLINT CHECK (age > 0),
|
||||||
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
country VARCHAR(50),
|
||||||
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
-- age - для возраста не нужны болшие числа
|
||||||
|
-- country - можно было бы использовать VARCHAR(2), я бы тут спросил у тимлида как будет удобней использовать поле)
|
||||||
|
-- created_at - нужно знать не только дату но и время создания/регистрации
|
||||||
|
-- id - если не прописать not null в primary key, то not null ставится автоматически?
|
||||||
|
|
||||||
|
CREATE TABLE products (
|
||||||
|
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
category VARCHAR(50),
|
||||||
|
price DECIMAL(10, 2) NOT NULL,
|
||||||
|
in_stock BOOLEAN DEFAULT true,
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE orders(
|
||||||
|
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
user_id INT,
|
||||||
|
order_date TIMESTAMPTZ NOT NULL,
|
||||||
|
status VARCHAR(20),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
-- status - лучше прописать словами статус, так понятней будет. Я бы сюда поставил ENUM
|
||||||
|
|
||||||
|
CREATE TABLE order_items(
|
||||||
|
order_id INT NOT NULL,
|
||||||
|
product_id INT NOT NULL,
|
||||||
|
quantity INT NOT NULL,
|
||||||
|
price_at_purchase DECIMAL(10, 2) NOT NULL,
|
||||||
|
PRIMARY KEY (order_id, product_id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
INSERT INTO users (name, age, email, country, is_active, created_at)
|
||||||
|
VALUES ('Alice', 25, LOWER('alice@example.com'), 'US', true, '2024-01-10 10:00:00'),
|
||||||
|
('Bob', 30, LOWER('bob@example.com'), 'UK', true, '2024-02-05 09:30:00'),
|
||||||
|
('Carol', 27, LOWER('carol@example.com'), 'US', false, '2024-03-01 15:20:00'),
|
||||||
|
('Diana', 22, LOWER('diana@GMAIL.com'), 'US', true, '2024-01-12 08:15:00'),
|
||||||
|
('Evan', 35, LOWER('EVAN@example.com'), 'UK', true, '2024-01-18 19:40:00'),
|
||||||
|
('Farida', 29, LOWER('farida@example.com'), 'KZ', true, '2024-02-20 11:05:00'),
|
||||||
|
('George', 31, LOWER('george+test@example.com'), 'US', true, '2024-03-05 16:45:00'),
|
||||||
|
('Ivan', 26, LOWER('ivan@example.com'), 'RU', true, '2024-01-11 14:30:00'),
|
||||||
|
('Julia', 28, LOWER('julia@example.com'), 'US', true, '2024-03-07 18:05:00');
|
||||||
|
|
||||||
|
INSERT INTO products (name, category, price, in_stock)
|
||||||
|
VALUES ('iPhone 15', 'Электроника', 80000, true),
|
||||||
|
('Kindle', 'Электроника', 15000, true),
|
||||||
|
('Кроссовки Nike', 'Одежда', 9000, true),
|
||||||
|
('Кроссовки NoName', 'Одежда', 3000, false),
|
||||||
|
('Наушники Pro', 'Электроника', 12000, true),
|
||||||
|
('Фитнес‑браслет', 'Гаджеты', 6000, true),
|
||||||
|
('Подарочная карта', 'Сервисы', 1000, true);
|
||||||
|
|
||||||
|
INSERT INTO orders (user_id, order_date, status)
|
||||||
|
VALUES (1, '2025-01-10 12:00:00', 'paid'),
|
||||||
|
(3, '2025-01-12 09:30:00', 'cancelled'),
|
||||||
|
(1, '2025-01-15 18:20:00', 'paid'),
|
||||||
|
(2, '2025-01-20 11:05:00', 'created'),
|
||||||
|
(4, '2025-02-01 17:45:00', 'paid'),
|
||||||
|
(5, '2025-02-10 10:10:00', 'refunded'),
|
||||||
|
(7, '2025-03-01 09:00:00', 'paid'),
|
||||||
|
(9, '2025-03-05 19:30:00', 'paid');
|
||||||
|
|
||||||
|
INSERT INTO order_items (order_id, product_id, quantity, price_at_purchase)
|
||||||
|
VALUES (1, 1, 1, 78000),
|
||||||
|
(1, 3, 2, 8500),
|
||||||
|
(2, 2, 1, 14000),
|
||||||
|
(3, 5, 1, 11000),
|
||||||
|
(3, 7, 1, 1000),
|
||||||
|
(4, 2, 1, 15000),
|
||||||
|
(5, 6, 1, 6000),
|
||||||
|
(6, 1, 1, 82000),
|
||||||
|
(7, 3, 1, 9000),
|
||||||
|
(8, 4, 2, 2800);
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
-- Задание 1
|
||||||
|
SELECT category, COUNT(*) AS products_count
|
||||||
|
FROM products
|
||||||
|
GROUP BY category
|
||||||
|
ORDER BY products_count DESC;
|
||||||
|
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. С ORDER BY гораздо удобней читать результат, без него данные отдаются в непонятном порядке.
|
||||||
|
-- 2. Для категории smartphone добавится +1 значение.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 2
|
||||||
|
SELECT order_id,
|
||||||
|
SUM(quantity) AS total_quantity,
|
||||||
|
SUM(quantity * price_at_purchase) AS order_revenue
|
||||||
|
FROM order_items
|
||||||
|
GROUP BY order_id
|
||||||
|
ORDER BY order_revenue DESC;
|
||||||
|
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. Если убрать GROUP BY и order_id то запрос выдаст 1 строку со всеми суммами total_quantity и order_revenue.
|
||||||
|
-- Если убрать только GROUP BY и не убрать order_id то выдаст ошибку. Так как нельзя оставлять неагрегированные столбцы без GROUP BY.
|
||||||
|
-- 2. Если добавить product_id без добавления её в GROUP BY или агрегат то выдаст ошибку.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 3
|
||||||
|
SELECT p.category,
|
||||||
|
SUM(oi.quantity * oi.price_at_purchase) AS category_revenue
|
||||||
|
FROM order_items oi
|
||||||
|
JOIN products p ON p.id = oi.product_id
|
||||||
|
GROUP BY p.category
|
||||||
|
ORDER BY category_revenue;
|
||||||
|
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. По задаче нам же нужно сгруппировать и посчитать сумму по категориям а не по id товарам.
|
||||||
|
-- 2. Если добавить p.name в SELECT без добавления её в GROUP BY или агрегат то выдаст ошибку. Нельзя вставлять неагрегированные столбцы без GROUP BY.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 4
|
||||||
|
SELECT oi.order_id,
|
||||||
|
SUM(oi.quantity * oi.price_at_purchase) AS order_revenue
|
||||||
|
FROM orders o
|
||||||
|
JOIN order_items oi ON oi.order_id = o.id
|
||||||
|
WHERE o.status = 'paid'
|
||||||
|
GROUP BY oi.order_id
|
||||||
|
ORDER BY order_revenue DESC;
|
||||||
|
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. В этом задании мы получили сумму(выручку) по заказам оплаченным, а в прошлом задании мы получили сумму(выручку) по категориям товаров.
|
||||||
|
-- 2. Посчитать выручку, сколько заработала компания. Остальные заказы еще не оплачены.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 5
|
||||||
|
SELECT p.category,
|
||||||
|
SUM(oi.quantity * oi.price_at_purchase) AS category_revenue
|
||||||
|
FROM order_items oi
|
||||||
|
JOIN products p ON p.id = oi.product_id
|
||||||
|
GROUP BY p.category
|
||||||
|
HAVING SUM(oi.quantity * oi.price_at_purchase) > 1000
|
||||||
|
ORDER BY category_revenue;
|
||||||
|
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. WHERE не принимает агрегирующие функции, можно сделать так - WHERE oi.quantity * oi.price_at_purchase > 1000
|
||||||
|
-- 2. WHERE фильтрует до GROUP BY, а HAVING после GROUP BY.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 6
|
||||||
|
SELECT country,
|
||||||
|
COUNT(*) AS active_users_count
|
||||||
|
FROM users
|
||||||
|
WHERE is_active = TRUE
|
||||||
|
GROUP BY country
|
||||||
|
HAVING COUNT(*) > 2
|
||||||
|
ORDER BY active_users_count DESC;
|
||||||
|
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. Предположу что отбрасывает неактивных пользователей сразу, перед групировкой и агрегаций. В дальнейшем может сэкономить на времени вычисления, особенно если данных будет много.
|
||||||
|
-- 2. Пустую таблицу без строк.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 7
|
||||||
|
SELECT p.name AS product_name,
|
||||||
|
SUM(oi.quantity) AS total_quantity_sold
|
||||||
|
FROM order_items oi
|
||||||
|
JOIN products p ON p.id = oi.product_id
|
||||||
|
GROUP BY product_name
|
||||||
|
ORDER BY total_quantity_sold DESC
|
||||||
|
LIMIT 3;
|
||||||
|
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. Тут мы выясняем какой товар самый ходовой и сколько его продается, а рейтинг по выручке показывает сколько зарабатывает фирма от продажи товара.
|
||||||
|
-- 2. В случаях когда, например, нужно выяснить какой товар хорошо продается чтоб его закупить на склад для продажи.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 8
|
||||||
|
SELECT order_id,
|
||||||
|
SUM(quantity * price_at_purchase) AS order_revenue
|
||||||
|
FROM order_items
|
||||||
|
GROUP BY order_id
|
||||||
|
HAVING SUM(quantity * price_at_purchase) < 100
|
||||||
|
ORDER BY order_revenue DESC;
|
||||||
|
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. Есть товары дешевые и люди которые мало зарабатывают, ещё есть всякие скидки на ряд товаров.
|
||||||
|
-- 2. Я думаю проще увидеть отфильтрованные заказы, чем листать весь список с заказами.
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
-- Задача 1
|
||||||
|
SELECT u.name
|
||||||
|
FROM users u
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT o.user_id
|
||||||
|
FROM orders o
|
||||||
|
)
|
||||||
|
ORDER BY u.name;
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. JOIN склеивает одну таблицу со второй, а подзапрос с IN выполняет запрос и получает список с user_id,
|
||||||
|
-- далее по этому списку отфильтровывает пользователей в основном запросе.
|
||||||
|
-- 2. Когда запрос простой и короткий.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задача 2
|
||||||
|
SELECT u.id,
|
||||||
|
u.name,
|
||||||
|
u.email
|
||||||
|
FROM users u
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM orders o
|
||||||
|
WHERE o.user_id = u.id
|
||||||
|
);
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. Предположу что NOT EXISTS более бещопасно работает с null.
|
||||||
|
-- 2. Отправить таким пользователям рекомендацию, специальный промокод на скидку для первого заказа или рекламу.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задача 3
|
||||||
|
SELECT u.name,
|
||||||
|
(
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM orders o
|
||||||
|
WHERE u.id = o.user_id
|
||||||
|
) AS orders_count
|
||||||
|
FROM users u
|
||||||
|
ORDER BY orders_count DESC, name;
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. Если подзапрос SELECT вернётся две и более строки то будет ошибка.
|
||||||
|
-- 2. Сложность возрастет и скорее всего нужно будет оптимизировать запрос.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задача 4
|
||||||
|
WITH total_revenue_cte AS (
|
||||||
|
SELECT o.user_id,
|
||||||
|
SUM(oi.quantity * oi.price_at_purchase) AS total_price
|
||||||
|
FROM orders o
|
||||||
|
JOIN order_items oi ON oi.order_id = o.id
|
||||||
|
GROUP BY o.user_id
|
||||||
|
)
|
||||||
|
SELECT u.name,
|
||||||
|
trc.total_price AS total_revenue
|
||||||
|
FROM users u
|
||||||
|
JOIN total_revenue_cte trc ON trc.user_id = u.id
|
||||||
|
ORDER BY total_revenue DESC;
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. Проще читать запрос "посчитать выручку по пользователям" когда вынесен отдельно.
|
||||||
|
-- 2. Вроде как в postgres можно в базу данных записывать функции, если я ничего не путаю.
|
||||||
|
-- И далее вызывать эту функцию при необходимости, так можно избежать дублирования кода.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задача 5
|
||||||
|
WITH product_price_at_purchase_cte AS (
|
||||||
|
SELECT oi.product_id,
|
||||||
|
SUM(oi.quantity * oi.price_at_purchase) AS product_revenue
|
||||||
|
FROM order_items oi
|
||||||
|
GROUP BY oi.product_id
|
||||||
|
)
|
||||||
|
SELECT p.name AS product_name,
|
||||||
|
p.category,
|
||||||
|
ppps.product_revenue
|
||||||
|
FROM products p
|
||||||
|
JOIN product_price_at_purchase_cte ppps ON ppps.product_id = p.id
|
||||||
|
ORDER BY ppps.product_revenue DESC
|
||||||
|
LIMIT 3;
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. Возможно потому что products оcновная таблица, сналчала нужно из order_items достать агрегацию по id а потом склеивать с products.
|
||||||
|
-- 2. Через CTE мы получаем готовую таблицу и далее делаем с ней что хотим,
|
||||||
|
-- а в гуруппировке через GROUP BY нельзя добавить дополнительные поля name и category.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задача 6
|
||||||
|
WITH total_revenue_cte AS (
|
||||||
|
SELECT o.user_id,
|
||||||
|
SUM(oi.quantity * oi.price_at_purchase) AS total_revenue
|
||||||
|
FROM orders o
|
||||||
|
JOIN order_items oi ON oi.order_id = o.id
|
||||||
|
GROUP BY o.user_id
|
||||||
|
)
|
||||||
|
SELECT u.name,
|
||||||
|
u.email,
|
||||||
|
trc.total_revenue
|
||||||
|
FROM users u
|
||||||
|
JOIN total_revenue_cte trc ON trc.user_id = u.id
|
||||||
|
WHERE u.is_active = TRUE AND trc.total_revenue > 1000
|
||||||
|
ORDER BY total_revenue DESC;
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. Думаю как то так - user_most_expenses
|
||||||
|
-- 2. Можно предлагать рекламу товаров, анализировать что пользователи покупали и потом предлагать схожую продукцию.
|
||||||
|
|
||||||
|
|
||||||
|
WITH order_revenue_cte AS (
|
||||||
|
SELECT order_id,
|
||||||
|
SUM(quantity * price_at_purchase) AS order_revenue
|
||||||
|
FROM order_items
|
||||||
|
GROUP BY order_id
|
||||||
|
)
|
||||||
|
SELECT o.id,
|
||||||
|
o.order_date,
|
||||||
|
oc.order_revenue
|
||||||
|
FROM orders o
|
||||||
|
JOIN order_revenue_cte oc ON oc.order_id = o.id
|
||||||
|
WHERE o.status = 'paid' AND oc.order_revenue < 100;
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. Какие реальные причины таких заказов, я думаю что просто не все люди богатые что бы заказывать на большие суммы (если я правильно понял статус paid)
|
||||||
|
-- Чтобы узнать злоупотребления промокодами нужно копаться в других данных, где хранятся данные о введенных промокодах.
|
||||||
|
-- 2. Я думаю выгоднее фильтровать внутри подзапроса, чтоб подзапрос не создавал большую таблицу и основному запросу не пришлось ее перебирать.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
-- Задача 1
|
||||||
|
SELECT name, category, price,
|
||||||
|
price * 1.3 AS price_with_markup,
|
||||||
|
price * 1.5 AS price_with_vat
|
||||||
|
FROM products
|
||||||
|
WHERE (price BETWEEN 100 AND 1300)
|
||||||
|
AND category NOT IN ('smartwatch')
|
||||||
|
AND in_stock = TRUE;
|
||||||
|
|
||||||
|
-- Вопрос:
|
||||||
|
-- Что лучше читается BETWEEN 100 AND 1300 или price >= 100 AND price <= 1300 и почему?
|
||||||
|
-- Ответ:
|
||||||
|
-- Для меня BETWEEN 100 AND 1300 читается лучше, сразу понятен срез от 100 и 1300.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задача 2
|
||||||
|
SELECT name, price,
|
||||||
|
price * 0.85 AS price_with_discount
|
||||||
|
FROM products
|
||||||
|
WHERE (price < 100 OR price > 1000);
|
||||||
|
|
||||||
|
-- Вопрос:
|
||||||
|
-- Почему важно явно писать скобки в условии с OR, даже если кажется, что и так понятно?
|
||||||
|
-- Ответ:
|
||||||
|
-- Потому что условие может не правильно отобразить результат, AND выполняется раньше OR.
|
||||||
|
-- Для надежности лучше скобками прописывать явно.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задача 3
|
||||||
|
SELECT id, name, category, price
|
||||||
|
FROM products
|
||||||
|
WHERE price >= 100
|
||||||
|
AND category IN ('tablet', 'smartphone', 'mouse')
|
||||||
|
ORDER BY price DESC;
|
||||||
|
|
||||||
|
|
||||||
|
-- Задача 4
|
||||||
|
SELECT name, category, price
|
||||||
|
FROM products
|
||||||
|
WHERE name NOT LIKE '%iPhone%';
|
||||||
|
|
||||||
|
|
||||||
|
-- Задача 5
|
||||||
|
SELECT name, category, price
|
||||||
|
FROM products
|
||||||
|
WHERE name ILIKE '%pro%';
|
||||||
|
|
||||||
|
SELECT name, category, price
|
||||||
|
FROM products
|
||||||
|
WHERE name ILIKE '%mx%'
|
||||||
|
ORDER BY price DESC;
|
||||||
|
|
||||||
|
-- Вопрос:
|
||||||
|
-- В одном предложении объясни, почему для поисковой строки лучше ILIKE '%pro%', а не = 'Pro'.
|
||||||
|
-- Ответ:
|
||||||
|
-- В названии товаров могут быть буквы (в словах) с разными регистрами, при поиске клиенту можен не показать часть товаров только потому что слово выглядить так - PRO а не так - Pro.
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
-- Задание 1
|
||||||
|
SELECT o.id AS order_id,
|
||||||
|
o.order_date,
|
||||||
|
o.status,
|
||||||
|
u.name AS user_name,
|
||||||
|
u.country
|
||||||
|
FROM orders o
|
||||||
|
JOIN users u ON o.user_id = u.id;
|
||||||
|
|
||||||
|
-- Ответ на вопросы
|
||||||
|
-- 1. Логичнее начать c orders, так как нам в первую очередь нужны все заказы,
|
||||||
|
-- а потом только пользователи которые их сделали.
|
||||||
|
|
||||||
|
-- 2. Я думаю уместнее сделать LEFT.
|
||||||
|
-- Я пробовал сделать с INNER и LEFT и у меня не пропали строки.
|
||||||
|
-- Получается это из за того что у каждого заказа есть пользователь, не дуаю что должен создастся заказ без пользователя.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 2
|
||||||
|
SELECT o.id AS order_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
oi.quantity,
|
||||||
|
oi.price_at_purchase
|
||||||
|
FROM orders o
|
||||||
|
JOIN order_items oi ON oi.order_id = o.id
|
||||||
|
JOIN products p ON p.id = oi.product_id
|
||||||
|
WHERE o.id = 101;
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- Выдаст дублированный состав заказа с id 101,
|
||||||
|
-- получается он будет обращаться к каждаму заказу, но отображать продукты заказа с id 101
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 3
|
||||||
|
SELECT o.id AS order_id,
|
||||||
|
u.name AS name,
|
||||||
|
p.name AS product_name,
|
||||||
|
oi.quantity,
|
||||||
|
oi.price_at_purchase
|
||||||
|
FROM orders o
|
||||||
|
JOIN users u ON u.id = o.user_id
|
||||||
|
JOIN order_items oi ON oi.order_id = o.id
|
||||||
|
JOIN products p ON p.id = oi.product_id;
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- 1. Появляется дубликат заказов потому что у 3 заказа есть несколько товаров, join превращает(размножает) их в несколько строк.
|
||||||
|
-- 2. Отличается тем что мы выводим все заказы с товарами а не один заказ.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 4
|
||||||
|
SELECT u.name AS user_name,
|
||||||
|
o.id AS order_id,
|
||||||
|
o.status
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN orders o ON o.user_id = u.id;
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- INNER JOIN не выдаст пользователей без заказа. Только тех у кого есть пара с orders.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 5
|
||||||
|
SELECT u.id AS user_id,
|
||||||
|
u.name AS user_name
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN orders o ON o.user_id = u.id
|
||||||
|
WHERE o.id IS NULL;
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- 1. Проверять IS NULL нужно по полю orders.id
|
||||||
|
-- 2. Мы должны в запросе получить всех пользователей, а потом в фильтре "узнать" есть ли у них заказы.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 6
|
||||||
|
SELECT o.id AS order_id,
|
||||||
|
u.name AS user_name,
|
||||||
|
p.name AS product_name,
|
||||||
|
oi.quantity
|
||||||
|
FROM orders o
|
||||||
|
JOIN users u ON u.id = o.user_id
|
||||||
|
JOIN order_items oi ON oi.order_id = o.id
|
||||||
|
JOIN products p ON p.id = oi.product_id
|
||||||
|
WHERE p.name ILIKE '%kindle%';
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- В заказе 2 есть только один товар с названием Kindle.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 7
|
||||||
|
SELECT p.name AS product_name,
|
||||||
|
oi.order_id,
|
||||||
|
oi.quantity
|
||||||
|
FROM products p
|
||||||
|
LEFT JOIN order_items oi ON oi.product_id = p.id;
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- 1. order_id будет NULL
|
||||||
|
-- 2. Если использовать INNER JOIN то он не покажет товар который не заказывали.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 8
|
||||||
|
SELECT o.id AS order_id,
|
||||||
|
u.name AS user_name,
|
||||||
|
p.name AS product_name,
|
||||||
|
oi.quantity,
|
||||||
|
oi.price_at_purchase
|
||||||
|
FROM products p
|
||||||
|
JOIN order_items oi ON oi.product_id = p.id
|
||||||
|
JOIN orders o ON o.id = oi.order_id
|
||||||
|
JOIN users u ON u.id = o.user_id
|
||||||
|
WHERE o.status = 'paid';
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- У меня получился тот же результат если написать вот так - JOIN orders o ON o.id = oi.order_id and o.status = 'paid'
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 9
|
||||||
|
SELECT o.id AS order_id,
|
||||||
|
o.order_date,
|
||||||
|
p.name AS product_name,
|
||||||
|
oi.quantity,
|
||||||
|
oi.price_at_purchase
|
||||||
|
FROM products p
|
||||||
|
JOIN order_items oi ON oi.product_id = p.id
|
||||||
|
JOIN orders o ON o.id = oi.order_id
|
||||||
|
JOIN users u ON u.id = o.user_id
|
||||||
|
WHERE u.name = 'Alice';
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- Если у пользователя нет ни одного заказа, то этот запрос вернет пустую строку.
|
||||||
|
-- Не вернет потому что нечего отобразить таблице, если нет заказов то и нет купленных товаров.
|
||||||
|
-- Но если прописать RIGHT JOIN users u ON u.id = o.user_id то отобразит все строки NULL.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 10
|
||||||
|
SELECT o.id AS order_id,
|
||||||
|
o.user_id,
|
||||||
|
o.status
|
||||||
|
FROM orders o
|
||||||
|
LEFT JOIN order_items oi ON oi.order_id = o.id
|
||||||
|
WHERE oi.order_id IS NULL;
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- Такой запрос выдаст пустые строки.
|
||||||
|
-- INNER JOIN должен выдать строки только если есть "пара".
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
-- Задание 1
|
||||||
|
SELECT id, name, country, created_at, email
|
||||||
|
FROM users
|
||||||
|
ORDER BY created_at DESC, name;
|
||||||
|
|
||||||
|
-- Почему выбрал именно эти столбцы и зачем такая сортировка?
|
||||||
|
-- Ответ на вопрос:
|
||||||
|
-- Эти столбцы нужны для анализа пользователей, для тех же аналитиков.
|
||||||
|
-- Сортировка по дате и имени, я так предпологаю, нужня для удобства.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 2
|
||||||
|
-- Запрос 1
|
||||||
|
SELECT id, name, country
|
||||||
|
FROM users
|
||||||
|
WHERE country = 'US'
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
-- Запрос 2
|
||||||
|
SELECT id, name, country
|
||||||
|
FROM users
|
||||||
|
WHERE country = 'UK'
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
-- Какой из двух запросов маркетологу полезнее и почему?
|
||||||
|
-- Ответ на вопрос:
|
||||||
|
-- Я так понимаю маркетологу нужно понять какое кол-во пользователей в каждой из стран зарегестрировалось в приложении.
|
||||||
|
-- Возможно для понимания продвижения продукта и насколько успешна маркетинговая стратегия. И в дальнейшем её корректировка.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 3
|
||||||
|
-- Запрос 1
|
||||||
|
SELECT id, name, email
|
||||||
|
FROM users
|
||||||
|
WHERE email IS NOT NULL -- я знаю что email и так должен быть NOT NULL по DDL, но на всякий прописал
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
-- Запрос 2
|
||||||
|
SELECT DISTINCT LOWER(email) as email_lower
|
||||||
|
FROM users
|
||||||
|
WHERE email IS NOT NULL;
|
||||||
|
|
||||||
|
-- Зачем вообще приводить email к одному регистру и убирать дубликаты на стороне базы?
|
||||||
|
-- Ответ на вопрос:
|
||||||
|
-- Приводить email к нижнему регистру необходимо для того чтобы небыло дубликатов почты.
|
||||||
|
-- После гугления я узнал что в почтовых адресах могут допускаться верхние регистры букв, только потом создать email с теми же буквами но в нижнем регистре нельзя будет, они будут считаться индентичными.
|
||||||
|
-- Получается несколько пользователей могут залогинится под одним и тем же email адресом но с разными регистрами. А такое нельзя допускать.
|
||||||
|
-- Для рассылки дубликаты нужно убирать что-бы не приходила рассылка дважды в один и тот же email.
|
||||||
|
|
||||||
|
-- Задание 4
|
||||||
|
-- Запрос 1
|
||||||
|
SELECT id, name, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE created_at > '2024-01-14'
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
|
||||||
|
-- Запрос 2
|
||||||
|
SELECT id, name, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE created_at > '2024-01-14'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 3;
|
||||||
|
|
||||||
|
-- Запрос 3
|
||||||
|
SELECT id, name, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE created_at > '2024-01-14'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
OFFSET 1 LIMIT 2;
|
||||||
|
|
||||||
|
-- В каком реальном сценарии тебе пригодится OFFSET?
|
||||||
|
-- Ответ на вопрос:
|
||||||
|
-- Не знаю, возможно понадобится посмотреть несколько записей из середины списка.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- Задание 1
|
||||||
|
SELECT id, name, 'active' AS user_status
|
||||||
|
FROM users
|
||||||
|
WHERE is_active = TRUE
|
||||||
|
UNION ALL
|
||||||
|
SELECT id, name, 'inactive' AS user_status
|
||||||
|
FROM users
|
||||||
|
WHERE is_active = FALSE
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- 1. Количество строк не изменилось
|
||||||
|
-- 2. Предположу, что подача информации в таком виде более информативней(понятней).
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 2
|
||||||
|
SELECT id AS order_id, user_id, order_date, 'old' AS period
|
||||||
|
FROM orders
|
||||||
|
WHERE order_date < '2024-01-01'
|
||||||
|
UNION ALL
|
||||||
|
SELECT id AS order_id, user_id, order_date, 'new' AS period
|
||||||
|
FROM orders
|
||||||
|
WHERE order_date >= '2024-01-01'
|
||||||
|
ORDER BY order_date, order_id;
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- 1. В dbeaver в самой левой колонке отображается номерация строк, а снизу общее количество строк который выдал запрос.
|
||||||
|
-- На последней строке с old можно увидеть число в самой левой колонке, это и есть количество строк c old. Далее отнимаем общее кол-во строк от old и получаем кол-во new.
|
||||||
|
-- 2. Тяжело ответить на такой вопрос, не знаю. Если у меня были бы сомнения, я бы у новичка спросил.
|
||||||
|
|
||||||
|
|
||||||
|
-- Задание 3
|
||||||
|
SELECT DISTINCT p.id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
'ordered' AS product_status
|
||||||
|
FROM products p
|
||||||
|
JOIN order_items oi ON oi.product_id = p.id
|
||||||
|
UNION ALL
|
||||||
|
SELECT DISTINCT p.id AS product_id,
|
||||||
|
p.name AS product_name,
|
||||||
|
'never_ordered' AS product_status
|
||||||
|
FROM products p
|
||||||
|
LEFT JOIN order_items oi ON oi.product_id = p.id
|
||||||
|
WHERE oi.product_id IS NULL
|
||||||
|
ORDER BY product_name;
|
||||||
|
|
||||||
|
-- Ответ на вопрос
|
||||||
|
-- 1. Нам нужно чтобы товар(products) ни разу не заказывали(order_items), получается что мы проверяем есть ли ссылка в каком нибудь заказе на продукт.
|
||||||
|
-- 2. Дает сигнал бизнесу что товар не хотят покупать, возможно на него нужно сделать скидку или прорекламировать товар у блогера какого нибудь.
|
||||||
Reference in New Issue
Block a user