Merge branch 'sql_homework' of https://gitea.de-infra.servehttp.com/de-infrastructure/DWH into sql_homework

This commit is contained in:
Alex
2026-02-07 14:20:16 +03:00
68 changed files with 1512 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

@@ -0,0 +1,53 @@
/*Задача 1: Какой будет порядок выполнения у запросов?
SELECT name, age
FROM users
WHERE is_active = true AND country = 'Canada'
ORDER BY age
LIMIT 3;
*/
1. FROM users
2. WHERE is_active = true AND country = 'Canada'
3. SELECT name, age
4. ORDER BY age
5. LIMIT 3
/*
SELECT country, COUNT(*) AS active_users
FROM users
WHERE is_active = true
GROUP BY country
HAVING COUNT(*) > 1;
*/
1. FROM users
2. WHERE is_active = true
3. GROUP BY country
4. HAVING COUNT(*) > 1
5. SELECT country, COUNT(*) AS active_users
/*
SELECT country, AVG(age) AS avg_age
FROM users
GROUP BY country
HAVING COUNT(*) >= 3 AND AVG(age) > 25;
*/
1. FROM users
2. GROUP BY country
3. HAVING COUNT(*) >= 3 AND AVG(age) > 25
4. SELECT country, AVG(age) AS avg_age
/*
SELECT name, age
FROM users
WHERE is_active = false AND age > 30
ORDER BY name;
*/
1. FROM users
2. WHERE is_active = false AND age > 30
3. SELECT name, age
4. ORDER BY name
Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

@@ -0,0 +1,104 @@
-- Создание и заполнение таблицы
create table products_extended(
id INT primary key,
name TEXT,
category TEXT,
price DECIMAL,
in_stock BOOLEAN,
email_support TEXT
);
INSERT INTO products_extended (id, name, category, price, in_stock, email_support) VALUES
(1, 'iPhone 14', 'smartphone', 799, TRUE, 'iphone@apple.com'),
(2, 'MacBook Air', 'laptop', 1199, FALSE, NULL),
(3, 'Galaxy S22', 'smartphone', 699, TRUE, 'galaxy@support.com'),
(4, 'ThinkPad X1', 'laptop', 1399, TRUE, 'thinkpad@lenovo.com'),
(5, 'iPad Pro', 'tablet', 999, FALSE, 'ipad@apple.com'),
(6, 'Kindle Paperwhite', 'ebook_reader', 149, TRUE, NULL),
(7, 'Mi Band 7', 'smartwatch', 49, TRUE, 'miband@xiaomi.com'),
(8, 'Surface Pro', 'tablet', 1299, TRUE, 'surface@microsoft.com'),
(9, 'Honor Pad X9', 'tablet', 219, TRUE, 'honor@support.com'),
(10, 'Dell XPS 13', 'laptop', 1099, FALSE, 'dell@support.com');
/*
Задача 1:
Выведи все товары, которые:
- в наличии
- НЕ относятся к `smartwatch` и `ebook_reader`
- стоят от 300 до 1300
И для них посчитай:
1. price с наценкой 10%
2. price с НДС 20%
3. Статус: если email_support IS NULL, подставь нет почты, иначе — сам email
*/
select name, category, price, price * 1.1 as price_with_markup, price * 1.2 as price_with_vat, coalesce(email_support, 'нет почты') as status
from products_extended
where in_stock = true and category not in ('smartwatch', 'ebookreader') and price between 300 and 1300;
/*
Задача 2:
Найти все товары:
1. У которых цена меньше 150 или больше 1200
2. У которых** есть email поддержки
Для каждого выведи:
1. Название
2. Категорию
3. Email
4. Цену с прибавкой 25%
5. Отметку `premium` — если цена > 1000
*/
select name, category, email_support, price * 1.25 as price_25, (price> 1000) as premium
from products_extended
where (price < 150 or price > 1200) and email_support is not null
order by premium;
/*
Задача 3:
Найди все товары:
- где категория — `smartphone`, `laptop`, `tablet`
- название НЕ содержит `'i'` (чтобы исключить Apple)
- цена от 500 до 1300
Выведи:
- `id`, `name`, `category`, `price`
- отсортируй по `id`
*/
select id, name, category, price
from products_extended
where category in('smartphone', 'laptop', 'tablet') and name not like '%i%' and price between 500 and 1300
order by id desc;
/*
Задача 4:
Найди товары, у которых:
- email_support IS NULL
- они в наличии
- стоят менее 800
Выведи:
- `name`, `category`, `price`
- добавь колонку `comment`: `'нет поддержки'`
*/
select name, category, price, coalesce (email_support, 'нет поддержки') as comment
from products_extended
where email_support is null and in_stock is not null and price < 800;
/*
Задача 5:
Найди товары, у которых:
- название заканчивается на `'Pro'` ИЛИ содержит `'X'` внутри (например, `X1`, `XPS`)
- в наличии
- с ценой > 700
Выведи:
- `name`, `category`, `price`
- флаг `premium` — `true`, если `price > 1000`, иначе `false`
*/
select name, category, price, (price>1000) as premium
from products_extended
where (name like '%Pro%' or name like '%X%') and in_stock is true and price > 700
order by price desc;
Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

