Merge branch 'main' into vberetar_sql_cte

This commit is contained in:
2026-05-28 20:06:21 +04:00
55 changed files with 1888 additions and 9 deletions
@@ -0,0 +1,117 @@
--Типы данных
--Задача 1
CREATE TABLE raw_data
ON CLUSTER clickhouse_cluster
(
user_id String,
balance String,
created_at String
) ENGINE = MergeTree ORDER BY tuple();
SELECT DISTINCT
toDecimal64OrNull(balance, 2) AS balance_decimal,
toDateTime64OrNull(created_at, 3) AS created_at_date
FROM raw_data
WHERE
toDecimal64OrNull(balance, 2) IS NOT NULL
AND toDateTime64OrNull(created_at, 3) IS NOT NULL
/*
CAST(balance AS Decimal(10, 2)) - этом случае будет ошибка, так как данный метод не поддеживает NULL
Следующие два метода по сути своей преобразование типа данных с условием ИЛИ, то есть:
toDecimal64OrNull(balance, 2) - в этом методе на выходе будут корректные данные, в противном случае NULL
toDecimal64OrZero(balance, 2) - аналогично с NULL, только будет ноль.
*/
--Задача 2
CREATE TABLE events
ON CLUSTER clickhouse_cluster
(
event_id String,
event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid"
event_time String -- форматы: "12:30:45", "25:99:99"
)
ENGINE = MergeTree
ORDER BY event_id;
--Заполнил данными для проверки
INSERT INTO events (event_id, event_date, event_time) VALUES
('1', '2023-01-01', '12:30:45'),
('2', '01.02.2023', '08:15:00'),
('3', 'invalid', '25:99:99'),
('4', '2023-05-20', 'invalid'),
('5', '31.12.2023', '23:59:59');
SELECT * FROM events;
SELECT DISTINCT
toDate(DateTime_Valid) AS Event_Date,
toHour(DateTime_Valid) AS Event_Hour,
DateTime_Valid
FROM (
SELECT
parseDateTimeBestEffortOrNull(concat(event_date, ' ', event_time)) AS DateTime_Valid
FROM events
)
WHERE DateTime_Valid IS NOT NULL;
--Задача 3
DROP TABLE IF EXISTS user_activity ON CLUSTER clickhouse_cluster;
CREATE TABLE user_activity
ON CLUSTER clickhouse_cluster
(
user_id UInt32,
actions String -- JSON: ["login", "purchase", "logout"]
) ENGINE = MergeTree ORDER BY tuple();
--Тест данные
INSERT INTO user_activity (user_id, actions) VALUES
(1, '["login", "purchase", "logout"]'),
(2, '["login", "logout"]'),
(3, '["login"]'),
(4, '["login","purchase"]');
SELECT
user_id,
-- Сначала реобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Потом считваем длину массива и дальше суммируем длину всех массивов действий для каждого пользователя
sum(length(JSONExtract(actions, 'Array(String)'))) AS count_actions
FROM user_activity
WHERE
-- Проверяем наличие 'purchase' в массиве
has(JSONExtract(actions, 'Array(String)'), 'purchase')
GROUP BY
user_id;
/*
Преобразовал записи из набора "каких-то" букв (String) в массив строк Array(String). Нашел пользователей в новом массиве Array(String), совершивших действие "purchase".
Просто написать actions = 'purchase' не получится, так как изначально actions просто (String) и соответсвенно "["login", "purchase", "logout"]" нельзя сравнить с "purchase". Поэтому сначала преобразовываем в массив, а потом с функцией has(array, elem) ищем то, что нужно.
То есть менять бы не пришлось, если бы в actions было изначально разные значения в каждой строке ("login", "purchase" или "logout")
*/
--Задача 4
DROP TABLE IF EXISTS sensor_data ON CLUSTER clickhouse_cluster;
CREATE TABLE sensor_data
ON CLUSTER clickhouse_cluster
(
sensor_id UInt32,
value Float64,
status Enum('ok' = 1, 'error' = 2)
) ORDER BY sensor_id;
--Тест данные
INSERT INTO sensor_data (sensor_id, value, status) VALUES
(1, 25.5, 'ok'),
(2, 120.75, 'error'),
(3, 15.0, 1),
(4, 9999999.99, 'ok'),
(5, 0.001, 'ok');
SELECT
toString(status) AS status_string,
avg(value) AS avg_value --Среднее значение value для каждого status
FROM sensor_data
WHERE accurateCastOrNull(value, 'Decimal(10,2)') IS NOT NULL --Фильтруем строки, где value нельзя преобразовать в Decimal(10,2)
GROUP BY status_string;
@@ -0,0 +1,70 @@
-- Задачи на отработку Запросы - JOIN --
DROP TABLE IF EXISTS dsolovianov.products ON CLUSTER clickhouse_cluster SYNC;
DROP TABLE IF EXISTS dsolovianov.prices ON CLUSTER clickhouse_cluster SYNC;
CREATE TABLE dsolovianov.products
ON CLUSTER clickhouse_cluster
(
product_id UInt32,
name String
) ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/products',
'{replica}'
)
ORDER BY product_id;
INSERT INTO dsolovianov.products VALUES
(101, 'iPhone 14'),
(102, 'Galaxy S22'),
(103, 'Pixel 7');
CREATE TABLE dsolovianov.prices
ON CLUSTER clickhouse_cluster
(
product_id UInt32,
price UInt32,
region String
) ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/prices',
'{replica}'
)
ORDER BY product_id;
INSERT INTO dsolovianov.prices VALUES
(101, 999, 'US'),
(101, 1049, 'EU'),
(102, 850, 'US'),
(102, 899, 'CA'),
(103, 799, 'JP');
--№1 Сделай ANY LEFT JOIN, чтобы получить цену любой из регионов.
SELECT
p.product_id,
p.name,
pr.price,
pr.region
FROM dsolovianov.products AS p
ANY LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id;
--№2 Что будет, если в таблице `prices` несколько цен на один `product_id`?
/* Для одного product_id будет выбрана только первая ЛЮБАЯ (ANY) подходящая в поиске.*/
--№3 Как повлияет `ALL LEFT JOIN`? Почему в ClickHouse его избегают?
SELECT
p.product_id,
p.name,
pr.price,
pr.region
FROM dsolovianov.products AS p
ALL LEFT JOIN dsolovianov.prices AS pr ON p.product_id = pr.product_id;
/* `ALL LEFT JOIN` в Clickhouse ведет себя также, как LEFT JOIN в SQL, то есть собирает все совпадения.
Начинается перегон данных между таблицами. Из-за этого растет нагрузка на ОЗУ и CPU.
короче, по сути ALL LEFT JOIN создает декартово произведение*/
@@ -0,0 +1,244 @@
/* Запросы - Синтаксис*/
--! Задача 1 !--
DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC;
CREATE TABLE users
ON CLUSTER clickhouse_cluster
(
user_id UInt32,
name String,
age UInt8,
country String,
is_active Bool
) ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/users_v2',
'{replica}'
)
ORDER BY user_id;
INSERT INTO users VALUES
(1, 'Ivan', 30, 'RU', 1),
(2, 'Anna', 25, 'RU', 1),
(3, 'Petr', 45, 'RU', 0),
(4, 'Olga', 35, 'RU', 1),
(5, 'John', 55, 'US', 1),
(6, 'Alice', 42, 'US', 0),
(7, 'Tom', 38, 'US', 1),
(8, 'Lucas', 28, 'BR', 1),
(9, 'Maria', 60, 'BR', 1),
(10, 'João', 27, 'BR', 0),
(11, 'Kenji', 33, 'JP', 1),
(12, 'Yuki', 29, 'JP', 0),
(13, 'Chen', 40, 'CN', 1),
(14, 'Li', 48, 'CN', 1),
(15, 'Fatima', 31, 'AE', 1),
(16, 'Ahmed', 36, 'AE', 1),
(17, 'George', 24, 'US', 1),
(18, 'Nina', 34, 'RU', 1),
(19, 'Max', 50, 'DE', 1),
(20, 'Sophie', 22, 'DE', 0);
--Получи 10 самых старших активных пользователей.
SELECT
user_id,
name,
age,
is_active
FROM users
WHERE is_active = true
ORDER BY age DESC
LIMIT 10;
--Выведи пользователей из country = 'RU', отсортированных по имени.
SELECT
user_id,
name,
country
FROM users
WHERE country = 'RU'
ORDER BY name;
--Что произойдёт, если написать LIMIT 10 без ORDER BY? Просто выведет 10 случайных активных прользователей без какой либо сортировки.
SELECT
user_id,
name,
age
FROM users
WHERE is_active = true
LIMIT 10;
--Посчитай количество пользователей по странам.
SELECT
country,
COUNT(*) as user_country
FROM users
GROUP BY country;
SELECT
country,
uniqExact(country) as unique_countries --возвращает точное количество уникальных значений в столбце country
FROM users
GROUP BY country;
--Для каждой страны выведи минимальный и максимальный возраст.
SELECT
country,
MIN(age) as min_age,
MAX(age) as max_age
FROM users
GROUP BY country
ORDER BY country;
--Отсортируй страны по убыванию количества пользователей.
SELECT
country,
COUNT(*) as user_country
FROM users
GROUP BY country
ORDER BY user_country DESC;
--Всё вместе
SELECT
country,
COUNT(*) as user_country,
SUM(is_active) as active_users,
MIN(age) as min_age,
MAX(age) as max_age,
uniqExact(country) as unique_countries
FROM users
GROUP BY country
ORDER BY country;
--! Задача 2 !--
DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC;
CREATE TABLE dsolovianov.events ON CLUSTER clickhouse_cluster
(
user_id UInt32,
event_time DateTime,
event_type String
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/events_v2',
'{replica}'
)
ORDER BY (user_id, event_time);
INSERT INTO dsolovianov.events (user_id, event_time, event_type) VALUES
(1, '2025-07-17 09:00:00', 'click'),
(1, '2025-07-17 09:05:00', 'view'),
(1, '2025-07-17 09:10:00', 'purchase'),
(2, '2025-07-17 10:00:00', 'click'),
(2, '2025-07-17 10:05:00', 'click'),
(2, '2025-07-17 11:00:00', 'view'),
(3, '2025-07-16 12:00:00', 'view'),
(3, '2025-07-16 12:30:00', 'click'),
(3, '2025-07-17 08:00:00', 'purchase'),
(4, '2025-07-17 13:00:00', 'click'),
(4, '2025-07-17 14:00:00', 'view'),
(4, '2025-07-17 15:00:00', 'view'),
(5, '2025-07-16 20:00:00', 'view'),
(5, '2025-07-17 01:00:00', 'click'),
(5, '2025-07-17 06:00:00', 'purchase'),
(6, '2025-07-17 07:30:00', 'view'),
(6, '2025-07-17 08:45:00', 'click'),
(7, '2025-07-16 23:59:59', 'click'),
(7, '2025-07-17 00:01:00', 'purchase'),
(7, '2025-07-17 10:10:00', 'view');
--1. Добавь колонку с кумулятивным количеством событий по `user_id` (используй `count() OVER ...`).
SELECT
user_id,
event_time,
event_type,
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time) AS cumulative_events
FROM dsolovianov.events
ORDER BY user_id, event_time;
--2. Сделай аналог ранжирования событий внутри пользователя: `row_number() OVER (PARTITION BY user_id ORDER BY event_time)`
SELECT
user_id,
event_time,
event_type,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time) AS event_rank
FROM dsolovianov.events
ORDER BY user_id, event_rank;
--3. Что будет, если ты явно задашь `ROWS BETWEEN`? Поддерживается ли?
--Да, `ROWS BETWEEN` поддерживается:
SELECT
user_id,
event_time,
event_type,
-- кумулятивное окно (все предыдущие строки)
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_events,
-- скользящее окно: текущая + 2 предыдущих события
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS last_3_events,
-- окно: предыдущая + текущая + следующая
COUNT(*) OVER (PARTITION BY user_id ORDER BY event_time
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS prev_curr_next
FROM dsolovianov.events
ORDER BY user_id, event_time;
--4. Сделай `WITH recent_events AS (...)` — все события за последние 24 часа.
WITH recent_events AS (
SELECT *
FROM dsolovianov.events
WHERE event_time >= '2025-07-16 15:00:00'
)
SELECT * FROM recent_events
ORDER BY event_time DESC;
--5. Используй CTE, чтобы посчитать топ-5 пользователей по числу этих событий.
WITH recent_events AS (
SELECT *
FROM dsolovianov.events
WHERE event_time >= '2025-07-16 15:00:00'
)
SELECT
user_id,
COUNT(*) AS event_count
FROM recent_events
GROUP BY user_id
ORDER BY event_count DESC
LIMIT 5;
--6. Добавь второй `WITH` — для маппинга `event_type → category` и используй его в основном запросе.
WITH
event_categories AS (
SELECT
event_type,
CASE
WHEN event_type = 'click' THEN 'engagement'
WHEN event_type = 'view' THEN 'engagement'
WHEN event_type = 'purchase' THEN 'conversion'
ELSE 'other'
END AS category
FROM (SELECT DISTINCT event_type FROM dsolovianov.events)
),
recent_events AS (
SELECT *
FROM dsolovianov.events
WHERE event_time >= '2025-07-16 15:00:00'
)
SELECT
re.user_id,
re.event_time,
re.event_type,
ec.category,
ROW_NUMBER() OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS event_rank,
COUNT(*) OVER (PARTITION BY re.user_id ORDER BY re.event_time) AS cumulative_events
FROM recent_events re
LEFT JOIN event_categories ec ON re.event_type = ec.event_type
ORDER BY re.user_id, re.event_time;
/*
Вопрос: поддерживает ли ClickHouse многоуровневые или рекурсивные CTE?
многоуровневые CTE - да
рекурсивные CTE - нет в классическом понимании, а скорее в итеративном смысле.
*/
@@ -0,0 +1,166 @@
-- Индексы и сжатие - Индексы --
-- #1
/*
3. Посмотри, сколько блоков было пропущено (в `system.query_log`, `rows_read`, `rows`).
Не посмотрю, так как нет доступа system.query_log, rows_read, rows (ACCESS_DENIED)
4. Что будет, если `ORDER BY log_id`, а не `ts`?
ORDER BY log_id → данные физически отсортированы по log_id. Значения ts расположены хаотично относительно физической сортировки.
Индекс minmax по ts будет почти бесполезен. Minmax индекс будет иметь широкие диапазоны
*/
DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC;
CREATE TABLE dsolovianov.logs
ON CLUSTER clickhouse_cluster
(
log_id UInt64,
ts DateTime
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/logs',
'{replica}'
)
ORDER BY ts
PARTITION BY toYYYYMM(ts)
PRIMARY KEY (ts);
INSERT INTO dsolovianov.logs (log_id, ts) VALUES
(1, '2024-06-01 10:00:00'),
(2, '2024-06-01 11:00:00'),
(3, '2024-06-02 12:00:00'),
(4, '2024-06-03 13:00:00'),
(5, '2024-06-04 14:00:00');
-- 1. Добавляем minmax индекс:
ALTER TABLE dsolovianov.logs ADD INDEX idx_ts ts TYPE minmax GRANULARITY 2;
-- 2. Сделай запрос: `SELECT * FROM logs WHERE ts BETWEEN '2024-06-02' AND '2024-06-03'`
SELECT *
FROM dsolovianov.logs
WHERE ts
BETWEEN '2024-06-02' AND '2024-06-03';
-- Раз нет доступа, но можно предположить, что вот такой запрос сработает:
SELECT query, read_rows, result_rows
FROM system.query_log
WHERE query LIKE '%logs%'
ORDER BY event_time DESC
LIMIT 1;
-- #2
/*
1. Создай таблицу с `ORDER BY id`, добавь индекс:
```sql
ALTER TABLE clicks ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2;
```
3. Посмотри `EXPLAIN` — индекс используется?
Данные отсортированы по id, а не по country. Значения country распределены хаотически относительно физического порядка.
4. Как будет вести себя индекс, если `country` имеет 10M уникальных значений?
Будет создано 10M партиций, что убьёт производительность.
**/
DROP TABLE IF EXISTS dsolovianov.clicks ON CLUSTER clickhouse_cluster SYNC;
CREATE TABLE dsolovianov.clicks
ON CLUSTER clickhouse_cluster
(
id UInt64,
country String
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/clicks',
'{replica}'
)
ORDER BY id
PARTITION BY country
PRIMARY KEY (id);
INSERT INTO dsolovianov.clicks (id, country) VALUES
(1, 'RU'),
(2, 'US'),
(3, 'RU'),
(4, 'DE'),
(5, 'RU');
ALTER TABLE dsolovianov.clicks ON CLUSTER clickhouse_cluster
ADD INDEX idx_country country TYPE set(1000) GRANULARITY 2;
-- 2. Выполни запрос `SELECT * FROM clicks WHERE country = 'RU'`.
EXPLAIN indexes = 1
SELECT * FROM dsolovianov.clicks WHERE country = 'RU';
EXPLAIN
SELECT * FROM dsolovianov.clicks WHERE country = 'RU';
/*
Expression ((Project names + Projection))
Expression ((WHERE + Change column names to column identifiers))
ReadFromMergeTree (dsolovianov.clicks)
Indexes:
MinMax
Keys:
country
Condition: (country in ['RU', 'RU'])
Parts: 1/3
Granules: 1/3
Partition
Keys:
country
Condition: (country in ['RU', 'RU'])
Parts: 1/1
Granules: 1/1
PrimaryKey
Condition: true
Parts: 1/1
Granules: 1/1
Skip
Name: idx_country
Description: set GRANULARITY 2
Parts: 1/1
Granules: 1/1
Ranges: 1
*/
-- #3
DROP TABLE IF EXISTS dsolovianov.users ON CLUSTER clickhouse_cluster SYNC;
CREATE TABLE dsolovianov.users
ON CLUSTER clickhouse_cluster
(
id UInt64,
email String
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/users',
'{replica}'
)
ORDER BY id
PRIMARY KEY (id);
INSERT INTO dsolovianov.users (id, email) VALUES
(1, 'alice@mail.com'),
(2, 'bob@mail.com'),
(3, 'charlie@mail.com'),
(4, 'dave@mail.com'),
(5, 'eve@example.com');
ALTER TABLE dsolovianov.users ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2;
EXPLAIN indexes = 1
SELECT * FROM users WHERE email = 'alice@mail.com';
-- Bloom filter может пропустить некоторые гранулы, но не все, ибо данные не отсортированы по email и индексные блоки покрывают случайные email'ы
EXPLAIN indexes = 1
SELECT * FROM users WHERE email LIKE '%mail%';
EXPLAIN indexes = 1
SELECT * FROM users WHERE email LIKE 'alice%';
/*
Bloom filter не поддерживает частичные совпадения (LIKE '..%', LIKE '%..', LIKE '%..%'). Из-за этого ClickHouse не может пропустить гранулы.
Клику становится сложно вычислить хэш для проверки, если подходящих значений по LIKE будет больше одного. Нет гарантии, что данные на выходе будут корректные.
*/
@@ -0,0 +1,146 @@
-- Индексы и сжатие - Партиции --
-- #1
/*
1. Создай таблицу sales с `PARTITION BY toYYYYMM(sale_date)`.
2. Выполни `ALTER TABLE ... DROP PARTITION '202405'`.
3. Что произойдет, если не указывать `PARTITION BY`? <- удалит все данные из таблицы. партиция то одна :)
*/
DROP TABLE IF EXISTS dsolovianov.sales ON CLUSTER clickhouse_cluster SYNC;
CREATE TABLE dsolovianov.sales
ON CLUSTER clickhouse_cluster
(
id UInt64,
region String,
sale_date Date,
amount Float64
) ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/sales',
'{replica}'
)
PARTITION BY toYYYYMM(sale_date)
ORDER BY (region, sale_date);
INSERT INTO dsolovianov.sales (id, region, sale_date, amount) VALUES
(1, 'RU', '2024-05-01', '100.0'),
(2, 'RU', '2024-05-15', '120.0'),
(3, 'US', '2024-06-01', '90.0'),
(4, 'US', '2024-07-03', '150.0'),
(5, 'DE', '2024-07-04', '160.0');
ALTER TABLE dsolovianov.sales DROP PARTITION '202405';
SELECT *
FROM dsolovianov.sales;
-- #2
/*
1. Создай таблицу с `PARTITION BY event_type`.
2. Удали все события `click` (подсказка: это `ALTER TABLE DROP PARTITION 'click'`).
3. Чем такой подход может быть опасен (подумай про кардинальность значений)?
При большом количестве уникальных event_type создастся много мелких партиций.
*/
DROP TABLE IF EXISTS dsolovianov.events ON CLUSTER clickhouse_cluster SYNC;
CREATE TABLE dsolovianov.events
ON CLUSTER clickhouse_cluster
(
id UInt64,
event_type String,
event_date Date
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/events',
'{replica}'
)
PARTITION BY event_type
ORDER BY (event_date, id);
INSERT INTO dsolovianov.events (id, event_type, event_date) VALUES
(1, 'login', '2024-06-01'),
(2, 'click', '2024-06-01'),
(3, 'click', '2024-06-02'),
(4, 'purchase', '2024-06-03'),
(5, 'login', '2024-06-04');
ALTER TABLE dsolovianov.events DROP PARTITION 'click';
-- #3
/*
1. Создай таблицу с `PARTITION BY cityHash64(user_id) % 100`.
2. Что ты выиграешь с точки зрения баланса?
Данные распределятся примерно поровну между 100 партициями. Упрощается управление удалением старых данных (можно дропать целые партиции)
3. Почему такой тип `PARTITION` не ускорит `SELECT * WHERE user_id = 456`?
Потому что партиции определяются хэшем, а не прямым значением user_id и к тому же сортировка в партициях по log_time, а не user_id.
*/
DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC;
CREATE TABLE dsolovianov.logs
ON CLUSTER clickhouse_cluster
(
id UInt64,
user_id UInt64,
log_time DateTime
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/logs',
'{replica}'
)
PARTITION BY cityHash64(user_id) % 100 -- 100 партиций по хэшу user_id
ORDER BY (log_time, id);
INSERT INTO dsolovianov.logs (id, user_id, log_time) VALUES
(1, 123, '2024-07-17 10:00:00'),
(2, 456, '2024-07-17 10:01:00'),
(3, 789, '2024-07-17 10:02:00');
SELECT
id,
user_id,
cityHash64(user_id) % 100 AS partition_number
FROM dsolovianov.logs
ORDER BY id;
SELECT * FROM logs WHERE user_id = 456;
-- #4
/*
1. Создай таблицу с `PARTITION BY (region, toYYYYMM(visit_date))`.
2. Как будет выглядеть имя партиции?
Имена партиций будут в формате: ('значение_region', 'YYYYMM'). Пример: ('RU', '202406')
3. Сможешь ли ты эффективно удалить **все июльские данные по `RU`**? Да. Удаляется целая партиция без сканирования и нагрузки на систему.
*/
DROP TABLE IF EXISTS dsolovianov.visits ON CLUSTER clickhouse_cluster SYNC;
CREATE TABLE dsolovianov.visits
ON CLUSTER clickhouse_cluster
(
id UInt64,
region String,
visit_date Date
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/visits',
'{replica}'
)
PARTITION BY (region, toYYYYMM(visit_date))
ORDER BY (region, visit_date, id);
INSERT INTO dsolovianov.visits (id, region, visit_date) VALUES
(1, 'RU', '2024-06-01'),
(2, 'RU', '2024-06-15'),
(3, 'US', '2024-06-03'),
(4, 'US', '2024-07-01'),
(5, 'RU', '2024-07-05');
ALTER TABLE dsolovianov.visits ON CLUSTER clickhouse_cluster
DROP PARTITION ('RU', '202407');
SELECT * FROM dsolovianov.visits;
@@ -0,0 +1,39 @@
-- Индексы и сжатие - Сжатие --
-- #1 Создай таблицу logs с колонкой message CODEC(ZSTD(3)).
-- #2 Добавь created_at CODEC(Delta, ZSTD) — это улучшит хранение по времени.
DROP TABLE IF EXISTS dsolovianov.logs ON CLUSTER clickhouse_cluster SYNC;
CREATE TABLE dsolovianov.logs
ON CLUSTER clickhouse_cluster
(
id UInt64,
level String,
message String CODEC(ZSTD(3)),
created_at DateTime CODEC(Delta, ZSTD)
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/dsolovianov/logs',
'{replica}'
)
ORDER BY (created_at, id)
PARTITION BY toYYYYMM(created_at);
INSERT INTO dsolovianov.logs (id, level, message, created_at) VALUES
(1, 'INFO', 'User logged in', '2024-07-01 10:00:00'),
(2, 'WARN', 'Request took too long', '2024-07-01 10:05:00'),
(3, 'ERROR', 'Unable to connect to server', '2024-07-01 10:10:00');
-- #3 Посмотри статистику сжатия:
SELECT
column,
compression_codec
FROM system.columns
WHERE table = 'logs';
-- Стата вот так выглядит:
column | compression_codec
id
level
message CODEC(ZSTD(3))
created_at CODEC(Delta(4), ZSTD(1))

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 175 KiB

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