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
|
||||
DROP TABLE page_views;
|
||||
|
||||
CREATE TABLE page_views (
|
||||
CREATE TABLE page_views
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id UInt64,
|
||||
url String,
|
||||
view_time DateTime
|
||||
@@ -11,7 +14,9 @@ ORDER BY (user_id, view_time);
|
||||
|
||||
--Задача 2: Создай таблицу users
|
||||
|
||||
CREATE TABLE users (
|
||||
CREATE TABLE users
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
id UInt64,
|
||||
name String,
|
||||
updated_at DateTime
|
||||
@@ -21,7 +26,9 @@ ORDER BY id;
|
||||
|
||||
-- Задача 3: Создай таблицу region_sales
|
||||
|
||||
CREATE TABLE region_sales (
|
||||
CREATE TABLE region_sales
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
region String,
|
||||
date Date,
|
||||
sales UInt64
|
||||
@@ -30,17 +37,19 @@ ENGINE = SummingMergeTree()
|
||||
PARTITION BY toYYYYMM(date)
|
||||
ORDER BY (region, date);
|
||||
|
||||
--Задача 4.1: Создай таблицу user_metrics
|
||||
--Задача 4: Создай таблицу user_metrics
|
||||
|
||||
CREATE TABLE user_metrics (
|
||||
CREATE TABLE user_metrics
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
region String,
|
||||
avg_age_state AggregateFunction(avg, UInt8),
|
||||
active_users_state AggregateFunction(uniq, UInt64)
|
||||
)
|
||||
ENGINE = AggregatingMergeTree()
|
||||
ORDER BY region;
|
||||
ENGINE = AggregatingMergeTree() --Подзадача 4.2
|
||||
ORDER BY region; --Подзадача 4.3
|
||||
|
||||
--Задача 4.3: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()`
|
||||
--Подзадача 4.4: - Вставь данные и Читай их через `avgMerge()` и `uniqMerge()`
|
||||
|
||||
INSERT INTO user_metrics
|
||||
SELECT
|
||||
@@ -61,7 +70,7 @@ FROM
|
||||
)
|
||||
GROUP BY region;
|
||||
|
||||
--Задача 4.4: - Читай их через `avgMerge()` и `uniqMerge()`
|
||||
--Подзадача 4.5: - Читай их через `avgMerge()` и `uniqMerge()`
|
||||
|
||||
SELECT
|
||||
region,
|
||||
@@ -0,0 +1,90 @@
|
||||
DROP TABLE IF EXISTS dsolovianov.core_fact_lesson_views ON CLUSTER clickhouse_cluster SYNC;
|
||||
DROP TABLE IF EXISTS dsolovianov.core_dim_user ON CLUSTER clickhouse_cluster SYNC;
|
||||
DROP TABLE IF EXISTS dsolovianov.core_dim_course ON CLUSTER clickhouse_cluster SYNC;
|
||||
DROP TABLE IF EXISTS dsolovianov.core_dim_lesson ON CLUSTER clickhouse_cluster SYNC;
|
||||
DROP TABLE IF EXISTS dsolovianov.core_dim_date ON CLUSTER clickhouse_cluster SYNC;
|
||||
DROP TABLE IF EXISTS dsolovianov.core_enrollments ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.core_dim_user
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id Int32,
|
||||
name String,
|
||||
age Int32,
|
||||
email String,
|
||||
registration_date Date
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_dim_user',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
|
||||
CREATE TABLE dsolovianov.core_dim_course
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
course_id Int32,
|
||||
title String,
|
||||
category String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_dim_course',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY course_id;
|
||||
|
||||
CREATE TABLE dsolovianov.core_dim_lesson
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
lesson_id Int32,
|
||||
title String,
|
||||
duration_min Int32,
|
||||
course_id Int32
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_dim_lesson',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY lesson_id;
|
||||
|
||||
CREATE TABLE dsolovianov.core_dim_date
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
date Date,
|
||||
year Int32,
|
||||
month Int32,
|
||||
day Int32,
|
||||
weekday String
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_dim_date',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY date;
|
||||
|
||||
CREATE TABLE dsolovianov.core_enrollments
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id Int32,
|
||||
course_id Int32,
|
||||
enrolled_at DateTime64(3)
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_enrollments',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
|
||||
CREATE TABLE dsolovianov.core_fact_lesson_views
|
||||
ON CLUSTER clickhouse_cluster
|
||||
(
|
||||
user_id Int32,
|
||||
lesson_id Int32,
|
||||
course_id Int32,
|
||||
viewed_at DateTime64(3)
|
||||
)
|
||||
ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/core_fact_lesson_views',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
@@ -0,0 +1,193 @@
|
||||
-- Этап 2. Создание витринных таблиц --
|
||||
|
||||
-- Витрина lesson_popularity_summary
|
||||
DROP TABLE IF EXISTS dsolovianov.lesson_popularity_summary ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.lesson_popularity_summary ON CLUSTER clickhouse_cluster (
|
||||
lesson_id UInt32,
|
||||
lesson_title String,
|
||||
course_id UInt32,
|
||||
course_title String,
|
||||
total_views UInt64,
|
||||
unique_users UInt64,
|
||||
first_view DateTime,
|
||||
last_view DateTime
|
||||
) ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/lesson_popularity_summary',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY (course_id, lesson_id);
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.lesson_popularity_summary_mv ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE MATERIALIZED VIEW dsolovianov.lesson_popularity_summary_mv ON CLUSTER clickhouse_cluster
|
||||
TO dsolovianov.lesson_popularity_summary
|
||||
AS
|
||||
SELECT
|
||||
l.lesson_id AS lesson_id,
|
||||
l.title AS lesson_title,
|
||||
l.course_id AS course_id,
|
||||
c.title AS course_title,
|
||||
COUNT(v.viewed_at) AS total_views,
|
||||
COUNT(DISTINCT v.user_id) AS unique_users,
|
||||
MIN(v.viewed_at) AS first_view,
|
||||
MAX(v.viewed_at) AS last_view
|
||||
FROM dsolovianov.core_fact_lesson_views v
|
||||
LEFT JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||
LEFT JOIN dsolovianov.core_dim_course c ON l.course_id = c.course_id
|
||||
GROUP BY l.lesson_id, l.title, l.course_id, c.title;
|
||||
|
||||
INSERT INTO dsolovianov.lesson_popularity_summary
|
||||
SELECT
|
||||
l.lesson_id AS lesson_id,
|
||||
l.title AS lesson_title,
|
||||
l.course_id AS course_id,
|
||||
c.title AS course_title,
|
||||
COUNT(v.viewed_at) AS total_views,
|
||||
COUNT(DISTINCT v.user_id) AS unique_users,
|
||||
MIN(v.viewed_at) AS first_view,
|
||||
MAX(v.viewed_at) AS last_view
|
||||
FROM dsolovianov.core_dim_lesson l
|
||||
JOIN dsolovianov.core_dim_course c ON l.course_id = c.course_id
|
||||
LEFT JOIN dsolovianov.core_fact_lesson_views v ON l.lesson_id = v.lesson_id
|
||||
GROUP BY l.lesson_id, l.title, l.course_id, c.title;
|
||||
|
||||
-- Витрину inactive_users_summary
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.inactive_users_summary ON CLUSTER clickhouse_cluster (
|
||||
user_id UInt32,
|
||||
name String,
|
||||
email String,
|
||||
age UInt32,
|
||||
registered_courses_count UInt32,
|
||||
registration_date Date
|
||||
) ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/inactive_users_summary',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY user_id;
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.inactive_users_summary_mv ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE MATERIALIZED VIEW dsolovianov.inactive_users_summary_mv ON CLUSTER clickhouse_cluster
|
||||
TO dsolovianov.inactive_users_summary
|
||||
AS
|
||||
SELECT
|
||||
u.user_id AS user_id,
|
||||
u.name AS name,
|
||||
u.email AS email,
|
||||
u.age AS age,
|
||||
COUNT(DISTINCT e.course_id) AS registered_courses_count,
|
||||
u.registration_date AS registration_date
|
||||
FROM dsolovianov.core_dim_user u
|
||||
LEFT JOIN dsolovianov.core_enrollments e ON u.user_id = e.user_id
|
||||
WHERE u.user_id NOT IN (SELECT DISTINCT user_id FROM dsolovianov.core_fact_lesson_views)
|
||||
GROUP BY u.user_id, u.name, u.email, u.age, u.registration_date;
|
||||
|
||||
INSERT INTO dsolovianov.inactive_users_summary
|
||||
SELECT
|
||||
u.user_id AS user_id,
|
||||
u.name AS name,
|
||||
u.email AS email,
|
||||
u.age AS age,
|
||||
COUNT(DISTINCT e.course_id) AS registered_courses_count,
|
||||
u.registration_date AS registration_date
|
||||
FROM dsolovianov.core_dim_user u
|
||||
LEFT JOIN dsolovianov.core_enrollments e ON u.user_id = e.user_id
|
||||
WHERE u.user_id NOT IN (SELECT DISTINCT user_id FROM dsolovianov.core_fact_lesson_views)
|
||||
GROUP BY u.user_id, u.name, u.email, u.age, u.registration_date;
|
||||
|
||||
-- Витрина course_completion_rate
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.course_completion_rate ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE TABLE dsolovianov.course_completion_rate ON CLUSTER clickhouse_cluster (
|
||||
user_id UInt32,
|
||||
user_name String,
|
||||
course_id UInt32,
|
||||
course_title String,
|
||||
lessons_in_course UInt32,
|
||||
lessons_viewed UInt32,
|
||||
completion_rate Float32
|
||||
) ENGINE = ReplicatedMergeTree(
|
||||
'/clickhouse/tables/{shard}/dsolovianov/course_completion_rate',
|
||||
'{replica}'
|
||||
)
|
||||
ORDER BY (user_id, course_id);
|
||||
|
||||
DROP TABLE IF EXISTS dsolovianov.course_completion_rate_mv ON CLUSTER clickhouse_cluster SYNC;
|
||||
|
||||
CREATE MATERIALIZED VIEW dsolovianov.course_completion_rate_mv ON CLUSTER clickhouse_cluster
|
||||
TO dsolovianov.course_completion_rate
|
||||
AS
|
||||
SELECT
|
||||
e.user_id AS user_id,
|
||||
u.name AS user_name,
|
||||
e.course_id AS course_id,
|
||||
c.title AS course_title,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM dsolovianov.core_dim_lesson l
|
||||
WHERE l.course_id = e.course_id
|
||||
) AS lessons_in_course,
|
||||
(
|
||||
SELECT COUNT(DISTINCT v.lesson_id)
|
||||
FROM dsolovianov.core_fact_lesson_views v
|
||||
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||
) AS lessons_viewed,
|
||||
(
|
||||
SELECT COUNT(DISTINCT v.lesson_id)
|
||||
FROM dsolovianov.core_fact_lesson_views v
|
||||
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||
) / (
|
||||
SELECT COUNT(*)
|
||||
FROM dsolovianov.core_dim_lesson l
|
||||
WHERE l.course_id = e.course_id
|
||||
) AS completion_rate
|
||||
FROM dsolovianov.core_enrollments e
|
||||
JOIN dsolovianov.core_dim_user u ON e.user_id = u.user_id
|
||||
JOIN dsolovianov.core_dim_course c ON e.course_id = c.course_id
|
||||
GROUP BY e.user_id, u.name, e.course_id, c.title;
|
||||
|
||||
INSERT INTO dsolovianov.course_completion_rate
|
||||
SELECT
|
||||
e.user_id AS user_id,
|
||||
u.name AS user_name,
|
||||
e.course_id AS course_id,
|
||||
c.title AS course_title,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM dsolovianov.core_dim_lesson l
|
||||
WHERE l.course_id = e.course_id
|
||||
) AS lessons_in_course,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT COUNT(DISTINCT v.lesson_id)
|
||||
FROM dsolovianov.core_fact_lesson_views v
|
||||
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||
), 0
|
||||
) AS lessons_viewed,
|
||||
COALESCE(
|
||||
(
|
||||
SELECT COUNT(DISTINCT v.lesson_id)
|
||||
FROM dsolovianov.core_fact_lesson_views v
|
||||
JOIN dsolovianov.core_dim_lesson l ON v.lesson_id = l.lesson_id
|
||||
WHERE v.user_id = e.user_id AND l.course_id = e.course_id
|
||||
), 0
|
||||
) / (
|
||||
SELECT COUNT(*)
|
||||
FROM dsolovianov.core_dim_lesson l
|
||||
WHERE l.course_id = e.course_id
|
||||
) AS completion_rate
|
||||
FROM dsolovianov.core_enrollments e
|
||||
JOIN dsolovianov.core_dim_user u ON e.user_id = u.user_id
|
||||
JOIN dsolovianov.core_dim_course c ON e.course_id = c.course_id
|
||||
GROUP BY e.user_id, u.name, e.course_id, c.title;
|
||||
|
||||
SELECT * FROM dsolovianov.course_completion_rate ORDER BY user_id;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
-- Этап 3. Анализ и запросы - Напиши SQL-запросы к витринам
|
||||
-- Напиши свои вариации для практики--
|
||||
|
||||
|
||||
-- Топ-5 самых просматриваемых уроков
|
||||
|
||||
SELECT
|
||||
lesson_title,
|
||||
course_title,
|
||||
total_views
|
||||
FROM dsolovianov.lesson_popularity_summary
|
||||
ORDER BY total_views DESC
|
||||
LIMIT 5;
|
||||
|
||||
|
||||
-- Неактивные пользователи, записавшиеся на курсы
|
||||
|
||||
SELECT
|
||||
name,
|
||||
email,
|
||||
registered_courses_count
|
||||
FROM dsolovianov.inactive_users_summary
|
||||
WHERE registered_courses_count > 0
|
||||
ORDER BY registration_date DESC;
|
||||
|
||||
-- Курсы с самым высоким процентом завершения
|
||||
|
||||
SELECT
|
||||
course_title,
|
||||
AVG(completion_rate) AS avg_completion
|
||||
FROM dsolovianov.course_completion_rate
|
||||
GROUP BY course_title
|
||||
ORDER BY avg_completion DESC
|
||||
LIMIT 5;
|
||||
|
||||
-- Мои вариации --
|
||||
|
||||
-- Среднее количество учеников на урок по каждому курсу
|
||||
|
||||
SELECT
|
||||
course_title,
|
||||
ROUND(AVG(unique_users), 2) AS avg_users_lesson,
|
||||
SUM(total_views) AS total_course_views
|
||||
FROM dsolovianov.lesson_popularity_summary
|
||||
GROUP BY course_title
|
||||
ORDER BY avg_users_lesson DESC;
|
||||
|
||||
-- Возрастное распределение неактивных пользователей
|
||||
|
||||
SELECT
|
||||
CASE
|
||||
WHEN age < 25 THEN 'до 25'
|
||||
WHEN age BETWEEN 25 AND 30 THEN '25-30'
|
||||
WHEN age BETWEEN 31 AND 35 THEN '31-35'
|
||||
ELSE '35+'
|
||||
END AS age_group,
|
||||
COUNT(*) AS inactive_users_count,
|
||||
AVG(registered_courses_count) AS avg_courses_enrolled
|
||||
FROM dsolovianov.inactive_users_summary
|
||||
GROUP BY age_group
|
||||
ORDER BY age_group;
|
||||
|
||||
-- Пользователи, которые начали, но не закончили курс (completion_rate между 0 и 1)
|
||||
|
||||
SELECT
|
||||
user_name,
|
||||
course_title,
|
||||
lessons_viewed,
|
||||
lessons_in_course,
|
||||
ROUND(completion_rate * 100, 0) AS no_completion,
|
||||
(lessons_in_course - lessons_viewed) AS remaining_lessons
|
||||
FROM dsolovianov.course_completion_rate
|
||||
WHERE completion_rate BETWEEN 0.01 AND 0.99
|
||||
ORDER BY completion_rate DESC, user_name;
|
||||
|
||||
-- Прогресс каждого пользователя по курсам (с указанием статуса)
|
||||
|
||||
SELECT
|
||||
user_name,
|
||||
course_title,
|
||||
lessons_viewed,
|
||||
lessons_in_course,
|
||||
ROUND(completion_rate * 100, 0) AS progress_percent,
|
||||
CASE
|
||||
WHEN completion_rate = 1.0 THEN 'Пройден'
|
||||
WHEN completion_rate = 0 THEN 'Не начат'
|
||||
WHEN completion_rate >= 0.5 THEN 'В процессе (>50%)'
|
||||
ELSE 'В процессе (<50%)'
|
||||
END AS status
|
||||
FROM dsolovianov.course_completion_rate
|
||||
ORDER BY user_name, completion_rate DESC;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
CREATE TABLE dsolovianov.users (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
age INT,
|
||||
email VARCHAR(100),
|
||||
country VARCHAR(100),
|
||||
is_active BOOLEAN,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE dsolovianov.products (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
category VARCHAR(50),
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
in_stock BOOLEAN
|
||||
);
|
||||
|
||||
CREATE TABLE dsolovianov.orders (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id INT,
|
||||
order_date TIMESTAMPTZ NOT NULL,
|
||||
status VARCHAR(20),
|
||||
CONSTRAINT orders_user_fk
|
||||
FOREIGN KEY (user_id) REFERENCES dsolovianov.users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE dsolovianov.order_items (
|
||||
order_id INT NOT NULL,
|
||||
product_id INT NOT NULL,
|
||||
quantity INT NOT NULL CHECK (quantity > 0),
|
||||
price_at_purchase DECIMAL(10,2) NOT NULL CHECK (price_at_purchase > 0),
|
||||
CONSTRAINT order_items_pk PRIMARY KEY (order_id, product_id),
|
||||
CONSTRAINT order_items_order_fk
|
||||
FOREIGN KEY (order_id) REFERENCES dsolovianov.orders(id),
|
||||
CONSTRAINT order_items_product_fk
|
||||
FOREIGN KEY (product_id) REFERENCES dsolovianov.products(id)
|
||||
);
|
||||
@@ -0,0 +1,59 @@
|
||||
INSERT INTO dsolovianov.users (name, age, email, country, is_active, created_at) VALUES
|
||||
('Alice', 25, 'alice@example.com', 'US', true, '2024-01-10 10:00:00+00'),
|
||||
('Bob', 30, 'bob@example.com', 'UK', true, '2024-02-05 09:30:00+00'),
|
||||
('Carol', 27, 'carol@example.com', 'US', false, '2024-03-01 15:20:00+00'),
|
||||
('Diana', 22, 'diana@GMAIL.com', 'US', true, '2024-01-12 08:15:00+00'),
|
||||
('Evan', 35, 'EVAN@example.com', 'UK', true, '2024-01-18 19:40:00+00'),
|
||||
('Farida', 29, 'farida@example.com', 'KZ', true, '2024-02-20 11:05:00+00'),
|
||||
('George', 31, 'george+test@example.com', 'US', true, '2024-03-05 16:45:00+00'),
|
||||
('Helen', 24, NULL, 'UK', false, '2024-03-06 08:20:00+00'),
|
||||
('Ivan', 26, 'ivan@example.com', 'RU', true, '2024-01-11 14:30:00+00'),
|
||||
('Julia', 28, 'julia@example.com', 'US', true, '2024-03-07 18:05:00+00');
|
||||
|
||||
INSERT INTO dsolovianov.products (name, category, price, in_stock) VALUES
|
||||
('iPhone 15', 'Электроника', 80000, true),
|
||||
('Kindle', 'Электроника', 15000, true),
|
||||
('Кроссовки Nike', 'Одежда', 9000, true),
|
||||
('Кроссовки NoName', 'Одежда', 3000, false),
|
||||
('Наушники Pro', 'Электроника', 12000, true),
|
||||
('Фитнес-браслет', 'Гаджеты', 6000, true),
|
||||
('Подарочная карта', 'Сервисы', 1000, true);
|
||||
|
||||
INSERT INTO dsolovianov.orders (user_id, order_date, status) VALUES
|
||||
(1, '2025-01-10 12:00:00+00', 'paid'),
|
||||
(3, '2025-01-12 09:30:00+00', 'cancelled'),
|
||||
(1, '2025-01-15 18:20:00+00', 'paid'),
|
||||
(2, '2025-01-20 11:05:00+00', 'created'),
|
||||
(4, '2025-02-01 17:45:00+00', 'paid'),
|
||||
(5, '2025-02-10 10:10:00+00', 'refunded'),
|
||||
(7, '2025-03-01 09:00:00+00', 'paid'),
|
||||
(9, '2025-03-05 19:30:00+00', 'paid');
|
||||
|
||||
INSERT INTO dsolovianov.order_items (order_id, product_id, quantity, price_at_purchase) VALUES
|
||||
-- Заказ #1: iPhone 15 (1 шт) + Наушники Pro (2 шт)
|
||||
(1, 1, 1, 80000.00),
|
||||
(1, 5, 2, 12000.00),
|
||||
|
||||
-- Заказ #2 (cancelled): Kindle (1 шт)
|
||||
(2, 2, 1, 15000.00),
|
||||
|
||||
-- Заказ #3: Кроссовки Nike (2 шт)
|
||||
(3, 3, 2, 9000.00),
|
||||
|
||||
-- Заказ #4 (created): Фитнес-браслет (1 шт)
|
||||
(4, 6, 1, 6000.00),
|
||||
|
||||
-- Заказ #5: iPhone 15 (1 шт) + Подарочная карта (3 шт)
|
||||
(5, 1, 1, 80000.00),
|
||||
(5, 7, 3, 1000.00),
|
||||
|
||||
-- Заказ #6 (refunded): Наушники Pro (1 шт)
|
||||
(6, 5, 1, 12000.00),
|
||||
|
||||
-- Заказ #7: Kindle (1 шт) + Фитнес-браслет (2 шт)
|
||||
(7, 2, 1, 15000.00),
|
||||
(7, 6, 2, 6000.00),
|
||||
|
||||
-- Заказ #8: iPhone 15 (1 шт) + Кроссовки Nike (1 шт)
|
||||
(8, 1, 1, 80000.00),
|
||||
(8, 3, 1, 9000.00);
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
- Задание 1 — Спроектируй «карточку пользователя»
|
||||
|
||||
Ты делаешь экран “Пользователи” в админке и решаешь, какие поля там важны.
|
||||
|
||||
Работаем с таблицей **`users`**
|
||||
|
||||
**✅ Требования:**
|
||||
|
||||
1. Напиши запрос, который вернёт в результате только столбцы: **`id`**, **`name`**, **`country`**, **`created_at`**, **`email`**
|
||||
2. Потом измени запрос так, чтобы:
|
||||
- сначала отсортировать по **`created_at`** (новые сверху)
|
||||
- потом по **`name`** по алфавиту внутри одной даты
|
||||
|
||||
- Задание 2 — Сегментация по странам (с выбором)
|
||||
|
||||
Маркетолог хочет понять, какая страна сейчас интереснее.
|
||||
|
||||
Работаем с таблицей **`users`**
|
||||
|
||||
**✅ Требования:**
|
||||
|
||||
1. Выбери страну **`US`**и напиши запрос, который:
|
||||
- вернёт **`id`**, **`name`**, **`country`**;
|
||||
- покажет только пользователей из выбранной страны;
|
||||
- отсортирует их по имени.
|
||||
2. Напиши второй запрос, который вернёт пользователей из страны **`UK`**.
|
||||
|
||||
- Задание 3 — Контакты для рассылки (думать про качество)
|
||||
|
||||
Нужно сделать список e‑mail для рассылки так, чтобы его не стыдно было отдавать маркетологу.
|
||||
|
||||
**✅ Требования:**
|
||||
|
||||
1. Напиши один запрос, который:
|
||||
- вернёт только пользователей, у которых email **есть**;
|
||||
- вернёт столбцы **`id`**, **`name`**, **`email`**;
|
||||
- отсортирует их по имени.
|
||||
2. Напиши второй запрос, который подготовит «чистый» вид email:
|
||||
- вернёт только **`email`** в нижнем регистре (как **`email_lower`**);
|
||||
- уберёт дубликаты email (нужно использовать **`DISTINCT`**).
|
||||
- Задание 4 — Новички и активность
|
||||
|
||||
«Тебе нужно посмотреть, кто пришёл недавно и кого стоит “подогреть”.»
|
||||
|
||||
**✅ Требования:**
|
||||
|
||||
1. Напиши запрос, который:
|
||||
- выберет пользователей, зарегистрированных **после** даты, которую ты сам выберешь (например, **`2024‑01‑11`**);
|
||||
- вернёт **`id`**, **`name`**, **`created_at`**;
|
||||
- отсортирует по **`created_at`** от новых к старым.
|
||||
2. Ограничь результат так, чтобы увидеть только 3 самых новых пользователя.
|
||||
3. Измени запрос так, чтобы пропустить самого нового и показать следующих двух (используй **`LIMIT`** и **`OFFSET`**).
|
||||
*/
|
||||
SELECT id,
|
||||
name,
|
||||
country,
|
||||
created_at,
|
||||
email
|
||||
FROM dsolovianov.users
|
||||
ORDER BY created_at DESC, name ASC;
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
country
|
||||
FROM dsolovianov.users
|
||||
WHERE country LIKE 'US'
|
||||
ORDER BY name;
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
country
|
||||
FROM dsolovianov.users
|
||||
WHERE country LIKE 'UK'
|
||||
ORDER BY name;
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
email,
|
||||
LOWER(email) AS email_lower
|
||||
FROM dsolovianov.users
|
||||
WHERE email IS NOT NULL
|
||||
ORDER BY name;
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
created_at
|
||||
FROM dsolovianov.users
|
||||
WHERE created_at > '2024-01-11'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 3 OFFSET 1;
|
||||
@@ -0,0 +1,124 @@
|
||||
/*INSERT INTO dsolovianov.products (name, category, price, in_stock) VALUES
|
||||
('Mi Band 7', 'smartwatch', 49.00, true),
|
||||
('Surface Pro', 'tablet', 1299.00, true),
|
||||
('iPhone 15', 'smartphone', 999.00, true),
|
||||
('Logitech MX Master', 'mouse', 120.00, true);*/
|
||||
|
||||
/*
|
||||
- Задача 1 — Товары для продажи
|
||||
|
||||
Ты настраиваешь главную витрину магазина. Нужно показывать только товары, которые сейчас реально готовы продавать.
|
||||
|
||||
**✅ Условия:**
|
||||
|
||||
- товар **в наличии**
|
||||
- категория **НЕ smartwatch**
|
||||
- цена **от 100 до 1300**
|
||||
- отдельно хотим видеть цену:
|
||||
- с наценкой 30% ( price + 30%)
|
||||
- с «виртуальным НДС» 50% (price + 50%)
|
||||
|
||||
**✅ Вывести:**
|
||||
|
||||
- `name`, `category`, `price`
|
||||
- `price_with_markup` = price + 30%
|
||||
- `price_with_vat` = price + 50%
|
||||
|
||||
- Задача 2 — Скидочная корзина: дешёвые или дорогие
|
||||
|
||||
Нужно собрать товары под акцию
|
||||
|
||||
**✅ Условия:**
|
||||
|
||||
- участвуют товары с ценой **меньше 100** или **больше 1000**
|
||||
- скидка 15% (price - 15%)
|
||||
|
||||
**✅ Вывести:**
|
||||
|
||||
- `name`, `price`
|
||||
- `price_with_discount` = price - 15%
|
||||
|
||||
- Задача 3 — Категории для рекламы
|
||||
|
||||
Маркетолог просит товары только из нужных категорий:
|
||||
|
||||
**✅ Условия:**
|
||||
|
||||
- category в списке: `tablet`, `smartphone`, `mouse`
|
||||
- цена **не меньше 100**
|
||||
|
||||
**✅ Вывести:**
|
||||
|
||||
- `id`, `name`, `category`, `price`
|
||||
- сортировка по цене по убыванию
|
||||
- Задача 4 — Убрать iPhone из выборки
|
||||
|
||||
Собираем список товаров, **кроме iPhone**, чтобы сделать подборку “не Apple”.
|
||||
|
||||
**✅ Условия:**
|
||||
|
||||
- название **НЕ содержит** `iPhone`
|
||||
|
||||
**✅ Вывести:**
|
||||
|
||||
- `name`, `category`, `price`
|
||||
- Задача 5 — Поиск товаров по названию
|
||||
|
||||
Представь, что у пользователя есть строка поиска на сайте. Нужно сделать **2 запроса**, которые помогут искать товары по названию.
|
||||
|
||||
- 1) Найти товары, где в названии есть `"pro"` (без учёта регистра)
|
||||
|
||||
📌 Условия:
|
||||
|
||||
- поиск должен сработать на `pro`, `PRO`, `Pro`
|
||||
- вывести: `name`, `category`, `price`
|
||||
- 2) Найти товары, где в названии есть `"mx"` (без учёта регистра)
|
||||
|
||||
📌 Условия:
|
||||
|
||||
- вывести: `name`, `category`, `price`
|
||||
- отсортировать по `price DESC`
|
||||
*/
|
||||
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price,
|
||||
(price * 0.30) + price AS price_with_markup,
|
||||
(price * 0.50) + price AS price_with_vat
|
||||
FROM dsolovianov.products
|
||||
WHERE category NOT IN ('smartwatch')
|
||||
AND (price >= 100 AND price <= 1300);
|
||||
|
||||
SELECT name,
|
||||
price,
|
||||
price - (price * 0.15) AS price_with_discount
|
||||
FROM dsolovianov.products
|
||||
WHERE price < 100 OR price > 1000;
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
category,
|
||||
price
|
||||
FROM dsolovianov.products
|
||||
WHERE category IN ('tablet', 'smartphone', 'mouse') AND price >= 100
|
||||
ORDER BY price DESC;
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price
|
||||
FROM dsolovianov.products
|
||||
WHERE category NOT LIKE '%iPhone%';
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price
|
||||
FROM dsolovianov.products
|
||||
WHERE name ILIKE '%pro%';
|
||||
|
||||
SELECT name,
|
||||
category,
|
||||
price
|
||||
FROM dsolovianov.products
|
||||
WHERE name ILIKE '%mx%';
|
||||
|
||||
@@ -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