+1
View File
@@ -0,0 +1 @@
CREATE TABLE test_db;#
Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

@@ -0,0 +1,86 @@
/*
Задача 1:
Для каждой категории показать 2 строки:
- Самый дорогой товар с email'ом
- Самый дорогой товар без email'а
*/
with with_mail as (
select category, name, price, email_support
from (
select category, name, price, email_support,
row_number() over (partition by category order by price desc) as rn
from products_extended
where email_support is not null
) t
where rn = 1
),
without_mail as (
select category, name, price, email_support
from (
select category, name, price, email_support,
row_number() over (partition by category order by price desc) as rn
from products_extended
where email_support is null
) t
where rn = 1
)
select * from with_mail
union all
select * from without_mail
order by category;
/*
Задача 2:
Для каждого товара показать:
- `name` — название товара
- `category` — категория товара
- `price` — цена текущего товара
- `next_price` — цена следующего товара внутри той же категории, упорядоченного по цене
- `diff` — разница между ценой текущего товара и ценой следующего товара
(`price - next_price`)
*/
with t as(
select name, category, price,
lead(price) over (partition by category order by price) as next_price
from products_extended)
select name, category, price, next_price,
price - next_price as diff
from t order by category;
/*
Задача 3:
Показать `category`, `name`, `price`, `cumulative_sales`
Где `cumulative_sales` — это сумма `price` **внутри категории** по мере возрастания цены
*/
select category, name, price,
sum(price) over (partition by category order by price) as cumulative_sales
from products_extended;
/*
Задача 4:
Вывести `category`, `name`, `price`, `avg_price`, `is_above_avg`
Где `avg_price` — средняя цена по категории
`is_above_avg = true`, если `price > avg_price`
*/
with t as(
select category, name, price,
round(avg(price) over (partition by category), 1) as avg_price
from products_extended)
select category, name, price, avg_price,
price > avg_price as is_above_avg
from t order by category;
+20
View File
@@ -0,0 +1,20 @@
--Задача 1068
SELECT p.product_name, s.year, s.price
FROM Sales s
JOIN Product p
ON s.product_id = p.product_id;
--Задача 1378
SELECT EmployeeUNI.unique_id,
Employees.name
FROM Employees
LEFT JOIN EmployeeUNI
ON Employees.id = EmployeeUNI.id;
--Задача 577
SELECT e.name, b.bonus
FROM Employee e
LEFT JOIN Bonus b
ON e.empId = b.empId
WHERE b.bonus < 1000
OR b.bonus IS NULL;
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

