add homework
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
-- Задача 1. Пользователи с заказами
|
||||
SELECT id AS user_id
|
||||
FROM dmezentsev.users
|
||||
WHERE id IN (SELECT user_id
|
||||
FROM dmezentsev.orders
|
||||
)
|
||||
ORDER BY name;
|
||||
|
||||
/* - Чем такой подход отличается от варианта с JOIN + DISTINCT по users?
|
||||
|
||||
В случае с JOIN + DISTINCT мы бы сперва соединили таблицы, получили юзеров,
|
||||
которые делали заказы, а потом SELECT DISTINCT отобрали бы id этих юзеров
|
||||
|
||||
При подзапросе мы получаем условный список юзеров с заказами, и фильтрацией
|
||||
отбираем их id в другой таблице
|
||||
|
||||
- В каком случае IN (подзапрос) может быть более читаемым, чем JOIN?
|
||||
|
||||
В случае, когда нужны поля первой таблицы, а подзапрос только как фильтр.
|
||||
Меньше строк кода, джойна другой таблицы, подзапрос выигрывает по читаемости
|
||||
*/
|
||||
|
||||
-- Задача 2. Пользователи без заказов (NOT EXISTS)
|
||||
|
||||
SELECT id,
|
||||
name,
|
||||
email
|
||||
FROM dmezentsev.users u
|
||||
WHERE NOT EXISTS (SELECT 1
|
||||
FROM dmezentsev.orders o
|
||||
WHERE o.user_id = u.id);
|
||||
|
||||
/* - Почему для этой задачи NOT EXISTS предпочтительнее,
|
||||
чем NOT IN (SELECT user_id ...)?
|
||||
|
||||
Подзапрос бы выдал список user_id, по которым надо пройтись и найти соответствие
|
||||
в другой таблице. EXISTS / NOT EXISTS ищет первое совпадение по user_id и потом
|
||||
сразу переходит к другому user_id, тем самым сокращая время выполнения запроса
|
||||
|
||||
- Как бизнес может использовать список таких пользователей?
|
||||
|
||||
Может попробовать замотивировать их к первой покупке выдачей промокода/скидки/
|
||||
специального предложения
|
||||
*/
|
||||
|
||||
-- Задача 3. Сколько заказов у каждого пользователя (подзапрос в SELECT)
|
||||
|
||||
SELECT u.name,
|
||||
(SELECT count(*)
|
||||
FROM dmezentsev.orders o
|
||||
WHERE o.user_id = u.id) AS orders_count
|
||||
FROM dmezentsev.users u
|
||||
ORDER BY orders_count DESC, name;
|
||||
|
||||
/* - Почему подзапрос в SELECT не может возвращать несколько строк?
|
||||
|
||||
Потому что для каждого u.name он возвращает только одну строку - число заказов.
|
||||
Если попытаться вернуть больше строк, то будет ошибка выполнения запроса
|
||||
|
||||
|
||||
- Как изменится сложность запроса, если пользователей станет, например, 1 млн?
|
||||
|
||||
Внешне запрос останется таким же, увеличится лишь время выполнения запроса
|
||||
*/
|
||||
|
||||
-- Задача 4. Сумма выручки по каждому пользователю (подзапрос или CTE)
|
||||
|
||||
SELECT u.name,
|
||||
t.total_revenue
|
||||
FROM (SELECT o.user_id,
|
||||
SUM(oi.quantity * oi.price_at_purchase) AS total_revenue
|
||||
FROM dmezentsev.orders o
|
||||
JOIN dmezentsev.order_items oi ON oi.order_id = o.id
|
||||
GROUP BY o.user_id) AS t
|
||||
JOIN dmezentsev.users u ON t.user_id = u.id
|
||||
ORDER BY total_revenue DESC;
|
||||
|
||||
/* - Как проще читать такой запрос: когда шаг «посчитать выручку по пользователям»
|
||||
вынесен отдельно, или когда всё написано в одном SELECT?
|
||||
|
||||
Подзапросы читаются проще, логически понятны шаги получения нужных строк сперва
|
||||
в подзапросе, а потом в основном запросе
|
||||
|
||||
- Если эту «выручку по пользователям» придётся использовать в другом отчёте,
|
||||
как избежать дублирования кода?
|
||||
|
||||
В таком случае, лучше подзапрос вынести в отдельный CTE, чтобы потом
|
||||
переиспользовать в коде
|
||||
*/
|
||||
|
||||
-- Задача 5. Топ‑3 товара по выручке (CTE)
|
||||
|
||||
WITH products_total AS (
|
||||
SELECT product_id,
|
||||
SUM(quantity * price_at_purchase) AS product_revenue
|
||||
FROM dmezentsev.order_items
|
||||
GROUP BY product_id
|
||||
)
|
||||
|
||||
SELECT p.name,
|
||||
p.category,
|
||||
pt.product_revenue
|
||||
FROM dmezentsev.products p
|
||||
JOIN products_total pt ON p.id = pt.product_id
|
||||
ORDER BY product_revenue DESC
|
||||
LIMIT 3;
|
||||
|
||||
/* - Почему удобно сначала считать агрегаты по товарам,
|
||||
а потом уже подтягивать имена/категории?
|
||||
|
||||
Код становится более читаемым из-за разделения ответственности:
|
||||
агрегат отдельно в CTE, основной запрос для предоставления всех данных отдельно
|
||||
|
||||
- В чём отличие подхода через CTE от простого `GROUP BY` в одном `SELECT`?
|
||||
|
||||
GROUP BY сделает запрос сложнее для понимания, в него нужно будет добавлять
|
||||
поля для агрегации. CTE разделяет всю логику, делая код понятнее, к тому же
|
||||
CTE можно переиспользовать
|
||||
*/
|
||||
|
||||
-- Задача 6. Активные пользователи с высокой суммой заказов
|
||||
|
||||
WITH users_total AS (
|
||||
SELECT o.user_id,
|
||||
SUM(quantity * price_at_purchase) AS total_revenue
|
||||
FROM dmezentsev.orders o
|
||||
JOIN dmezentsev.order_items oi ON o.id = oi.order_id
|
||||
GROUP BY o.user_id
|
||||
)
|
||||
|
||||
SELECT u.name,
|
||||
u.email,
|
||||
ut.total_revenue
|
||||
FROM dmezentsev.users u
|
||||
JOIN users_total ut ON u.id = ut.user_id
|
||||
WHERE u.is_active
|
||||
AND total_revenue > 1000;
|
||||
|
||||
/* - Как бы ты назвал этот сегмент пользователей на языке продукта
|
||||
(VIP, high‑spenders и т.п.)?
|
||||
|
||||
Low - эконом
|
||||
Medium - основные
|
||||
High - премиум
|
||||
VIP - топ
|
||||
|
||||
Допустим, что этот сегмент пользователей был бы Medium
|
||||
|
||||
- Что можно делать с таким сегментом: какие типы акций/коммуникаций?
|
||||
|
||||
Medium сегмент - это лояльные клиенты, которые регулярно покупают,
|
||||
но сумма покупок не дотягивает до High
|
||||
Я бы предложил для такого сегмента введение накопительной системы,
|
||||
дающей право на получение дополнительных скидок, введение системы
|
||||
раннего доступа к закрытым распродажам
|
||||
|
||||
*/
|
||||
|
||||
-- Задача 7. «Подозрительно маленькие» оплаченные заказы
|
||||
|
||||
WITH orders_total AS (
|
||||
SELECT oi.order_id,
|
||||
SUM(oi.quantity * oi.price_at_purchase) AS order_revenue
|
||||
FROM dmezentsev.order_items oi
|
||||
INNER JOIN dmezentsev.orders o ON oi.order_id = o.id
|
||||
WHERE o.status = 'paid'
|
||||
GROUP BY oi.order_id
|
||||
)
|
||||
SELECT ot.order_id,
|
||||
o.order_date,
|
||||
ot.order_revenue
|
||||
FROM orders_total ot
|
||||
JOIN dmezentsev.orders o ON ot.order_id = o.id
|
||||
WHERE ot.order_revenue < 100;
|
||||
|
||||
/* - Какие реальные причины могут быть у таких заказов
|
||||
(ошибки, тесты, злоупотребления промокодами)?
|
||||
|
||||
Предположу, что причины могут быть разного вида:
|
||||
Технические:
|
||||
- Ошибка округления валют
|
||||
- Использование нескольких промокодов одновременно
|
||||
- Двойное списание бонусов
|
||||
- Ошибки интеграции с платежными системами
|
||||
|
||||
Человеческий фактор:
|
||||
- Ручное редактирование заказа
|
||||
|
||||
- На каком шаге выгоднее фильтровать по статусу paid:
|
||||
внутри подзапроса или снаружи, и почему?
|
||||
|
||||
Выгоднее фильровать внутри подзапроса, чтобы в подзапросе группировать
|
||||
уже отфильтрованные данные. И уже в основном запросе джойнить, а меньше
|
||||
строк для джойна ускорят запрос
|
||||
*/
|
||||
Executable
+375
@@ -0,0 +1,375 @@
|
||||
### Задача 1 — Читаем план
|
||||
|
||||
#### Требования:
|
||||
|
||||
Выполни этот запрос:
|
||||
|
||||
```sql
|
||||
EXPLAIN
|
||||
SELECT name, email
|
||||
FROM users
|
||||
WHERE id = 1;
|
||||
```
|
||||
|
||||
**1. Какой тип сканирования использует база?**
|
||||
|
||||
Index Scan (индексное сканирование)
|
||||
|
||||
**2. Что означает строка `Index Cond: (id = 1)`?**
|
||||
|
||||
Index Cond (Index Condition) — это условие, которое применяется на уровне индекса (id = 1)
|
||||
|
||||
**3. Почему база использует индекс, а не читает всю таблицу?**
|
||||
|
||||
База выбирает индекс вместо полного сканирования таблицы, потому что id — это первичный ключ с индексом, что делает Index Scan значительно быстрее и дешевле, чем Seq Scan
|
||||
|
||||
### Задача 2 — Seq Scan vs Index Scan
|
||||
|
||||
#### Требования:
|
||||
|
||||
Выполни оба запроса с EXPLAIN:
|
||||
|
||||
```sql
|
||||
-- Запрос А
|
||||
EXPLAIN
|
||||
SELECT * FROM users WHERE id = 2;
|
||||
|
||||
-- Запрос Б
|
||||
EXPLAIN
|
||||
SELECT * FROM users WHERE age > 25;
|
||||
```
|
||||
|
||||
**1. Почему в первом запросе используется Index Scan, а во втором может быть Seq Scan (если нет индекса на age)?**
|
||||
|
||||
- Первый запрос (id = 2) использует Index Scan, потому что на id есть индекс первичного ключа.
|
||||
- Второй запрос (age > 25) использует Seq Scan, потому что на age нет индекса
|
||||
|
||||
### Задача 3 — Замеряем время
|
||||
|
||||
#### Требования:
|
||||
|
||||
Выполни запрос с EXPLAIN ANALYZE:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT name, price
|
||||
FROM products
|
||||
WHERE category = 'Электроника';
|
||||
```
|
||||
|
||||
**1. Запиши Execution Time**
|
||||
|
||||
Execution Time: 0.699 ms
|
||||
|
||||
**2. Определи тип сканирования (Seq Scan или Index Scan)**
|
||||
|
||||
Seq Scan
|
||||
|
||||
**3. Сколько строк вернул запрос?**
|
||||
|
||||
rows=3
|
||||
|
||||
### Задача 4 — До и после индекса
|
||||
|
||||
#### Требования:
|
||||
|
||||
**Шаг 1:** Выполни запрос без индекса:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM users WHERE country = 'Россия';
|
||||
```
|
||||
|
||||
Запиши **Execution Time** и тип сканирования.
|
||||
|
||||
Execution Time: 76.281 ms
|
||||
Seq Scan
|
||||
|
||||
**Шаг 2:** Создай индекс:
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_country ON users(country);
|
||||
```
|
||||
|
||||
**Шаг 3:** Снова выполни тот же запрос с EXPLAIN ANALYZE.
|
||||
|
||||
**1. Изменился ли тип сканирования?**
|
||||
|
||||
Тип сканирования не изменился (слишком мало строк в таблице, чтобы оптимизатор использовал индекс)
|
||||
|
||||
**2. Стал ли запрос быстрее? На сколько?**
|
||||
|
||||
Execution Time: 0.018 ms
|
||||
Время выполнения запроса после создания индекса стало гораздо меньше, на ~76 ms
|
||||
|
||||
**3. Почему индекс помог?**
|
||||
|
||||
Индекс поможет, потому что:
|
||||
|
||||
- Уменьшит количество читаемых данных - читать только страницы с нужными значениями
|
||||
- Использует структуру B-Tree
|
||||
- Позволяет найти строки без сканирования всей таблицы
|
||||
|
||||
### Задача 5 — Анализируем JOIN
|
||||
|
||||
#### Требования: выполни запрос:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date, o.status
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.country = 'Россия';
|
||||
```
|
||||
|
||||
**1. Какой тип JOIN использует база (Hash Join, Nested Loop, Merge Join)?**
|
||||
|
||||
Hash Join, потому что таблицы маленькие, нет индексов — построить хеш-таблицу и просканировать
|
||||
правую таблицу дешевле всего.
|
||||
|
||||
**2. С какой таблицы база начала чтение?**
|
||||
|
||||
orders
|
||||
|
||||
**3. Сколько времени занял весь запрос?**
|
||||
|
||||
Execution Time: 0.083 ms
|
||||
|
||||
### Задача 6 — Почему медленно?
|
||||
|
||||
#### Дан запрос:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE LOWER(email) = 'alexey@mail.ru';
|
||||
```
|
||||
|
||||
**1. Выполни запрос и посмотри на план**
|
||||
|
||||
Seq Scan on users (cost=0.00..1.15 rows=1 width=467) (actual time=0.012..0.012 rows=0 loops=1)
|
||||
Filter: (lower((email)::text) = 'alexey@mail.ru'::text)
|
||||
Rows Removed by Filter: 10
|
||||
Planning Time: 0.429 ms
|
||||
Execution Time: 0.026 ms
|
||||
|
||||
**2. Использует ли база индекс?**
|
||||
|
||||
План показывает Seq Scan, а не Index Scan.
|
||||
|
||||
**3. Почему НЕТ? (даже если индекс на email есть)**
|
||||
|
||||
Применение функции LOWER() к колонке
|
||||
|
||||
**4. Как исправить запрос, чтобы индекс заработал?**
|
||||
|
||||
- Создать индекс с учетом применения функции LOWER()
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_users_email_lower ON users (LOWER(email));
|
||||
```
|
||||
- Использовать ILIKE
|
||||
- Хранить нормализованный email
|
||||
|
||||
### Задача 7 — Функции убивают индексы
|
||||
|
||||
#### Сравни два запроса:
|
||||
|
||||
```sql
|
||||
-- Запрос А
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM users WHERE age + 5 > 30;
|
||||
|
||||
-- Запрос Б
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM users WHERE age > 25;
|
||||
```
|
||||
|
||||
**1. Какой запрос работает быстрее?**
|
||||
|
||||
Запрос Б работает быстрее: 0.016 ms против 0.043 ms
|
||||
|
||||
**2. Почему первый запрос НЕ может использовать индекс на `age`?**
|
||||
|
||||
Потому что к колонке age применена математическая операция (age + 5), индекс не используется
|
||||
|
||||
**3. Как правильно переписать запрос А?**
|
||||
|
||||
```sql
|
||||
SELECT * FROM users WHERE age > 25;
|
||||
```
|
||||
|
||||
### Задача 8 — Диагностика медленного запроса
|
||||
|
||||
#### Дан запрос:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, COUNT(o.id) AS orders_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE EXTRACT(YEAR FROM u.created_at) = 2026
|
||||
GROUP BY u.name;
|
||||
```
|
||||
|
||||
**1. Почему функция `EXTRACT()` — плохая идея?**
|
||||
|
||||
Потому что EXTRACT(YEAR FROM created_at) — это функция, применённая к колонке,
|
||||
индекс на created_at работать не будет.
|
||||
|
||||
**2. Как переписать условие WHERE без функции?**
|
||||
|
||||
```sql
|
||||
WHERE u.created_at >= '2026-01-01' AND u.created_at < '2027-01-01'
|
||||
```
|
||||
|
||||
**3. Будет ли использоваться индекс на `created_at` в текущем виде?**
|
||||
|
||||
Индекс на created_at НЕ БУДЕТ использоваться
|
||||
|
||||
### Задача 9 — SELECT * против конкретных колонок
|
||||
|
||||
#### Сравни два запроса:
|
||||
|
||||
```sql
|
||||
-- Запрос А
|
||||
EXPLAIN ANALYZE
|
||||
SELECT * FROM products WHERE price > 50000;
|
||||
|
||||
-- Запрос Б
|
||||
EXPLAIN ANALYZE
|
||||
SELECT name, price FROM products WHERE price > 50000;
|
||||
```
|
||||
|
||||
**1. Какой запрос работает быстрее?**
|
||||
|
||||
Запрос Б работает быстрее (0.017 ms против 0.051 ms)
|
||||
|
||||
**2. Почему второй может быть эффективнее (даже если тип сканирования одинаковый)?**
|
||||
|
||||
Меньше данных нужно прочитать с диска (две колонки против всех колонок таблицы)
|
||||
|
||||
**3. В каких ситуациях разница будет особенно заметна?**
|
||||
|
||||
- Большое количество строк
|
||||
- Медленные диски (HDD)
|
||||
- Высокая нагрузка (тысячи запросов в секунду)
|
||||
|
||||
### Задача 10 — Оптимизируем JOIN с условием
|
||||
|
||||
#### Дан запрос:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, p.name AS product_name, oi.quantity
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE u.country = 'Россия' AND p.category = 'Электроника';
|
||||
```
|
||||
|
||||
**1. Выполни запрос и посмотри на план**
|
||||
|
||||
Nested Loop (cost=12.66..41.04 rows=1 width=440) (actual time=34.552..34.556 rows=0 loops=1)
|
||||
Join Filter: (u.id = o.user_id)
|
||||
Rows Removed by Join Filter: 5
|
||||
-> Seq Scan on users u (cost=0.00..1.12 rows=1 width=226) (actual time=0.012..0.015 rows=1 loops=1)
|
||||
Filter: ((country)::text = 'RU'::text)
|
||||
Rows Removed by Filter: 9
|
||||
-> Nested Loop (cost=12.66..39.84 rows=6 width=230) (actual time=34.510..34.536 rows=5 loops=1)
|
||||
-> Hash Join (cost=12.51..38.61 rows=6 width=230) (actual time=34.094..34.105 rows=5 loops=1)
|
||||
Hash Cond: (oi.product_id = p.id)
|
||||
-> Seq Scan on order_items oi (cost=0.00..22.70 rows=1270 width=20) (actual time=33.943..33.945 rows=10 loops=1)
|
||||
-> Hash (cost=12.50..12.50 rows=1 width=226) (actual time=0.047..0.048 rows=3 loops=1)
|
||||
Buckets: 1024 Batches: 1 Memory Usage: 9kB
|
||||
-> Seq Scan on products p (cost=0.00..12.50 rows=1 width=226) (actual time=0.022..0.027 rows=3 loops=1)
|
||||
Filter: ((category)::text = 'Электроника'::text)
|
||||
Rows Removed by Filter: 4
|
||||
-> Index Scan using orders_pkey on orders o (cost=0.15..0.21 rows=1 width=16) (actual time=0.084..0.084 rows=1 loops=5)
|
||||
Index Cond: (id = oi.order_id)
|
||||
Planning Time: 0.278 ms
|
||||
Execution Time: 34.691 ms
|
||||
|
||||
**2. В каком порядке база читает таблицы?**
|
||||
|
||||
Порядок чтения таблиц:
|
||||
1. users
|
||||
2. products
|
||||
3. order_items
|
||||
4. Hash Join
|
||||
5. orders
|
||||
6. Nested Loop.
|
||||
|
||||
**3. Сколько строк проходит через каждый JOIN?**
|
||||
|
||||
1. users (1)
|
||||
2. products (3)
|
||||
3. order_items (10)
|
||||
4. Hash Join (5)
|
||||
5. orders (5)
|
||||
6. Nested Loop (0)
|
||||
|
||||
**4. Можно ли оптимизировать? (Подсказка: подумай про индексы на `country` и `category`)**
|
||||
|
||||
- Создать индексы: idx_users_country, idx_products_category
|
||||
- Обновить статистику: ANALYZE всех таблиц
|
||||
|
||||
### Задача 11 — Найди и исправь
|
||||
|
||||
#### Представь, что тебе дали код коллеги:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE LOWER(u.name) LIKE '%иван%'
|
||||
AND EXTRACT(MONTH FROM o.order_date) = 1
|
||||
AND u.age + 10 > 35;
|
||||
```
|
||||
|
||||
**1. Выполни этот запрос с EXPLAIN ANALYZE**
|
||||
|
||||
Nested Loop (cost=0.00..22.35 rows=1 width=549) (actual time=0.034..0.035 rows=0 loops=1)
|
||||
Join Filter: (u.id = o.user_id)
|
||||
-> Seq Scan on users u (cost=0.00..1.20 rows=1 width=467) (actual time=0.033..0.033 rows=0 loops=1)
|
||||
Filter: ((lower((name)::text) ~~ '%иван%'::text) AND ((age + 10) > 35))
|
||||
Rows Removed by Filter: 10
|
||||
-> Seq Scan on orders o (cost=0.00..21.10 rows=4 width=82) (never executed)
|
||||
Filter: (EXTRACT(month FROM order_date) = '1'::numeric)
|
||||
Planning Time: 0.235 ms
|
||||
Execution Time: 0.045 ms
|
||||
|
||||
**2. Найди **ВСЕ** проблемы, которые мешают оптимизации**
|
||||
|
||||
- SELECT * - выбор всех колонок
|
||||
- LOWER(u.name) — функция на колонке, не используется индекс на name
|
||||
- LIKE '%иван%' — процент в начале означает "искать подстроку в любой позиции"
|
||||
- EXTRACT(MONTH FROM o.order_date) = 1 — функция на колонке
|
||||
- u.age + 10 > 35 — расчеты на колонке
|
||||
|
||||
**3. Перепиши запрос правильно**
|
||||
|
||||
```sql
|
||||
SELECT u.id, u.name, u.age, u.email, u.country, o.id AS order_id, o.order_date, o.status
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.name ILIKE '%иван%'
|
||||
AND o.order_date >= '2025-01-01'
|
||||
AND o.order_date < '2025-02-01'
|
||||
AND u.age > 25;
|
||||
```
|
||||
|
||||
**4. Сравни Execution Time до и после**
|
||||
|
||||
На малых данных разница незначительна:
|
||||
0.036 ms против 0.045ms
|
||||
|
||||
**5. Объясни, почему твой вариант лучше**
|
||||
|
||||
- Перечисление нужных полей в SELECT
|
||||
- Регистронезависимая фильтрация ILIKE
|
||||
- Убраны функции и расчеты по колонкам, не позволяющие использовать индексы
|
||||
|
||||
Все это в совокупности на больших таблицах ускорит время выполнения запроса
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
### Задание 1. Чтение и чтение
|
||||
|
||||
#### Требования:
|
||||
|
||||
- Используй таблицу `users` из примера (Alice и Bob).
|
||||
- Сформулируй, что делают две одновременные сессии, каждая выполняет **`SELECT * FROM users WHERE id = 1;`**.
|
||||
|
||||
Обе сессии читают данные пользователя Alice (id = 1) из таблицы users. Поскольку это обычные запросы на чтение, они не блокируют друг друга.
|
||||
|
||||
**1. Почему оба запроса могут выполниться сразу, без ожидания?**
|
||||
|
||||
Эти запросы выполняются параллельно, не дожидаясь друг друга, и мгновенно возвращают результат
|
||||
|
||||
### Задание 2. Два `UPDATE` одного пользователя
|
||||
|
||||
#### Требования:
|
||||
|
||||
- Сценарий: две сессии одновременно пытаются поменять email у Alice (**`id = 1`**).
|
||||
- Одна меняет на **`alice.new@example.com`**, другая — на **`alice.marketing@example.com`**.
|
||||
- Опиши, что будет делать вторая сессия, пока первая транзакция не закончилась.
|
||||
|
||||
1. Сессия 1 начинает транзакцию и выполняет UPDATE. PostgreSQL захватывает блокировку строки для id = 1.
|
||||
2. Сессия 2 пытается выполнить свой UPDATE, не может выполнить UPDATE сразу — она должна ждать.
|
||||
3. Сессия 2 переходит в состояние ожидания, пока сессия 1 не завершит свою транзакцию
|
||||
4. Сессия 1 завершает транзакцию
|
||||
5. Сессия 2 выполняет свой UPDATE уже поверх нового состояния строки.
|
||||
|
||||
**1. Почему нельзя разрешить обоим UPDATE пройти одновременно?**
|
||||
|
||||
Нельзя разрешить обоим UPDATE пройти одновременно, потому что это приведёт к потерянному обновлению, нарушению целостности данных
|
||||
|
||||
**2. Какой email в итоге останется у Alice и почему порядок COMMIT важен?**
|
||||
|
||||
Итоговый email зависит от порядка COMMIT — побеждает последняя зафиксированная транзакция
|
||||
|
||||
Порядок COMMIT определяет, какое значение останется.
|
||||
|
||||
### Задание 3. `SELECT ... FOR UPDATE`
|
||||
|
||||
#### Требования:
|
||||
|
||||
- Сценарий:
|
||||
В Сессии 1 выполняется **`BEGIN; SELECT * FROM users WHERE id = 2 FOR UPDATE;`** и пользователь Bob «забронирован» для изменений.
|
||||
- Придумай, что может сделать Сессия 2:
|
||||
- что будет с `SELECT * FROM users WHERE id = 2;`
|
||||
|
||||
Выполняется сразу — возвращает текущие данные Bob, не дожидаясь Сессии 1
|
||||
|
||||
- что будет с `UPDATE users SET country = 'FR' WHERE id = 2;`
|
||||
|
||||
Сессия 2 ждёт — блокируется до тех пор, пока Сессия 1 не выполнит COMMIT или ROLLBACK
|
||||
|
||||
**1. Почему **`SELECT`** без `FOR UPDATE` не блокируется, а `UPDATE` ждёт?**
|
||||
|
||||
Чтение (без FOR UPDATE) — не изменяет данные
|
||||
|
||||
**2. В каких задачах интернет‑магазина `SELECT ... FOR UPDATE` помогает избежать ошибок?**
|
||||
|
||||
- Резервирование товара при оформлении заказа: без FOR UPDATE могло бы случиться, что два покупателя одновременно видят последний товар в наличии, оба оформляют заказ, и товар продаётся дважды
|
||||
- Применение персональной скидки или купона: иначе юзер мог бы отправить два параллельных запроса и использовать один купон дважды
|
||||
- Изменение цены товара во время оформления корзины: не допустит, когда юзер положил товар в корзину по одной цене, а к моменту оплаты цена выросла
|
||||
|
||||
### Задание 4. Дедлок на двух пользователях
|
||||
|
||||
#### Требования:
|
||||
|
||||
- Возьми сценарий из теории:
|
||||
- Сессия 1 сначала обновляет `country` у Alice, потом пытается обновить Bob;
|
||||
- Сессия 2 сначала обновляет Bob, потом пытается обновить Alice.
|
||||
|
||||
**1. Объясни своими словами, почему возникает дедлок и что сделает база с одной из транзакций.**
|
||||
|
||||
Каждая транзакция ждёт ресурс, который удерживает другая транзакция. Ни одна из них не может продолжить выполнение.
|
||||
|
||||
Когда deadlock обнаружен, база откатывает (ROLLBACK) одну из транзакций (ту, которая выполняется меньше или которая привела к дедлоку), чтобы разрешить конфликт
|
||||
|
||||
**2. Как можно переписать код, чтобы дедлок не возникал *(подсказка: порядок обновления строк)*?**
|
||||
|
||||
```sql
|
||||
BEGIN;
|
||||
UPDATE users SET country = 'CA' WHERE id = 1;
|
||||
UPDATE users SET country = 'CA' WHERE id = 2;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
**3. Почему для интернет‑магазина это важно, если, например, массово меняют country многим пользователям скриптом?**
|
||||
|
||||
Для интернет-магазина это критично при массовых обновлениях — без единого порядка тысячи дедлоков парализуют работу системы, вызывая откаты, падение производительности и несогласованные данные.
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
### Задача 1 — Когда какой метод?
|
||||
|
||||
| **Ситуация** | **Какой JOIN выберет база?** | **Почему?** |
|
||||
| --- | --- | --- |
|
||||
| 10 пользователей, 1 млн заказов, есть индекс на orders.user_id | Nested Loop | Маленькая левая таблица (10 строк) + индекс на правой |
|
||||
| 100 000 пользователей, 100 000 заказов, нет индексов | Hash Join | Обе таблицы средние, нет индексов |
|
||||
| Обе таблицы отсортированы по ключу, есть индексы | Merge Join | Данные уже отсортированы — можно пройтись за один проход |
|
||||
| Маленькая таблица (5 строк) соединяется с большой (1 млн строк), нет индекса | Hash Join | Hash Join просканирует большую таблицу 1 раз |
|
||||
|
||||
### Задача 2 — Размер таблиц имеет значение
|
||||
|
||||
#### Требования:
|
||||
|
||||
Создай тестовые данные:
|
||||
|
||||
```sql
|
||||
-- Маленькая таблица (10 строк)
|
||||
CREATE TABLE small_users AS
|
||||
SELECT generate_series(1, 10) AS id, 'User ' || generate_series(1, 10) AS name;
|
||||
|
||||
-- Большая таблица (100 000 строк)
|
||||
CREATE TABLE big_orders AS
|
||||
SELECT
|
||||
generate_series(1, 100000) AS id,
|
||||
(random() * 9 + 1)::int AS user_id,
|
||||
CURRENT_DATE - (random() * 365)::int AS order_date;
|
||||
```
|
||||
|
||||
Теперь выполни:
|
||||
|
||||
```sql
|
||||
-- Вариант 1: Без индекса
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, COUNT(o.id)
|
||||
FROM small_users u
|
||||
JOIN big_orders o ON u.id = o.user_id
|
||||
GROUP BY u.name;
|
||||
|
||||
-- Вариант 2: С индексом
|
||||
CREATE INDEX idx_big_orders_user_id ON big_orders(user_id);
|
||||
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, COUNT(o.id)
|
||||
FROM small_users u
|
||||
JOIN big_orders o ON u.id = o.user_id
|
||||
GROUP BY u.name;
|
||||
```
|
||||
|
||||
**1. Какой метод `JOIN` используется в каждом случае?**
|
||||
|
||||
- Без индекса — Hash Join (строит хеш по маленькой таблице, сканирует большую)
|
||||
- С индексом — Nested Loop (для каждого пользователя делает Index Scan в таблице заказов)
|
||||
|
||||
**2. Почему с индексом база может выбрать `Nested Loop`?**
|
||||
|
||||
Потому что левая таблица (small_users) очень маленькая (10 строк), а индекс на big_orders.user_id позволяет мгновенно находить заказы каждого пользователя
|
||||
|
||||
**3. Насколько быстрее стало с индексом?**
|
||||
|
||||
С индексом в 10-20 раз быстрее (~2.5 мс против ~45 мс). Index Scan читает только нужные строки (не все 100 000 заказов)
|
||||
|
||||
### Задача 3 — Влияние сортировки
|
||||
|
||||
#### Требования:
|
||||
|
||||
Выполни три варианта:
|
||||
|
||||
```sql
|
||||
-- Вариант 1: Без ORDER BY
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id;
|
||||
|
||||
-- Вариант 2: С ORDER BY по ключу соединения
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
ORDER BY u.id, o.user_id;
|
||||
|
||||
-- Вариант 3: С ORDER BY по другому полю
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
ORDER BY o.order_date;
|
||||
```
|
||||
|
||||
**1. Изменился ли тип `JOIN` во втором варианте?**
|
||||
|
||||
Тип JOIN НЕ ИЗМЕНИЛСЯ — во всех вариантах используется Hash Join
|
||||
|
||||
**2. Появился ли `Merge Join`?**
|
||||
|
||||
Merge Join НЕ ПОЯВИЛСЯ — для Merge Join нужен индекс на orders.user_id, чтобы заказы были отсортированы по ключу соединения.
|
||||
|
||||
**3. Почему `ORDER BY` может повлиять на выбор `JOIN`?**
|
||||
|
||||
ORDER BY влияет на выбор JOIN ТОЛЬКО при наличии индексов на ключах соединения. Если бы индекс на orders.user_id существовал, ORDER BY по ключу соединения привёл бы к появлению Merge Join, который использует отсортированные данные
|
||||
|
||||
### Задача 4 — Найди узкое место
|
||||
|
||||
#### Требования:
|
||||
|
||||
Дан медленный запрос:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
u.name,
|
||||
p.name AS product_name,
|
||||
oi.quantity,
|
||||
o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE u.country = 'Россия';
|
||||
```
|
||||
|
||||
**1. Выполни с `EXPLAIN ANALYZE`**
|
||||
|
||||
**2. Посмотри, какие типы `JOIN` используются**
|
||||
|
||||
Все JOIN — Hash Join
|
||||
|
||||
**3. Есть ли `Seq Scan`? Если да — где?**
|
||||
|
||||
Да, есть Seq Scan на всех таблицах (таблицы очень маленькие (7-10 строк), и PostgreSQL считает, что читать всю таблицу последовательно дешевле, чем использовать индекс)
|
||||
|
||||
**4. Предложи, какие индексы нужно создать**
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_users_country ON users(country);
|
||||
CREATE INDEX idx_orders_user_id ON orders(user_id);
|
||||
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
|
||||
CREATE INDEX idx_order_items_product_id ON order_items(product_id);
|
||||
```
|
||||
|
||||
- для фильтрации по стране
|
||||
- для ускорения поиска заказов конкретного пользователя
|
||||
- для ускорения поиска позиций заказа
|
||||
- для ускорения поиска товара в позициях
|
||||
|
||||
**5. Создай индексы и проверь, изменились ли типы `JOIN`**
|
||||
|
||||
После создания индексов (на больших данных) типы JOIN изменятся с Hash Join на Nested Loop, а сканирование users станет Index Scan.
|
||||
|
||||
### Задача 5 — Многоступенчатый JOIN
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT
|
||||
u.name,
|
||||
COUNT(DISTINCT o.id) AS orders_count,
|
||||
COUNT(DISTINCT oi.product_id) AS products_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
LEFT JOIN order_items oi ON o.id = oi.order_id
|
||||
WHERE u.country = 'Россия'
|
||||
GROUP BY u.name;
|
||||
```
|
||||
|
||||
**1. Посмотри на план выполнения**
|
||||
|
||||
**2. Какие методы `JOIN` используются на каждом уровне?**
|
||||
|
||||
План выполнения показывает: оба JOIN — Hash Left Join, все таблицы читаются через Seq Scan (так как данные маленькие).
|
||||
|
||||
**3. Почему для первого `JOIN` один метод, а для второго — другой?**
|
||||
|
||||
Оба JOIN используют Hash Left Join, потому что нет индексов на внешних ключах, а таблицы маленькие, чтобы построить хеш-таблицы в памяти.
|
||||
|
||||
**4. Сколько строк проходит через каждый `JOIN`?**
|
||||
|
||||
1. После фильтра country = 'RU': users — 1 строка
|
||||
2. После первого LEFT JOIN (users и orders) — 1 строка
|
||||
3. После второго LEFT JOIN (orders и order_items) — 1 строка (у заказа 1 позиция)
|
||||
4. Результат: 1 строка с агрегатами (orders_count=1, products_count=1)
|
||||
|
||||
### Задача 6 — `LEFT JOIN` vs `INNER JOIN`
|
||||
|
||||
#### Требования:
|
||||
|
||||
Сравни два запроса:
|
||||
|
||||
```sql
|
||||
-- Вариант A: INNER JOIN
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
INNER JOIN orders o ON u.id = o.user_id;
|
||||
|
||||
-- Вариант B: LEFT JOIN
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id;
|
||||
```
|
||||
|
||||
**1. Одинаковый ли тип физического JOIN используется?**
|
||||
|
||||
Нет, типы JOIN разные: INNER JOIN использует Hash Join, LEFT JOIN использует Hash Left Join
|
||||
|
||||
**2. Есть ли разница в Execution Time?**
|
||||
|
||||
Небольшая, LEFT JOIN медленнее на ~0.003 мс
|
||||
|
||||
**3. Почему LEFT JOIN может быть медленнее?**
|
||||
|
||||
- Должен сохранять ВСЕ строки из левой таблицы (даже без соответствия) и добавлять NULL
|
||||
- Возвращает больше строк
|
||||
|
||||
### Задача 7 — Порядок таблиц в JOIN
|
||||
|
||||
#### Требования:
|
||||
|
||||
Сравни:
|
||||
|
||||
```sql
|
||||
-- Вариант 1: users JOIN orders
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id;
|
||||
|
||||
-- Вариант 2: orders JOIN users
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.order_date
|
||||
FROM orders o
|
||||
JOIN users u ON o.user_id = u.id;
|
||||
```
|
||||
|
||||
**1. Изменился ли тип `JOIN`?**
|
||||
|
||||
Тип JOIN НЕ ИЗМЕНИЛСЯ — в обоих случаях используется Hash Join
|
||||
|
||||
**2. Изменился ли порядок чтения таблиц в плане?**
|
||||
|
||||
Порядок чтения таблиц в плане НЕ ИЗМЕНИЛСЯ — оптимизатор в обоих случаях начал с orders (меньшей таблицы)
|
||||
|
||||
**3. Одинаковое ли `Execution Time`?**
|
||||
|
||||
Execution Time ОДИНАКОВОЕ, потому что планы идентичны
|
||||
|
||||
**4. Почему оптимизатор может выбрать разный порядок?**
|
||||
|
||||
Оптимизатор может выбрать разный порядок на основе COST: учитывает размер таблиц, фильтрацию, наличие индексов и тип JOIN
|
||||
|
||||
### Задача 8 — Спровоцируй Merge Join
|
||||
|
||||
#### Требования:
|
||||
|
||||
Напиши запрос, который **гарантированно** использует Merge Join.
|
||||
|
||||
**Подсказки:**
|
||||
- Используй ORDER BY
|
||||
- Убедись, что есть индексы на ключи соединения
|
||||
- Обе таблицы должны быть достаточно большими
|
||||
|
||||
Создал таблицы на 100 000 + строк с помощью ИИ, не стал сюда копировать
|
||||
|
||||
```sql
|
||||
EXPLAIN
|
||||
SELECT u.id, u.name, o.id, o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
ORDER BY u.id, o.user_id;
|
||||
```
|
||||
|
||||
**Фишка в том, что Merge Join будет использоваться, если:**
|
||||
|
||||
- Индекс на users.id (первичный ключ — есть)
|
||||
- Индекс на orders.user_id (нужно создать)
|
||||
- ORDER BY u.id, o.user_id — совпадает с ключами JOIN
|
||||
- Достаточно большой размер таблиц
|
||||
|
||||
### Задача 9 — Когда Nested Loop лучше Hash Join?
|
||||
|
||||
#### Требования:
|
||||
|
||||
Придумай ситуацию (размер таблиц, наличие индексов, условия), когда:
|
||||
|
||||
**1. Nested Loop будет быстрее Hash Join**
|
||||
|
||||
- Левая таблица очень маленькая (1-100 строк), Nested Loop сделает мало итераций
|
||||
- Есть индекс на правой таблице по ключу JOIN
|
||||
- Правая таблица очень большая (миллионы строк)
|
||||
|
||||
**2. Hash Join будет быстрее Nested Loop**
|
||||
|
||||
- Обе таблицы большие (тысячи+ строк)
|
||||
- Нет индекса на правой таблице
|
||||
- Достаточно памяти для хеш-таблицы
|
||||
|
||||
**3. Создай тестовые данные и проверь свою гипотезу!**
|
||||
|
||||
Создавал с помощью ИИ таблицы и принудительно отключал у оптимизатора варианты физического джойна (то есть сперва обе таблицы через Nested Loop, потом через Hash Join, затем так же другие две таблицы), вывел результаты в табличку:
|
||||
|
||||
| Сценарий | Тип JOIN | Время |
|
||||
|----------|----------|-------|
|
||||
| маленькая + большая + индекс | Nested Loop | ~180 ms |
|
||||
| маленькая + большая + индекс | Hash Join | ~400 ms |
|
||||
| большая + большая + нет индекса | Hash Join | ~85 ms |
|
||||
| большая + большая + нет индекса | Nested Loop | ~12500 ms |
|
||||
|
||||
Гипотезы подтвердились: Nested Loop хорош для 'маленькая + большая + индекс', Hash Join хорош для 'большая + большая + нет индекса'. Но лучше всё-таки не отключать оптимизатору вариант выбора физического джойна)))
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
### Задача 1 — Одинаковые ли запросы?
|
||||
|
||||
#### Требования:
|
||||
|
||||
- Напиши, будут ли эти два запроса давать одинаковый результат? Объясни почему.
|
||||
|
||||
```sql
|
||||
-- Вариант А
|
||||
SELECT name, age
|
||||
FROM users
|
||||
WHERE country = 'RU' AND age > 25;
|
||||
|
||||
-- Вариант Б
|
||||
SELECT name, age
|
||||
FROM users
|
||||
WHERE age > 25 AND country = 'RU';
|
||||
```
|
||||
|
||||
Оба запроса дадут абсолютно одинаковый результат. В обоих случаях будут выбраны те пользователи, у которых одновременно country = 'RU' И age > 25
|
||||
|
||||
**1. Какой из них будет работать быстрее?**
|
||||
|
||||
Они будут работать с одинаковой скоростью — оптимизатор запросов сам анализирует условия и выбирает оптимальный порядок выполнения, игнорируя порядок в тексте запроса.
|
||||
|
||||
### Задача 2 — JOIN в разном порядке
|
||||
|
||||
#### Требования:
|
||||
|
||||
Напиши два варианта запроса: сначала пользователи JOIN заказы, потом наоборот.
|
||||
|
||||
**1. Будут ли результаты одинаковыми?**
|
||||
|
||||
Если бы использовался INNER JOIN: результаты были бы идентичными
|
||||
|
||||
**2. Твой вариант 1**
|
||||
|
||||
```sql
|
||||
SELECT u.id, u.name, o.id AS order_id, o.order_date, o.status
|
||||
FROM dmezentsev.users u
|
||||
JOIN dmezentsev.orders o ON u.id = o.user_id;
|
||||
```
|
||||
|
||||
**3. Твой вариант 2**
|
||||
|
||||
```sql
|
||||
SELECT u.id, u.name, o.id AS order_id, o.order_date, o.status
|
||||
FROM dmezentsev.orders o
|
||||
LEFT JOIN dmezentsev.users u ON o.user_id = u.id;
|
||||
```
|
||||
|
||||
**4. Какой таблицей оптимизатор начнёт чаще всего? Почему?**
|
||||
|
||||
Оптимизатор PostgreSQL начинает с меньшей таблицы, потому что это снижает стоимость JOIN-операции
|
||||
|
||||
### Задача 3 — Первое знакомство с EXPLAIN
|
||||
|
||||
#### Требования:
|
||||
|
||||
Выполни команду:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT name, email
|
||||
FROM users
|
||||
WHERE country = 'RU';
|
||||
```
|
||||
|
||||
**1. Какой тип сканирования использует база (Seq Scan, Index Scan)?**
|
||||
|
||||
Seq Scan (последовательное сканирование) — база будет читать всю таблицу users подряд, одну строку за другой, проверяя условие country = 'RU'.
|
||||
|
||||
**2. Сколько строк она прочитала?**
|
||||
|
||||
База прочитала все 10 строк таблицы, проверила каждую, но вернула только 1.
|
||||
|
||||
**3. Сколько времени это заняло?**
|
||||
|
||||
0.025–0.050 ms
|
||||
|
||||
### Задача 4 — Сравни два плана
|
||||
|
||||
#### Требования:
|
||||
|
||||
Выполни оба запроса с EXPLAIN ANALYZE:
|
||||
|
||||
```sql
|
||||
-- Запрос А
|
||||
EXPLAIN ANALYZE
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE country = 'RU';
|
||||
|
||||
-- Запрос Б
|
||||
EXPLAIN ANALYZE
|
||||
SELECT name, email
|
||||
FROM users
|
||||
WHERE country = 'RU';
|
||||
```
|
||||
|
||||
**1. Почему второй запрос может быть быстрее первого?**
|
||||
|
||||
Второй запрос (SELECT name, email) может быть быстрее первого (SELECT *), потому что читает меньше данных (только две колонки вместо всех).
|
||||
|
||||
### Задача 5 — JOIN с EXPLAIN
|
||||
|
||||
#### Требования:
|
||||
|
||||
Посмотри план выполнения JOIN:
|
||||
|
||||
```sql
|
||||
EXPLAIN ANALYZE
|
||||
SELECT u.name, o.id, o.order_date
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.country = 'RU';
|
||||
```
|
||||
|
||||
**1. С какой таблицы начала читать база? Почему именно с неё?**
|
||||
|
||||
База начнёт читать с таблицы users, потому что у неё есть фильтр WHERE country = 'RU', который сильно уменьшает количество строк
|
||||
|
||||
### Задача 6 — Найди узкое место
|
||||
|
||||
#### Требования:
|
||||
|
||||
У тебя есть запрос:
|
||||
|
||||
```sql
|
||||
SELECT u.name, COUNT(o.id) AS orders_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.country = 'Россия'
|
||||
AND LOWER(u.name) LIKE '%алекс%'
|
||||
GROUP BY u.name;
|
||||
```
|
||||
|
||||
**1. Найди как минимум 2 проблемы, которые замедляют запрос. Как их исправить?**
|
||||
|
||||
- `LOWER(u.name)` — функция на колонке, которая также блокирует использование индекса и требует вычислений для каждой строки
|
||||
- `LIKE '%алекс%'` не использует индекс, требуя полного сканирования
|
||||
|
||||
**Исправить:**
|
||||
|
||||
- использовать `ILIKE` без `LOWER`
|
||||
- хранить уже нормализованные данные
|
||||
|
||||
### Задача 7 — Оптимизируй JOIN
|
||||
|
||||
#### Требования:
|
||||
|
||||
Дан запрос:
|
||||
|
||||
```sql
|
||||
SELECT u.name, p.name AS product_name, oi.quantity
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE u.country = 'Россия';
|
||||
```
|
||||
|
||||
**1. В каком порядке база будет читать таблицы? Почему? Можно ли как-то ускорить этот запрос?**
|
||||
|
||||
База начнёт читать с таблицы users, применяя фильтр country = 'Россия' ('RU'), потому что это сильно уменьшает количество строк.
|
||||
|
||||
**Ускорить можно:**
|
||||
|
||||
- создать индексы
|
||||
- использовать материализованные представления
|
||||
|
||||
### Задача 8 — Сравни подходы
|
||||
|
||||
#### Требования:
|
||||
|
||||
Какой запрос будет работать быстрее и почему?
|
||||
|
||||
```sql
|
||||
-- Вариант А
|
||||
SELECT name, price
|
||||
FROM products
|
||||
WHERE category = 'Электроника'
|
||||
AND price > 50000;
|
||||
|
||||
-- Вариант Б
|
||||
SELECT *
|
||||
FROM products
|
||||
WHERE category = 'Электроника'
|
||||
AND price > 50000;
|
||||
```
|
||||
|
||||
**1. Запусти оба с EXPLAIN ANALYZE и сравни время выполнения.**
|
||||
|
||||
Вариант А будет быстрее, меньше данных нужно прочитать
|
||||
|
||||
- `SELECT name, price` читает 2 колонки (name VARCHAR(100), price DECIMAL)
|
||||
- `SELECT *` читает все 5 колонок (id, name, category, price, in_stock)
|
||||
|
||||
### Задача 9 — Найди и исправь
|
||||
|
||||
#### Требования:
|
||||
|
||||
Представь, что тебе дали код коллеги:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE EXTRACT(YEAR FROM created_at) = 2026
|
||||
AND LOWER(country) = 'россия'
|
||||
AND age + 10 > 35;
|
||||
```
|
||||
|
||||
**1. Найди ВСЕ проблемы в этом запросе**
|
||||
|
||||
- `EXTRACT(YEAR FROM created_at)` делает невозможным использование индекса
|
||||
- `LOWER(country)` — функция на колонке, блокирующая использование индекса
|
||||
- Вычисление в условии `age + 10 > 35`
|
||||
|
||||
**2. Перепиши его правильно**
|
||||
|
||||
```sql
|
||||
SELECT id, name, email, country, age, created_at, is_active
|
||||
FROM dmezentsev.users
|
||||
WHERE created_at >= '2026-01-01'
|
||||
AND created_at < '2027-01-01'
|
||||
AND country = 'RU'
|
||||
AND age > 25;
|
||||
```
|
||||
|
||||
**3. Объясни, почему твой вариант лучше**
|
||||
|
||||
Он лучше, потому что может использовать индексы, не выполняет лишних вычислений, возвращает только нужные колонки и фактически найдёт данные (с правильным значением страны).
|
||||
|
||||
### Задача 10 — Исследуй реальные данные
|
||||
|
||||
#### Требования:
|
||||
|
||||
Создай таблицу с 10 000 записей пользователей (можно сгенерировать через функцию `generate_series`).
|
||||
|
||||
Сравни время выполнения:
|
||||
|
||||
```sql
|
||||
-- Запрос 1
|
||||
SELECT * FROM users WHERE age > 25;
|
||||
|
||||
-- Запрос 2
|
||||
SELECT name, email FROM users WHERE age > 25;
|
||||
```
|
||||
|
||||
**1. Используй EXPLAIN ANALYZE и посмотри, насколько второй запрос быстрее первого на большом объёме данных.**
|
||||
|
||||
Второй запрос (SELECT name, email) будет быстрее на 30-50% на 10 000 записях, потому что читает меньше данных (только 2 колонки вместо всех), ну и поэтому передаёт меньше байт по сети
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
### Задача 1 — Запрос с группировкой
|
||||
|
||||
```sql
|
||||
SELECT country, COUNT(*) AS users_count
|
||||
FROM users
|
||||
WHERE is_active = true
|
||||
GROUP BY country
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY users_count DESC;
|
||||
```
|
||||
|
||||
**1. Когда применится `WHERE` — до или после `GROUP BY`?**
|
||||
|
||||
WHERE применяется ДО GROUP BY
|
||||
|
||||
**2. Когда применится `HAVING` — до или после `GROUP BY`?**
|
||||
|
||||
HAVING применяется ПОСЛЕ GROUP BY
|
||||
|
||||
**3. Можно ли использовать алиас `users_count` в `HAVING`?**
|
||||
|
||||
HAVING выполняется до SELECT. Алиас users_count создаётся только на этапе SELECT. Поэтому на момент выполнения HAVING алиас ещё не существует.
|
||||
|
||||
**4. Можно ли использовать алиас `users_count` в `ORDER BY`?**
|
||||
|
||||
ORDER BY выполняется после SELECT. На этом этапе алиас users_count уже создан и доступен для использования.
|
||||
|
||||
### Задача 2 — JOIN с фильтрацией
|
||||
|
||||
```sql
|
||||
SELECT u.name, o.order_date, o.status
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.country = 'Россия' AND o.status = 'completed'
|
||||
ORDER BY o.order_date DESC;
|
||||
```
|
||||
|
||||
**1. Опиши порядок выполнения своими словами (как будто объясняешь другу-новичку).**
|
||||
|
||||
Порядок выполнения:
|
||||
База сначала фильтрует пользователей по стране (WHERE u.country = 'Россия'), оставляя только пользователей из России. Затем соединяет (JOIN) этих пользователей с таблицей заказов, находя их заказы. После этого фильтрует заказы по статусу (WHERE o.status = 'completed'). Затем выбирает нужные колонки (SELECT): имя, дату заказа и статус заказа, и сортирует (ORDER BY).
|
||||
|
||||
### Задача 3 — Найди ошибку
|
||||
|
||||
Дан запрос:
|
||||
|
||||
```sql
|
||||
SELECT country, AVG(age) AS avg_age
|
||||
FROM users
|
||||
GROUP BY country
|
||||
HAVING country = 'Россия';
|
||||
```
|
||||
|
||||
**1. Запрос работает, но написан **неэффективно**. Почему?**
|
||||
|
||||
HAVING используется для фильтрации по колонке country, которая не является агрегированной. Для фильтрации по обычным колонкам (не результатам агрегации) предназначен WHERE.
|
||||
|
||||
**2. Перепиши его правильно, используя WHERE.**
|
||||
|
||||
```sql
|
||||
SELECT country, AVG(age) AS avg_age
|
||||
FROM users
|
||||
WHERE country = 'Россия'
|
||||
GROUP BY country;
|
||||
```
|
||||
|
||||
**3. Объясни, почему твой вариант быстрее.**
|
||||
|
||||
Потому что WHERE применяется до группировки: база сначала отфильтровывает только нужные строки (страна 'RU'), затем группирует и вычисляет средний возраст только для них. Это уменьшает объём данных для группировки, база может использовать индекс на country и не делает лишних вычислений
|
||||
|
||||
### Задача 4 — Исправь запрос
|
||||
|
||||
```sql
|
||||
SELECT category, AVG(price) AS avg_price
|
||||
FROM products
|
||||
GROUP BY category
|
||||
HAVING category = 'Электроника' AND AVG(price) > 20000;
|
||||
```
|
||||
|
||||
**1. Перепиши запрос так, чтобы часть условий была в `WHERE`.**
|
||||
|
||||
```sql
|
||||
SELECT category, AVG(price) AS avg_price
|
||||
FROM products
|
||||
WHERE category = 'Электроника'
|
||||
GROUP BY category
|
||||
HAVING AVG(price) > 20000;
|
||||
```
|
||||
|
||||
**2. Объясни, почему это быстрее.**
|
||||
|
||||
Это быстрее, потому что WHERE отсекает ненужные категории до группировки
|
||||
|
||||
**3. Нарисуй схему: сколько строк будет обработано в каждом случае.**
|
||||
|
||||
Исходный запрос обрабатывает 4 группы (7 строк) — потом 4 вычисления средней цены по каждой категории.
|
||||
Исправленный запрос фильтрует по условию — обрабатывает только 1 группу (3 строки) — 1 вычисление средней цены
|
||||
|
||||
### Задача 5 — Исправь все ошибки
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
country,
|
||||
COUNT(*) AS users_count
|
||||
FROM users
|
||||
WHERE users_count > 1
|
||||
GROUP BY country
|
||||
ORDER BY country;
|
||||
```
|
||||
|
||||
**1. Найди ошибку и исправь запрос.**
|
||||
|
||||
В запросе используется WHERE users_count > 1, но алиас users_count создаётся в SELECT, который выполняется после WHERE. Кроме того, WHERE не может фильтровать по агрегатным функциям COUNT(*)
|
||||
|
||||
```sql
|
||||
SELECT country, COUNT(*) AS users_count
|
||||
FROM users
|
||||
GROUP BY country
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY country;
|
||||
```
|
||||
|
||||
### Задача 6 — Полный цикл выполнения
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
u.country,
|
||||
COUNT(o.id) AS orders_count,
|
||||
AVG(o.id) AS avg_order_id
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.is_active = true
|
||||
GROUP BY u.country
|
||||
HAVING COUNT(o.id) > 1
|
||||
ORDER BY orders_count DESC
|
||||
LIMIT 3;
|
||||
```
|
||||
|
||||
**1. Опиши ПОЛНЫЙ порядок выполнения (все 7 шагов) и объясни, что происходит на каждом.**
|
||||
|
||||
- **FROM** — читаем таблицу users и готовим LEFT JOIN с orders
|
||||
- **JOIN** — соединяем с заказами, у пользователей без заказов — NULL
|
||||
- **WHERE** — фильтруем активных пользователей (is_active = true)
|
||||
- **GROUP BY** — группируем по стране
|
||||
- **HAVING** — оставляем только группы с количеством заказов > 1
|
||||
- **SELECT** — вычисляем COUNT(o.id) и AVG(o.id) для каждой группы
|
||||
- **ORDER BY** — сортируем по убыванию orders_count
|
||||
- **LIMIT** — берём первые 3 строки
|
||||
|
||||
### Задача 7 — Оптимизация
|
||||
|
||||
Сравни два запроса:
|
||||
|
||||
```sql
|
||||
-- Запрос А
|
||||
SELECT category, COUNT(*)
|
||||
FROM products
|
||||
GROUP BY category
|
||||
HAVING category = 'Электроника';
|
||||
|
||||
-- Запрос Б
|
||||
SELECT category, COUNT(*)
|
||||
FROM products
|
||||
WHERE category = 'Электроника'
|
||||
GROUP BY category;
|
||||
```
|
||||
|
||||
**1. Какой запрос быстрее? Почему?**
|
||||
|
||||
Запрос Б быстрее. Причина: фильтр WHERE category = 'Электроника' применяется до группировки
|
||||
|
||||
**2. Нарисуй таблицу: на каком шаге сколько строк обрабатывается в каждом запросе.**
|
||||
|
||||
| Показатель | Запрос А | Запрос Б |
|
||||
|------------|----------|----------|
|
||||
| Прочитано строк | 7 | 7 |
|
||||
| Создано групп | 4 | 1 |
|
||||
| Вычислено COUNT | 4 раза | 1 раз |
|
||||
|
||||
**3. Выполни оба с `EXPLAIN ANALYZE` и сравни `Execution Time`.**
|
||||
|
||||
У запроса А Rows Removed by Filter: 3 после группировки, у запроса Б фильтр применяется при сканировании таблицы
|
||||
|
||||
### Задача 8 — Многоэтапная фильтрация
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
u.name,
|
||||
COUNT(o.id) AS orders_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.country = 'Россия'
|
||||
AND u.age > 25
|
||||
AND o.status = 'completed'
|
||||
GROUP BY u.name
|
||||
HAVING COUNT(o.id) >= 2
|
||||
ORDER BY orders_count DESC;
|
||||
```
|
||||
|
||||
**1. Когда применяются условия `WHERE`?**
|
||||
|
||||
Условия WHERE применяются до группировки — фильтруют отдельные строки (пользователей и заказы) перед тем, как они попадут в GROUP BY
|
||||
|
||||
**2. Когда применяется условие `HAVING`?**
|
||||
|
||||
Условие HAVING применяется после группировки — фильтрует уже сформированные группы (пользователей) по результату агрегации (COUNT(o.id)).
|
||||
|
||||
**3. В чём разница между фильтрацией по `o.status` в `WHERE` и по `COUNT(o.id)` в `HAVING`?**
|
||||
|
||||
WHERE o.status = 'completed' фильтрует отдельные заказы до группировки и (из-за LEFT JOIN) отсекает пользователей без подходящих заказов. HAVING COUNT(o.id) >= 2 фильтрует пользователей после группировки по количеству их заказов.
|
||||
|
||||
### Задача 9 — JOIN и ORDER BY
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
u.name,
|
||||
p.name AS product_name,
|
||||
p.price
|
||||
FROM users u
|
||||
JOIN orders o ON u.id = o.user_id
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE u.country = 'Россия'
|
||||
ORDER BY p.price DESC
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
**1. На каком этапе применяется `ORDER BY`?**
|
||||
|
||||
ORDER BY применяется на предпоследнем этапе — после всех JOIN, WHERE и SELECT, но до LIMIT.
|
||||
|
||||
**2. Почему `LIMIT` идёт после `ORDER BY`, а не до?**
|
||||
|
||||
LIMIT идёт после ORDER BY, потому что должен взять первые N строк уже отсортированного результата.
|
||||
|
||||
**3. Что будет, если поменять их местами (технически невозможно, но подумай концептуально)?**
|
||||
|
||||
Концептуально при LIMIT, а потом ORDER BY сначала взяли бы случайные строки (в физическом порядке), а потом их отсортировали бы
|
||||
|
||||
### Задача 10 — Напиши запрос с нуля
|
||||
|
||||
Нужно получить:
|
||||
|
||||
- Список категорий товаров
|
||||
- Количество товаров в каждой категории
|
||||
- Среднюю цену товаров в категории
|
||||
- Только те категории, где средняя цена > 15000
|
||||
- Только те категории, где товаров >= 2
|
||||
- Отсортировать по средней цене (от большей к меньшей)
|
||||
- Взять только топ-3
|
||||
|
||||
**1. Напиши запрос**
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
category,
|
||||
COUNT(*) AS products_count,
|
||||
AVG(price) AS avg_price
|
||||
FROM dmezentsev.products
|
||||
GROUP BY category
|
||||
HAVING AVG(price) > 15000
|
||||
AND COUNT(*) >= 2
|
||||
ORDER BY avg_price DESC
|
||||
LIMIT 3;
|
||||
```
|
||||
|
||||
**2. Опиши порядок выполнения**
|
||||
|
||||
- **FROM** — читаем таблицу products (7 строк)
|
||||
- **GROUP BY** — группируем по category (4 группы)
|
||||
- **HAVING** — применяем оба условия (фильтруем группы)
|
||||
- **SELECT** — вычисляем COUNT(*) и AVG(price)
|
||||
- **ORDER BY** — сортируем по avg_price DESC
|
||||
- **LIMIT** — берём первые 3 строки
|
||||
|
||||
**3. Объясни, почему использовал `WHERE` или `HAVING` для каждого условия**
|
||||
|
||||
В этом запросе нет условий WHERE, потому что нет фильтрации по отдельным строкам. Оба условия работают с результатами агрегации (AVG(price), COUNT(*)), поэтому фильтруются уже в HAVING
|
||||
|
||||
**4. Выполни с `EXPLAIN ANALYZE` и посмотри план**
|
||||
|
||||
```
|
||||
Seq Scan on products -- Читаем все 7 строк таблицы
|
||||
HashAggregate -- Группируем по category (4 группы)
|
||||
Filter: (avg(price) > 15000 AND count(*) >= 2) -- Применяем HAVING
|
||||
Rows Removed by Filter: 3 -- 3 группы отброшены
|
||||
Sort -- Сортируем результат по avg_price DESC
|
||||
Limit -- Ограничиваем 3 строками
|
||||
```
|
||||
|
||||
### Задача 11 — Подзапросы и порядок
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
u.name,
|
||||
u.age,
|
||||
(SELECT COUNT(*)
|
||||
FROM orders o
|
||||
WHERE o.user_id = u.id) AS orders_count
|
||||
FROM users u
|
||||
WHERE u.country = 'Россия'
|
||||
ORDER BY orders_count DESC;
|
||||
```
|
||||
|
||||
**1. Когда выполняется подзапрос в `SELECT`?**
|
||||
|
||||
Подзапрос в SELECT выполняется для каждой строки после WHERE u.country = 'Россия', но до ORDER BY
|
||||
|
||||
**2. Можно ли использовать алиас `orders_count` в `WHERE`?**
|
||||
|
||||
Алиас orders_count нельзя использовать в WHERE, потому что WHERE выполняется до SELECT
|
||||
|
||||
**3. Можно ли использовать его в `ORDER BY`?**
|
||||
|
||||
Алиас orders_count можно использовать в ORDER BY, так как ORDER BY выполняется после SELECT.
|
||||
|
||||
**4. Перепиши этот запрос через `JOIN`. Какой вариант быстрее? Проверь с `EXPLAIN ANALYZE`!**
|
||||
|
||||
```sql
|
||||
SELECT u.name, u.age, COUNT(o.id) AS orders_count
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.country = 'RU'
|
||||
GROUP BY u.id, u.name, u.age
|
||||
ORDER BY orders_count DESC;
|
||||
```
|
||||
|
||||
На маленьких данных разница незаметна. Предположу, что на больших таблицах JOIN быстрее, потому что подзапрос выполняется для каждого пользователя, а JOIN — за один проход.
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
### Задание 1. Оформление заказа
|
||||
|
||||
#### Требования:
|
||||
|
||||
- Используй таблицы пет‑проекта: `users`, `orders`, `order_items`, `products`.
|
||||
- Заказ: пользователь покупает два разных товара.
|
||||
- Сформулируй транзакцию, которая за один раз:
|
||||
- создаёт запись в `orders`;
|
||||
- добавляет все позиции в `order_items`;
|
||||
- уменьшает остаток товаров в `products`.
|
||||
|
||||
**1. Что в данных пойдёт не так, если часть шагов выполнится, а часть нет?**
|
||||
|
||||
- Создан заказ, но не добавлены товары: заказ без позиций. Пользователь думает, что оформил заказ, а товары не зарезервированы.
|
||||
- Добавлены товары в order_items, но не создан заказ: записи о товарах висят без привязки к заказу
|
||||
|
||||
**2. Почему оформление заказа — типичный пример «либо всё, либо ничего»?**
|
||||
|
||||
Оформление заказа требует атомарности, потому что заказ, товары и складские остатки должны оставаться согласованными — иначе бизнес-логика нарушается (деньги списаны, а товара нет, или товар уехал, а заказ не оформлен).
|
||||
|
||||
### Задание 2. Ошибка посередине
|
||||
|
||||
#### Требования:
|
||||
|
||||
- Сценарий:
|
||||
- заказ создали;
|
||||
- одну позицию заказа добавили;
|
||||
- на второй позиции выяснилось, что товара нет на складе.
|
||||
- Сформулируй, что должна сделать транзакция в такой ситуации (что должно сохраниться, а что откатиться).
|
||||
|
||||
При оформлении заказа из двух позиций, когда на второй позиции выясняется отсутствие товара на складе, транзакция должна полностью откатить все изменения (выполнить ROLLBACK).
|
||||
|
||||
**1. Какие «битые» заказы можно получить без отката (`ROLLBACK`)?**
|
||||
|
||||
- Заказ без позиций — запись в orders есть, но в order_items пусто.
|
||||
- Заказ с частичным наполнением — первая позиция добавлена, вторая нет. Заказ существует, но неполный.
|
||||
- Заказ с некорректным статусом — статус created, но часть операций не выполнена.
|
||||
- Заказ с нарушенной суммой — если считается общая сумма заказа по order_items, она не будет соответствовать ожидаемой (вторая позиция отсутствует, но пользователь мог уже оплатить полную стоимость).
|
||||
|
||||
**2. Как такие ошибки повлияют на отчёты и витрины по выручке?**
|
||||
|
||||
- Завышение количества заказов
|
||||
- Искажение среднего чека
|
||||
- Неверный расчет выручки
|
||||
|
||||
### Задание 3. Один `UPDATE` как транзакция
|
||||
|
||||
#### Требования:
|
||||
|
||||
- Возьми простое действие в магазине:
|
||||
- смена `email` пользователя в `users`;
|
||||
- изменение цены товара в `products`.
|
||||
- Объясни, почему даже такое действие должно выполняться как маленькая транзакция.
|
||||
|
||||
Даже одиночный UPDATE выполняется как атомарная транзакция, что гарантирует «либо всё, либо ничего»
|
||||
|
||||
**1. Что может случиться, если во время изменения цены пропадёт соединение с базой?**
|
||||
|
||||
- Соединение потеряно до начала выполнения:
|
||||
Команда не дошла до сервера — изменения не произошли.
|
||||
- Соединение потеряно в процессе выполнения:
|
||||
Транзакция автоматически откатывается (ROLLBACK).
|
||||
- Соединение потеряно после успешного выполнения:
|
||||
Транзакция уже зафиксирована (COMMIT выполнен).
|
||||
|
||||
**2. Почему хорошо, что база сама откатывает незавершённые изменения?**
|
||||
|
||||
Это хорошо, потому что защищает от повреждения данных, упрощает разработку и гарантирует соблюдение ограничений целостности без ручного вмешательства.
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
-- Задание 1. Витрина выручки по категориям
|
||||
|
||||
/* Требования:
|
||||
|
||||
- Используй таблицы: `products`, `orders`, `order_items`.
|
||||
- Учитывай только оплаченные заказы (`status = 'paid'`).
|
||||
- Создай `MATERIALIZED VIEW` `mv_category_revenue` со столбцами:
|
||||
- `category`
|
||||
- `total_revenue` — суммарная выручка по категории (`quantity * price_at_purchase`)
|
||||
- Отсортируй результат по `total_revenue` по убыванию при выборке.
|
||||
*/
|
||||
|
||||
CREATE MATERIALIZED VIEW mv_category_revenue AS
|
||||
SELECT p.category,
|
||||
SUM(oi.quantity * oi.price_at_purchase) AS total_revenue
|
||||
FROM dmezentsev.products p
|
||||
JOIN dmezentsev.orders_items oi ON p.id = oi.product_id
|
||||
JOIN dmezentsev.orders o ON oi.order_id = o.id
|
||||
WHERE o.status = 'paid'
|
||||
ORDER BY total_revenue DESC
|
||||
|
||||
|
||||
/* 1. Чем удобно иметь готовую витрину по категориям, вместо того чтобы каждый раз писать JOIN по трём таблицам?
|
||||
- Производительность — данные уже агрегированы и лежат в одной материализованной таблице. Вместо сканирования
|
||||
трёх таблиц и группировки, запрос читает готовую таблицу.
|
||||
- Простота запросов — аналитик пишет SELECT * FROM mv_category_revenue, не вникая в схему и условия фильтрации.
|
||||
*/
|
||||
|
||||
|
||||
-- Задание 2. Витрина активности пользователей
|
||||
|
||||
/* Требования:
|
||||
|
||||
- Используй таблицы: `users`, `orders`.
|
||||
- Считай только оплаченные заказы.
|
||||
- Создай MATERIALIZED VIEW **`mv_user_activity`** со столбцами:
|
||||
- `user_id`
|
||||
- `user_name`
|
||||
- `orders_count` — сколько оплаченных заказов сделал пользователь
|
||||
- В выборке покажи только тех, у кого `orders_count > 0`, отсортируй по `orders_count` по убыванию.
|
||||
*/
|
||||
|
||||
CREATE MATERIALIZED VIEW mv_user_activity AS
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.user_name,
|
||||
COUNT(o.order_id) AS orders_count
|
||||
FROM users u
|
||||
JOIN orders o ON u.user_id = o.user_id
|
||||
WHERE o.status = 'paid'
|
||||
GROUP BY u.user_id, u.user_name
|
||||
HAVING COUNT(o.order_id) > 0
|
||||
ORDER BY orders_count DESC;
|
||||
|
||||
/* 1. Как по такой витрине найти «самых лояльных» клиентов?
|
||||
|
||||
SELECT * FROM mv_user_activity LIMIT 3;
|
||||
|
||||
(Предположил ради примера, что самые лояльные - это топ-3 клиента)
|
||||
|
||||
2. Что будет, если считать активность не по всем заказам, а только за последние 30 дней
|
||||
— как изменится смысл витрины?
|
||||
|
||||
Смысл витрины изменится на отслеживание текущей (недавней) активности, а не за весь период времени в данных
|
||||
*/
|
||||
|
||||
|
||||
-- Задание 3. Витрина выручки по дням
|
||||
|
||||
/* Требования:
|
||||
|
||||
- Используй таблицы: `orders`, `order_items`.
|
||||
- Учитывай только оплаченные заказы.
|
||||
- Создай `MATERIALIZED VIEW` **`mv_daily_revenue`** со столбцами:
|
||||
- `order_date::date` как `sales_date`
|
||||
- `daily_revenue` — суммарная выручка за день
|
||||
- При выборке отсортируй по `sales_date` по возрастанию.
|
||||
*/
|
||||
|
||||
CREATE MATERIALIZED VIEW mv_daily_revenue AS
|
||||
SELECT
|
||||
o.order_date::date AS sales_date,
|
||||
SUM(oi.quantity * oi.price_at_purchase) AS daily_revenue
|
||||
FROM orders o
|
||||
JOIN order_items oi ON o.id = oi.order_id
|
||||
WHERE o.status = 'paid'
|
||||
GROUP BY o.order_date::date
|
||||
ORDER BY sales_date ASC;
|
||||
|
||||
/* 1. Как по такой витрине заметить «просадки» и «пики» продаж?
|
||||
|
||||
- Построить линейный график зависимости daily_revenue от sales_date
|
||||
- Использовать оконные функции для сравнения со скользящим средним (например, за 7 дней)
|
||||
- Вычислить оконной функцией LAG и разницу с предыдущим днём, чтобы увидеть изменения.
|
||||
|
||||
2. Что быстрее: считать выручку «на лету» из сырых таблиц или читать из `mv_daily_revenue` ? Почему?
|
||||
|
||||
Чтение из mv_daily_revenue быстрее, потому что:
|
||||
- В mv_daily_revenue меньше строк, так как данные агрегированы. Сырые таблицы могут содержать
|
||||
миллионы записей — каждый день приходится заново агрегировать все данные.
|
||||
*/
|
||||
|
||||
-- Задание 4. Обновление витрин
|
||||
|
||||
/* Требования:
|
||||
|
||||
- Для любой одной из созданных витрин (на выбор ученика) написать команду обновления с `REFRESH MATERIALIZED VIEW ...`.
|
||||
*/
|
||||
|
||||
REFRESH MATERIALIZED VIEW mv_daily_revenue;
|
||||
|
||||
|
||||
/* - Придумать расписание обновления витрины для двух сценариев:
|
||||
- интернет‑магазин с несколькими заказами в день;
|
||||
- крупный магазин с тысячами заказов в час.
|
||||
|
||||
Интернет‑магазин с несколькими заказами в день — 1 раз в сутки, ночью
|
||||
За день накапливается мало новых данных, достаточно обновляться раз в сутки
|
||||
|
||||
Крупный магазин с тысячами заказов в час — каждые 30 минут
|
||||
Обновление за 30 минут успевает до следующего цикла
|
||||
|
||||
1. Чем грозит слишком редкий `REFRESH` для отчётов (какие ошибки могут появиться в анализе)?
|
||||
- Устаревшие данные — угроза применения ошибочных мер.
|
||||
- Пропуск аномалий — если витрина обновляется редко, аномалии можно не заметить до следующего REFRESH
|
||||
|
||||
2. Почему нельзя обновлять витрины «каждую секунду», даже если хочется всегда свежие данные?
|
||||
- Ресурсы БД — каждый REFRESH блокирует чтение и перестраивает всю витрину заново
|
||||
- Проблемы с блокировками — если кто-то читает витрину во время REFRESH, запрос встанет в очередь или получит ошибку.
|
||||
*/
|
||||
@@ -0,0 +1,209 @@
|
||||
-- Задача 1. Нумерация заказов пользователя
|
||||
|
||||
SELECT u.id AS user_id,
|
||||
u.name AS user_name,
|
||||
o.id AS order_id,
|
||||
o.order_date,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY u.id
|
||||
ORDER BY o.order_date
|
||||
) AS order_number_for_user
|
||||
FROM dmezentsev.users u
|
||||
JOIN dmezentsev.orders o ON u.id = o.user_id;
|
||||
|
||||
/* - В чём отличие такой нумерации от простой сортировки
|
||||
ORDER BY order_date без оконной функции?
|
||||
|
||||
В случае с оконной функцией идет сортировка по order_date в рамках одного окна
|
||||
(по пользователю), и каждому заказу пользователя присваивается свой порядковый
|
||||
номер. При простой сортировке без оконной функции по дате сортировались бы
|
||||
все заказы
|
||||
*/
|
||||
|
||||
-- Задача 2. Накопительная выручка по пользователю
|
||||
|
||||
WITH order_totals AS (
|
||||
SELECT o.user_id,
|
||||
u.name AS user_name,
|
||||
o.id AS order_id,
|
||||
o.order_date,
|
||||
SUM(oi.quantity * oi.price_at_purchase) AS order_revenue
|
||||
FROM dmezentsev.orders o
|
||||
JOIN dmezentsev.users u ON o.user_id = u.id
|
||||
JOIN dmezentsev.order_items oi ON o.id = oi.order_id
|
||||
GROUP BY o.user_id, u.name, o.id, o.order_date
|
||||
)
|
||||
|
||||
SELECT user_id,
|
||||
user_name,
|
||||
order_id,
|
||||
order_date,
|
||||
order_revenue,
|
||||
SUM(order_revenue) OVER (
|
||||
PARTITION BY user_id
|
||||
ORDER BY order_date
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) AS running_revenue_for_user
|
||||
FROM order_totals;
|
||||
|
||||
/* - Почему для накопительной суммы важно задавать рамку окна
|
||||
(от «самого первого» до «текущего»), а не оставлять значения по умолчанию?
|
||||
|
||||
По умолчанию RANGE BETWEEN ... Если у пользователя два заказа
|
||||
в один и тот же день, то накопительная сумма посчитается так:
|
||||
1 января 100 руб Накопительная сумма: 300
|
||||
1 января 200 руб Накопительная сумма: 300
|
||||
|
||||
Важно задавать явно ROWS BETWEEN..., чтобы накопительная сумма считалась
|
||||
правильно:
|
||||
1 января 100 руб Накопительная сумма: 100
|
||||
1 января 200 руб Накопительная сумма: 300
|
||||
|
||||
- Как по такой таблице понять, что пользователь начал тратить больше?
|
||||
|
||||
Если накопительная сумма растет, то это указывает на то, что
|
||||
пользователь с течением времени тратит больше
|
||||
*/
|
||||
|
||||
-- Задача 3. Рейтинг пользователей по выручке
|
||||
|
||||
WITH user_revenue AS (
|
||||
SELECT u.id AS user_id,
|
||||
u.name AS user_name,
|
||||
sum(oi.quantity * oi.price_at_purchase) AS total_revenue
|
||||
FROM dmezentsev.users u
|
||||
JOIN dmezentsev.orders o ON u.id = o.user_id
|
||||
JOIN dmezentsev.order_items oi ON o.id = oi.order_id
|
||||
GROUP BY u.name, u.id
|
||||
)
|
||||
|
||||
SELECT user_id,
|
||||
user_name,
|
||||
total_revenue,
|
||||
RANK() OVER (ORDER BY total_revenue DESC) AS revenue_rank,
|
||||
DENSE_RANK() OVER (ORDER BY total_revenue DESC) AS revenue_dense_rank
|
||||
FROM user_revenue;
|
||||
|
||||
/* - Чем RANK отличается от DENSE_RANK в случае, когда
|
||||
у двух пользователей одинаковая выручка?
|
||||
|
||||
Допустим, у двух таких пользователей будет 1ое место у каждого по выручке.
|
||||
При RANK у следующего будет 3ье место, при DENSE_RANK - 2ое.
|
||||
|
||||
RANK пропускает следующие числовые позиции после одинаковых
|
||||
значений (например: 1, 2, 2, 4), а DENSE_RANK не пропускает (например: 1, 2, 2, 3).
|
||||
*/
|
||||
|
||||
-- Задача 4. Первый и последний заказ пользователя
|
||||
|
||||
SELECT u.id AS user_id,
|
||||
u.name AS user_name,
|
||||
o.id AS order_id,
|
||||
o.order_date,
|
||||
FIRST_VALUE(o.order_date) OVER (
|
||||
PARTITION BY u.id
|
||||
ORDER BY o.order_date
|
||||
) AS first_order_date,
|
||||
LAST_VALUE(o.order_date) OVER (
|
||||
PARTITION BY u.id
|
||||
ORDER BY o.order_date
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
|
||||
) AS last_order_date
|
||||
FROM dmezentsev.users u
|
||||
JOIN dmezentsev.orders o ON u.id = o.user_id;
|
||||
|
||||
/* - Как можно использовать поля «первый заказ» и «последний заказ»
|
||||
в сегментации клиентов?
|
||||
|
||||
Для определения частоты покупок (разница между датами), выделения
|
||||
новых или ушедших клиентов (например, клиенты без заказов за последние 3 месяца)
|
||||
|
||||
- Почему для правильного LAST_VALUE важно задать рамку окна так, чтобы смотреть
|
||||
на всю историю пользователя, а не только до текущей строки?
|
||||
|
||||
Потому что по умолчанию рамка окна — RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,
|
||||
и LAST_VALUE показывает последнее значение в текущей рамке, т.е. текущую строку.
|
||||
А нужно по всей рамке, от первой до последней даты у каждого пользователя
|
||||
*/
|
||||
|
||||
-- Задача 5. Заказы и количество заказов пользователя
|
||||
|
||||
SELECT u.id AS user_id,
|
||||
u.name AS user_name,
|
||||
o.id AS order_id,
|
||||
COUNT(o.id) OVER (PARTITION BY u.id) AS orders_per_user
|
||||
FROM dmezentsev.users u
|
||||
JOIN dmezentsev.orders o ON u.id = o.user_id;
|
||||
|
||||
/* - Чем такой результат отличается от классического отчёта
|
||||
«по пользователям» с GROUP BY user_id?
|
||||
|
||||
При применении оконной функции строки не схлопываются, то есть если у
|
||||
пользователя несколько заказов, то по каждому заказу будет строка с данными,
|
||||
и в каждой строке будет общее число заказов
|
||||
*/
|
||||
|
||||
-- Задача 6. Сравнение суммы заказа с предыдущим
|
||||
|
||||
WITH orders_total AS (
|
||||
SELECT u.id AS user_id,
|
||||
o.id AS order_id,
|
||||
u.name AS user_name,
|
||||
o.order_date,
|
||||
SUM(oi.quantity * oi.price_at_purchase) AS order_revenue
|
||||
FROM dmezentsev.users u
|
||||
JOIN dmezentsev.orders o ON u.id = o.user_id
|
||||
JOIN dmezentsev.order_items oi ON o.id = oi.order_id
|
||||
GROUP BY u.id, o.id, u.name, o.order_date
|
||||
)
|
||||
|
||||
SELECT user_id,
|
||||
user_name,
|
||||
order_id,
|
||||
order_date,
|
||||
order_revenue,
|
||||
LAG(order_revenue) OVER (
|
||||
PARTITION BY user_id
|
||||
ORDER BY order_date
|
||||
) AS prev_order_revenue
|
||||
FROM orders_total;
|
||||
|
||||
/* - Как по такому результату понять,
|
||||
увеличивается или уменьшается средний чек с течением времени?
|
||||
|
||||
Нужно сравнить order_revenue с prev_order_revenue для каждого заказа.
|
||||
Если чаще order_revenue > prev_order_revenue — средний чек растёт.
|
||||
Если чаще order_revenue < prev_order_revenue — средний чек падает.
|
||||
sql
|
||||
*/
|
||||
|
||||
-- Задача 7. Категории с накопительной выручкой по времени
|
||||
|
||||
WITH category_revenue AS (
|
||||
SELECT p.category,
|
||||
DATE_TRUNC('day', o.order_date)::DATE AS order_date,
|
||||
SUM(oi.quantity * oi.price_at_purchase) AS daily_category_revenue
|
||||
FROM dmezentsev.order_items oi
|
||||
JOIN dmezentsev.products p ON oi.product_id = p.id
|
||||
JOIN dmezentsev.orders o ON oi.order_id = o.id
|
||||
GROUP BY p.category,
|
||||
DATE_TRUNC('day', o.order_date)::DATE
|
||||
)
|
||||
|
||||
SELECT category,
|
||||
order_date,
|
||||
daily_category_revenue,
|
||||
SUM(daily_category_revenue) OVER (
|
||||
PARTITION BY category
|
||||
ORDER BY order_date
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||
) AS running_category_revenue
|
||||
FROM category_revenue;
|
||||
|
||||
/* - Чем такой «накопительный» график по категории отличается по смыслу
|
||||
от обычного ежедневного (без SUM OVER)?
|
||||
|
||||
Обычный график daily_category_revenue показывает, какая выручка категории день ото дня.
|
||||
Накопительный график running_category_revenue показывает общий рост
|
||||
или падение категории за период собранных данных
|
||||
*/
|
||||
Reference in New Issue
Block a user