@@ -0,0 +1,233 @@
CREATE TABLE users (
id INT PRIMARY KEY,
name TEXT,
age INT,
email TEXT,
registration_date DATE
);
INSERT INTO users VALUES
(1, 'Alice', 25, 'alice@mail.com', '2023-01-10'),
(2, 'Bob', 30, 'bob@gmail.com', '2023-02-05'),
(3, 'Charlie', 22, 'charlie@mail.com', '2023-02-20'),
(4, 'Diana', 28, 'diana@mail.com', '2023-03-01'),
(5, 'Ethan', 35, 'ethan@gmail.com', '2023-03-10');
SELECT * FROM users ORDER BY id;
CREATE TABLE courses (
id INT PRIMARY KEY,
title TEXT,
category TEXT,
created_at DATE
);
INSERT INTO courses VALUES
(1, 'SQL для начинающих', 'data', '2023-01-01'),
(2, 'Python для анализа данных', 'programming', '2023-01-15'),
(3, 'BI с нуля', 'business', '2023-03-01');
SELECT * FROM courses ORDER BY id;
CREATE TABLE lessons (
id INT PRIMARY KEY,
course_id INT,
title TEXT,
duration_min INT,
FOREIGN KEY (course_id) REFERENCES courses(id)
);
INSERT INTO lessons VALUES
(1, 1, 'SELECT и FROM', 10),
(2, 1, 'JOIN', 15),
(3, 2, 'Pandas', 20),
(4, 3, 'Основы BI', 12),
(5, 3, 'Метрики и дашборды', 18);
SELECT * FROM lessons ORDER BY id;
CREATE TABLE enrollments (
id INT PRIMARY KEY,
user_id INT,
course_id INT,
enrolled_at DATE,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (course_id) REFERENCES courses(id)
);
INSERT INTO enrollments VALUES
(1, 1, 1, '2023-01-15'),
(2, 1, 2, '2023-02-01'),
(3, 2, 1, '2023-01-20'),
(4, 3, 2, '2023-03-05'),
(5, 4, 3, '2023-04-01');
SELECT * FROM enrollments ORDER BY id;
CREATE TABLE lesson_views (
id INT PRIMARY KEY,
user_id INT,
lesson_id INT,
viewed_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (lesson_id) REFERENCES lessons(id)
);
INSERT INTO lesson_views VALUES
(1, 1, 1, '2023-01-16 10:00:00'),
(2, 1, 2, '2023-01-16 10:15:00'),
(3, 2, 1, '2023-01-21 09:00:00'),
(4, 3, 3, '2023-03-06 12:00:00'),
(5, 4, 4, '2023-04-02 10:00:00'),
(6, 4, 5, '2023-04-03 10:00:00');
SELECT * FROM lesson_views ORDER BY id;
--Создаем materialized view c названием user_activity_summary
DROP MATERIALIZED VIEW IF EXISTS user_activity_summary;
CREATE MATERIALIZED VIEW user_activity_summary AS
WITH
enr AS (
SELECT
user_id,
COUNT(*) AS enrollment_count
FROM enrollments
GROUP BY user_id
),
views AS (
SELECT
user_id,
COUNT(DISTINCT lesson_id) AS viewed_lessons_count,
MIN(viewed_at) AS first_view,
MAX(viewed_at) AS last_view
FROM lesson_views
GROUP BY user_id
),
possible AS (
SELECT
e.user_id,
COUNT(l.id) AS possible_lessons
FROM enrollments e
JOIN lessons l ON l.course_id = e.course_id
GROUP BY e.user_id
)
SELECT
u.id AS user_id,
u.name,
enr.enrollment_count,
COALESCE(views.viewed_lessons_count, 0) AS viewed_lessons_count,
views.last_view,
views.first_view,
ROUND(
COALESCE(views.viewed_lessons_count, 0)::numeric
/ possible.possible_lessons * 100,
2
) || '%' AS engagement_rate
FROM users u
JOIN enr ON enr.user_id = u.id
JOIN possible ON possible.user_id = u.id
LEFT JOIN views ON views.user_id = u.id
ORDER BY u.id;
--Проверка
SELECT * FROM user_activity_summary ORDER BY user_id;
--Создаем materialized view c названием course_summary
DROP MATERIALIZED VIEW IF EXISTS course_summary;
CREATE MATERIALIZED VIEW course_summary AS
WITH
-- 1) сколько уроков в каждом курсе
lessons_cnt AS (
SELECT
course_id,
COUNT(*) AS lessons_count
FROM lessons
GROUP BY course_id
),
-- 2) сколько уникальных пользователей записано на курс
users_cnt AS (
SELECT
course_id,
COUNT(DISTINCT user_id) AS unique_users
FROM enrollments
GROUP BY course_id
),
-- 3) просмотры по курсу (через lessons -> course_id)
views_cnt AS (
SELECT
l.course_id,
COUNT(lv.id) AS total_views,
MIN(lv.viewed_at) AS first_viewed_at,
MAX(lv.viewed_at) AS last_viewed_at
FROM lessons l
LEFT JOIN lesson_views lv ON lv.lesson_id = l.id
GROUP BY l.course_id
)
SELECT
c.id AS course_id,
c.title AS course_title,
COALESCE(u.unique_users, 0) AS unique_users,
COALESCE(ls.lessons_count, 0) AS lessons_count,
COALESCE(v.total_views, 0) AS total_views,
ROUND(
COALESCE(v.total_views, 0)::numeric / NULLIF(COALESCE(u.unique_users, 0), 0),
2
) AS avg_views_per_user,
v.first_viewed_at,
v.last_viewed_at
FROM courses c
LEFT JOIN users_cnt u ON u.course_id = c.id
LEFT JOIN lessons_cnt ls ON ls.course_id = c.id
LEFT JOIN views_cnt v ON v.course_id = c.id
ORDER BY c.id;
SELECT * FROM course_summary ORDER BY course_id;
--Создаем materialized view c названием platform_summary
DROP MATERIALIZED VIEW IF EXISTS platform_summary;
CREATE MATERIALIZED VIEW platform_summary AS
SELECT
(SELECT COUNT(*) FROM users) AS total_users,
(SELECT COUNT(*) FROM courses) AS total_courses,
(SELECT COUNT(*) FROM lessons) AS total_lessons,
(SELECT COUNT(DISTINCT user_id) FROM lesson_views) AS users_with_views,
ROUND(
(SELECT COUNT(*) FROM lessons)::numeric
/ (SELECT COUNT(*) FROM courses),
2
) AS avg_lessons_per_course,
ROUND(
(SELECT COUNT(*) FROM lesson_views)::numeric
/ (SELECT COUNT(*) FROM lessons),
2
) AS avg_views_per_lesson;
SELECT * FROM platform_summary;
--Доп. задание: EXPLAIN
EXPLAIN SELECT * FROM user_activity_summary;
EXPLAIN SELECT * FROM course_summary;
Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

@@ -0,0 +1,72 @@
--Задача 1
В варианте А: WHERE фильтрует товары и убирает те, которые не в наличии, потом эти товары(строки)участвуют в группировке, в варианте В - HAVING фильтрует уже сгруппированные данные(категории)
--Задача 2356
SELECT teacher_id,
COUNT(DISTINCT subject_id) AS cnt
FROM teacher
GROUP BY teacher_id;
--Задача 1581
SELECT customer_id,
COUNT(v.visit_id) AS count_no_trans
FROM visits v
LEFT JOIN transactions t
ON v.visit_id = t.visit_id
WHERE t.transaction_id IS NULL
GROUP BY customer_id;
--Задача 1141
SELECT activity_date AS day,
COUNT(DISTINCT user_id) AS active_users
FROM Activity
WHERE activity_date BETWEEN '2019-06-28' AND '2019-07-27'
GROUP BY activity_date;
--Задача 596
SELECT class
FROM Courses
GROUP BY class
HAVING COUNT(student) >= 5;
--Задача 1075
SELECT project_id,
ROUND(AVG(experience_years), 2) AS average_years
FROM Project p
JOIN Employee e
ON p.employee_id = e.employee_id
GROUP BY project_id;
--Задача 1729
SELECT user_id,
COUNT (follower_id) AS followers_count
FROM followers
GROUP BY user_id
ORDER BY user_id;
--Задача 1633
SELECT
r.contest_id,
ROUND(
COUNT(r.user_id) * 100.0 / (SELECT COUNT(*) FROM Users),
2
) AS percentage
FROM Register r
GROUP BY r.contest_id
ORDER BY percentage DESC, r.contest_id ASC;
--Задача 1251
SELECT
p.product_id,
COALESCE(
ROUND(
SUM(u.units * p.price) * 1.0 / NULLIF(SUM(u.units), 0),
2
),
0
) AS average_price
FROM Prices p
LEFT JOIN UnitsSold u
ON p.product_id = u.product_id
AND u.purchase_date BETWEEN p.start_date AND p.end_date
GROUP BY p.product_id;
@@ -0,0 +1,43 @@
--Задача 1757
SELECT product_id
FROM Products
WHERE low_fats = 'Y' AND recyclable = 'Y';
--Задача 584
SELECT name
FROM Customer
WHERE referee_id!= 2 OR referee_id IS NULL;
--Задача 595
SELECT name, population, area
FROM World
WHERE area >= 3000000
OR population >= 25000000;
--Задача 1148
SELECT DISTINCT author_id AS id
FROM Views
WHERE author_id = viewer_id
ORDER BY id;
--Задача 1683
SELECT tweet_id
FROM Tweets
WHERE CHAR_LENGTH(content) > 15;
--Задача 620
SELECT id, movie, description, rating
FROM Cinema
WHERE id % 2 = 1
AND description != 'boring'
ORDER BY rating DESC;
--Задача 1667
SELECT
user_id,
CONCAT(
UPPER(SUBSTR(name, 1, 1)),
LOWER(SUBSTR(name, 2))
) AS name
FROM Users
ORDER BY user_id;
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

@@ -0,0 +1,33 @@
--Задача 1
CREATE TABLE products_extended (
id INT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
category VARCHAR(50),
price DECIMAL(6,2),
in_stock BOOLEAN DEFAULT true,
email_support VARCHAR
);
--Задача 2 Вариант с DEFAULT (in_stock не указываем)
INSERT INTO products_extended (id, name, category, price, email_support)
VALUES
(1, 'Mi Band 7', 'smartwatch', 49.00, 'miband@xiaomi.com'),
(2, 'Surface Pro', 'tablet', 1299.00, 'surface@microsoft.com');
--Задача 3
ALTER TABLE products_extended
ADD COLUMN rating DECIMAL(3,2),
DROP COLUMN email_support;
--Задача 4
UPDATE products_extended
SET price = price * 1.1
WHERE name = 'Mi Band 7';
--Задача 5(удаляет данные/таблицу)
TRUNCATE TABLE products_extended;
DROP TABLE products_extended;
@@ -0,0 +1,54 @@
SELECT
name,
category,
price,
price * 1.10 AS price_with_markup,
price * 1.20 AS price_with_vat,
COALESCE(email_support, 'нет почты') AS status
FROM products_extended
WHERE in_stock = true
AND category NOT IN ('smartwatch', 'ebook_reader')
AND price BETWEEN 300 AND 1300;
SELECT
name,
category,
email_support,
price * 1.25 AS price_25,
price > 1000 AS premium
FROM products_extended
WHERE (price < 150 OR price > 1200)
AND email_support IS NOT NULL;
SELECT
id,
name,
category,
price
FROM products_extended
WHERE category IN ('smartphone', 'laptop', 'tablet')
AND name NOT LIKE '%i%'
AND price BETWEEN 500 AND 1300
ORDER BY id;
SELECT
name,
category,
price,
'нет поддержки' AS comment
FROM products_extended
WHERE in_stock = true
AND email_support IS NULL
AND price < 800;
SELECT
name,
category,
price,
price > 1000 AS premium
FROM products_extended
WHERE (name LIKE '%Pro' OR name LIKE '%X%')
AND in_stock = true
AND price > 700;
@@ -0,0 +1,20 @@
--Задача 1211
SELECT
query_name,
ROUND(AVG(rating * 1.0 / position), 2) AS quality,
ROUND(AVG(CASE WHEN rating < 3 THEN 1 ELSE 0 END) * 100,2) AS poor_query_percentage
FROM Queries
GROUP BY query_name;
--Задача 1193
SELECT
TO_CHAR(trans_date, 'YYYY-MM') AS month,
country,
COUNT(*) AS trans_count,
SUM(CASE WHEN state = 'approved' THEN 1 ELSE 0 END) AS approved_count,
SUM(amount) AS trans_total_amount,
SUM(CASE WHEN state = 'approved' THEN amount ELSE 0 END) AS approved_total_amount
FROM Transactions
GROUP BY
TO_CHAR(trans_date, 'YYYY-MM'),
country;
+23
View File
@@ -0,0 +1,23 @@
--task 1
select
user_id,
indexOf(tags, 'promo') as promo_index,
arrayJoin(tags)
from events
where has(tags, 'promo')
-- task 2
select
user_id,
scores['math'] as math_score,
mapValues(scores) as all_values
from metrics
-- task 3
select
name,
contact.type as contact_type,
contact.value as contact_value
from users_nested
array join contacts as contact
where contact.type = 'email'
+61
View File
@@ -0,0 +1,61 @@
-- task 1
CREATE TABLE raw_data (
user_id String,
balance String,
created_at String
) ENGINE = MergeTree ORDER BY tuple();
select
user_id,
toDecimal64OrNull(balance, 2) as balance,
toDateTime64OrNull(created_at, 3) as created_at
from raw_data
where
toDecimal64OrNull(balance, 2) is not null and
toDateTime64OrNull(created_at, 3) is not null
-- task 2 (failed)
CREATE TABLE events (
event_id String,
event_date String, -- форматы: "2023-01-01", "01.02.2023", "invalid"
event_time String -- форматы: "12:30:45", "25:99:99"
);
select
event_id,
toStartOfHour(toDateTimeOrNull(concatWithSeparator(' ', event_date, event_time)))
from events
where
toDateOrNull(event_date) is not null and toTimeOrNull(event_time) is not null
-- task 3
CREATE TABLE user_activity (
user_id UInt32,
actions String -- JSON: ["login", "purchase", "logout"]
);
SELECT
user_id,
actions_arr,
length(actions_arr) AS actions_count
FROM
(
SELECT
user_id,
JSONExtract(actions, 'Array(String)') AS actions_arr
FROM user_activity
)
WHERE has(actions_arr, 'purchase');
-- task 4
CREATE TABLE sensor_data (
sensor_id UInt32,
value Float64,
status Enum('ok' = 1, 'error' = 2)
);
SELECT
toString(status) as status,
avg(value) as avg_value
from sensor_data
where
value is not null
group by status
+103
View File
@@ -0,0 +1,103 @@
--task 1
create table mdrozdov.products (
product_id Int,
name String
)
engine = MergeTree
order by product_id
insert into mdrozdov.products values
(101, 'iPhone 14'),
(102, 'Galaxy S22'),
(103, 'Pixel 7')
create table mdrozdov.prices (
product_id Int,
price Int,
region String
)
engine = MergeTree
order by product_id
insert into mdrozdov.prices values
(101, 999, 'US'),
(101, 1049, 'EU'),
(102, 850, 'US'),
(102, 899, 'CA'),
(103, 799, 'JP')
-- 1)
select
product_id,
name,
price,
region
from mdrozdov.products
any left join mdrozdov.prices
on mdrozdov.products.product_id = mdrozdov.prices.product_id
-- 2)
create table mdrozdov.ticks
ON CLUSTER clickhouse_cluster (
time DateTime,
symbol String,
price Decimal(10, 2)
)
engine = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/mdrozdov/ticks',
'{replica}'
)
order by (time, symbol)
insert into mdrozdov.ticks values
('2025-07-17 10:00:00', 'AAPL', 190.5),
('2025-07-17 10:01:00', 'AAPL', 191.2),
('2025-07-17 10:02:30', 'AAPL', 192.0),
('2025-07-17 10:05:00', 'GOOG', 2850.0)
create table mdrozdov.orders
ON CLUSTER clickhouse_cluster (
time DateTime,
symbol String,
size Int
)
engine = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/mdrozdov/orders',
'{replica}'
)
order by (symbol, time)
insert into mdrozdov.orders values
('2025-07-17 10:00:30', 'AAPL', 50),
('2025-07-17 10:02:00', 'AAPL', 75),
('2025-07-17 10:03:00', 'AAPL', 20),
('2025-07-17 10:04:00', 'GOOG', 15)
select
o.time,
o.symbol,
t.price as last_price
from mdrozdov.orders o
asof join mdrozdov.ticks t
on o.symbol = t.symbol and o.time >= t.time
-- task 3
select
user_id,
name
from users
where user_id in (
select user_id
from payments
where amount > 1000
)
-- task 4
select
bl.user_id,
u.name,
bl.event_time,
bl.event_type
from big_logs bl
any left join users u
using(user_id)
+64
View File
@@ -0,0 +1,64 @@
--page_views table
create table page_views (
user_id UInt64,
url String,
view_time DateTime
)
Engine = MergeTree
Order by (user_id, view_time)
Partition by toYYYYMM(view_time)
-- users table
create table users(
id UInt64,
name String,
updated_at DateTime
)
ENGINE = ReplacingMergeTree(updated_at)
Order by id;
-- region_sales table
create table mdrozdov.region_sales(
region String,
date Date,
sales UInt64
)
ENGINE = SummingMergeTree
Partition by toYYYYMM(date)
Order by (region, date)
insert into mdrozdov.region_sales (region, date, sales)
values ('Russia', '2025-01-14', 30), ('Russia', '2025-01-16', 22), ('USA', '2025-01-14', 1400)
OPTIMIZE TABLE mdrozdov.region_sales FINAL
-- user_metrics table
create table mdrozdov.user_metrics (
region String,
avg_age_state AggregateFunction(avg, UInt8),
active_users_state AggregateFunction(uniq, UInt64)
)
Engine = AggregatingMergeTree
Order by region
INSERT INTO mdrozdov.user_metrics
SELECT
'Moscow' AS region,
avgState(toUInt8(25)) AS avg_age_state,
uniqState(toUInt64(100)) AS active_users_state
UNION ALL
SELECT
'Moscow',
avgState(toUInt8(30)),
uniqState(toUInt64(50))
UNION ALL
SELECT
'SPB',
avgState(toUInt8(20)),
uniqState(toUInt64(10));
SELECT
region,
avgMerge(avg_age_state) AS avg_age,
uniqMerge(active_users_state) AS active_users
FROM mdrozdov.user_metrics
GROUP BY region
+98
View File
@@ -0,0 +1,98 @@
-- task 1
create table mdrozdov.sales2
on CLUSTER clickhouse_cluster (
id Int,
region String,
sale_date Date,
amount Decimal(8, 2)
)
engine = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/mdrozdov/sales2',
'{replica}'
)
partition by toYYYYMM(sale_date)
order by id
-- удаление таблицы
-- drop table mdrozdov.sales
-- on CLUSTER clickhouse_cluster; не удаляет таблицу полностью, она остается в zookeeper
INSERT into mdrozdov.sales2 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 mdrozdov.sales2
drop partition '202405'
--2 значения удалились, которые хранились в соответствующей партиции
-- task 2
create table mdrozdov.events2
on CLUSTER clickhouse_cluster (
id Int,
event_type String,
event_date Date
)
engine = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/mdrozdov/events2',
'{replica}'
)
partition by event_type
order by id
INSERT into mdrozdov.events2 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 mdrozdov.events2
drop partition 'click'
-- task 3
create table mdrozdov.logs
on CLUSTER clickhouse_cluster (
id Int,
user_id Int,
log_time DateTime
)
engine = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/mdrozdov/logs',
'{replica}'
)
partition by cityHash64(user_id) % 100
order by id
insert into mdrozdov.logs 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')
--Что ты выиграешь с точки зрения баланса?
--равномерное распределение данных в разных партициях, лучшее управление обновлением и удалением данных
-- Почему такой тип PARTITION не ускорит SELECT * WHERE user_id = 456?
-- потому что clickhouse не умеет выполнять условия партиции, чтобы найти сразу ту в которой надо искать,
-- поэтому будет пробегать все 100, чтобы найти нужное, поэтому важно, чтобы таблица была отсортирована по user_id
-- task 4
create table mdrozdov.visits
on CLUSTER clickhouse_cluster (
id Int,
region String,
visit_date Date
)
engine = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/mdrozdov/visits',
'{replica}'
)
partition by (region, toYYYYMM(visit_date))
order by (region, visit_date)
insert into mdrozdov.visits 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 mdrozdov.visits
drop partition ('RU', 202407) -- эффективное удаление июльских данных по россии
+136
View File
@@ -0,0 +1,136 @@
CREATE TABLE mdrozdov.users (
user_id Int,
name String,
age Int,
country String,
is_active bool
)
engine = MergeTree
order by user_id
insert into mdrozdov.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)
--1. Получи 10 самых старших **активных** пользователей.
select
user_id,
name,
age,
country,
is_active
from mdrozdov.users
where is_active
order by age desc
limit 10
--2. Выведи пользователей из `country = 'RU'`, отсортированных по имени.
select
user_id,
name,
age,
country,
is_active
from mdrozdov.users
where country = 'RU'
order by name asc
--3. Что произойдёт, если написать `LIMIT 10` **без `ORDER BY`**?
-- ответ будет недетерминирован
--4. Посчитай количество пользователей по странам.
select
country,
count(user_id) as total_users
from mdrozdov.users
group by country
--5. Для каждой страны выведи минимальный и максимальный возраст.
select
country,
min(age) as min_user_age,
max(age) as max_user_age
from mdrozdov.users
group by country
--6. Отсортируй страны по убыванию количества пользователей.
select
country,
count(user_id) as total_users
from mdrozdov.users
group by country
order by total_users desc
-- task 2
CREATE TABLE mdrozdov.events (
user_id Int,
event_time DateTime,
event_type String --mb better enum but idc
)
engine = MergeTree
order by user_id
insert into events 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-2) Добавь колонку с кумулятивным количеством событий по user_id (используй count() OVER ...)
select
user_id,
event_time,
event_type,
count(*) over (partition by user_id order by event_time) as total_click_exact_moment,
row_number() over (partition by user_id order by event_time) as row_number
from mdrozdov.events
--3) да, поддерживается, но не обязательно для коммулятивного суммирования строк
--4-6) Сделай WITH recent_events AS (...) — все события за последние 24 часа.
with mdrozdov.recent_events AS (
select *
from mdrozdov.events
where event_time >= toDateTime('2025-07-18') - interval 1 day
), category_list as (
select 'click' as event_type, 'interaction' as category
union all
select 'view', 'interaction'
union all
select 'purchase', 'conversion'
)
select
user_id,
count(*) as total_events_last_24h,
count(distinct cl.category) as uniq_categories
from recent_events re
left join category_list cl
using(event_type)
group by user_id
order by total_events_last_24h desc, user_id asc
limit 5
+65
View File
@@ -0,0 +1,65 @@
--task 1
create table mdrozdov.logs3
on CLUSTER clickhouse_cluster (
log_id Int,
ts DateTime
)
engine = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/mdrozdov/logs3',
'{replica}'
)
order by ts
alter table mdrozdov.logs3
add index idx_ts ts type minmax GRANULARITY 2;
select *
from mdrozdov.logs3
where ts between '2024-06-02' and '2024-06-03'
SELECT
query,
read_rows,
result_rows,
read_bytes
FROM system.query_log
WHERE query LIKE '%logs%'
ORDER BY event_time DESC
LIMIT 1;
-- task 2
create table mdrozdov.clicks
on CLUSTER clickhouse_cluster (
id UInt64,
country String
)
engine = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/mdrozdov/clicks',
'{replica}'
)
order by id
alter table mdrozdov.clicks
add index idx_country country type set(100) GRANULARITY 2
Explain
select *
from mdrozdov.clicks
where country = 'RU'
-- task 3
create table mdrozdov.users
on CLUSTER clickhouse_cluster (
id UInt64,
email String
)
engine = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/mdrozdov/users',
'{replica}'
)
order by id
ALTER TABLE mdrozdov.users
ADD INDEX idx_email email TYPE bloom_filter(0.01) GRANULARITY 2;
SELECT * FROM mdrozdov.users WHERE email = 'alice@mail.com';
+34
View File
@@ -0,0 +1,34 @@
-- task 1
CREATE TABLE dist_table
ENGINE = Distributed(
'cluster_name', -- нет доступа к конфигу infra clickhouse
'mdrozdov',
'logs',
rand()
);
--rand() это параметр по которому мы распределяем данные при insert на разные ноды, в данном
--случае нам это нужно для равномерности распределения, потому что таблица не требует особых связей или тп
-- task 2
--Создай VIEW top_active_users, который показывает топ-100 пользователей по числу событий
CREATE VIEW top_active_users AS
select user_id, count(*) as total_events
from events
group by user_id
order by total_events desc
limit 100
-- не подходит для дашборда с миллионами строк, потому что вычисляется каждый раз при вызове
-- для дашбордов лучше подойдет materialized view, тк хранит данные запроса и обновляется автоматически
-- при вставке новых данных
--task 3
CREATE MATERIALIZED VIEW
to
ENGINE = SummingMergeTree
order by date
mv_clicks_per_day AS
select
toDate(event_time) as date,
count(*) as total_clicks
from raw_events
group by date
+94
View File
@@ -0,0 +1,94 @@
-- Измерение пользователей
CREATE TABLE core.dim_user (
user_id INT PRIMARY KEY,
name TEXT,
age INT,
email TEXT,
registration_date DATE
)
distributed by (user_id);
-- Измерение курсов
CREATE TABLE core.dim_course (
course_id INT PRIMARY KEY,
title TEXT,
category TEXT
)
distributed by (course_id);
-- Измерение уроков
CREATE TABLE core.dim_lesson (
lesson_id INT PRIMARY KEY,
title TEXT,
duration_min INT,
course_id INT REFERENCES core.dim_course(course_id)
)
distributed by (lesson_id);
-- Опционально: измерение времени
CREATE TABLE core.dim_date (
date DATE PRIMARY KEY,
year INT,
month INT,
day INT,
weekday TEXT
)
distributed replicated;
-- Таблицы фактов:
CREATE TABLE core.fact_lesson_views (
user_id INT REFERENCES core.dim_user(user_id),
lesson_id INT REFERENCES core.dim_lesson(lesson_id),
course_id INT REFERENCES core.dim_course(course_id),
viewed_at TIMESTAMP
)
distributed by (course_id);
CREATE TABLE core.enrollments (
user_id INT REFERENCES core.dim_user(user_id),
course_id INT REFERENCES core.dim_course(course_id),
enrolled_at TIMESTAMP
)
distributed by (course_id);
--ETL переброс данных из raw в core
INSERT INTO core.dim_user (user_id, name, age, email, registration_date)
SELECT DISTINCT
id::INT,
name::TEXT,
age::INT,
email::TEXT,
registration_date::DATE
FROM raw.users;
INSERT INTO core.dim_course (course_id, title, category)
SELECT DISTINCT
id::INT,
title::TEXT,
category::TEXT
FROM raw.courses;
INSERT INTO core.dim_lesson (lesson_id, title, duration_min, course_id)
SELECT DISTINCT
id::INT,
title::TEXT,
duration_min::INT,
course_id::INT
FROM raw.lessons;
-- Заполнение таблицы фактов
insert into core.fact_lesson_views (user_id, lesson_id, course_id, viewed_at)
select
lw.user_id,
lw.lesson_id,
l.course_id,
lw.viewed_at
from raw.lessons_views lw
join raw.lessons l on lw.lesson_id = l.id;
insert into core.enrollments (user_id, course_id, enrolled_at)
select
user_id,
course_id,
enrolled_at
from raw.enrollments;
+71
View File
@@ -0,0 +1,71 @@
--lesson_popularity_summary data mart
create materialized view dm.lesson_popularity_summary as
select
dl.lesson_id,
dl.title as lesson_title,
dl.course_id,
dc.title as course_title,
count(flv.lesson_id) as total_views,
count(distinct flv.user_id) as unique_users,
min(flv.viewed_at) as first_view,
max(flv.viewed_at) as last_view
from core.dim_lesson dl
left join core.dim_course dc
on dl.course_id = dc.course_id
left join core.fact_lesson_views flv
on dl.lesson_id = flv.lesson_id
group by dl.lesson_id, dl.title, dl.course_id, dc.title
-- inactive_users_summary data mart
create materialized view dm.inactive_users_summary as
select
du.user_id,
du.name as user_name,
du.email,
du.age,
count(e.course_id) as enrollments_count,
du.registration_date
from core.dim_user du
left join core.enrollments e
on du.user_id = e.user_id
left join core.fact_lesson_views flv
on du.user_id = flv.user_id
where flv.user_id is null
group by du.user_id, du.name, du.email, du.age, du.registration_date
-- course_completion_rate data mart
create materialized view dm.course_completion_rate as
with total_lessons_per_course as(
select
course_id,
count(*) as lessons_in_course
from core.dim_lesson
group by course_id
), lessons_viewed as (
select
user_id,
course_id,
count(*) as lessons_viewed
from fact_lesson_views flv
group by user_id, course_id
)
select
du.user_id as user_id,
du.name as user_name,
dc.course_id as course_id,
dc.title as course_title,
tlpc.lessons_in_course,
coalesce(lv.lessons_viewed, 0) as lessons_viewed,
round(
coalesce(
coalesce(lv.lessons_viewed, 0)*1.0/nullif(tlpc.lessons_in_course, 0), 0
), 2) as completion_rate
from core.enrollments e
join core.dim_user du
on e.user_id = du.user_id
join total_lessons_per_course tlpc
on tlpc.course_id = e.course_id
join core.dim_course dc
on e.course_id = dc.course_id
left join lessons_viewed lv
on du.user_id = lv.user_id and dc.course_id = lv.course_id
+44
View File
@@ -0,0 +1,44 @@
create schema raw;
create schema core;
create schema dm;
create table raw.users(
id INT,
name varchar(50),
age INT,
email varchar(50),
registration_date date
)
distributed by (id);
create table raw.courses (
id INT,
title text,
category varchar(40),
created_at date
)
distributed by (id);
create table raw.lessons (
id INT,
course_id INT,
title TEXT,
duration_min INT
)
distributed by (course_id);
create table raw.enrollments (
id INT,
user_id INT,
course_id INT,
enrolled_at date
)
distributed by (user_id);
create table raw.lessons_views(
id int,
user_id int,
lesson_id int,
viewed_at timestamp
)
distributed by (user_id)