diff --git a/dmezentsev/postgresql/homework/CTE.sql b/dmezentsev/postgresql/homework/CTE.sql new file mode 100644 index 0000000..8c99b87 --- /dev/null +++ b/dmezentsev/postgresql/homework/CTE.sql @@ -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: +внутри подзапроса или снаружи, и почему? + +Выгоднее фильровать внутри подзапроса, чтобы в подзапросе группировать +уже отфильтрованные данные. И уже в основном запросе джойнить, а меньше +строк для джойна ускорят запрос + */ diff --git a/dmezentsev/postgresql/homework/Explain.md b/dmezentsev/postgresql/homework/Explain.md new file mode 100755 index 0000000..bbd5452 --- /dev/null +++ b/dmezentsev/postgresql/homework/Explain.md @@ -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 +- Убраны функции и расчеты по колонкам, не позволяющие использовать индексы + +Все это в совокупности на больших таблицах ускорит время выполнения запроса diff --git a/dmezentsev/postgresql/homework/Locking.md b/dmezentsev/postgresql/homework/Locking.md new file mode 100755 index 0000000..88d921c --- /dev/null +++ b/dmezentsev/postgresql/homework/Locking.md @@ -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 многим пользователям скриптом?** + +Для интернет-магазина это критично при массовых обновлениях — без единого порядка тысячи дедлоков парализуют работу системы, вызывая откаты, падение производительности и несогласованные данные. \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Logic_physic_joins.md b/dmezentsev/postgresql/homework/Logic_physic_joins.md new file mode 100755 index 0000000..2ccf34f --- /dev/null +++ b/dmezentsev/postgresql/homework/Logic_physic_joins.md @@ -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 хорош для 'большая + большая + нет индекса'. Но лучше всё-таки не отключать оптимизатору вариант выбора физического джойна))) \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Optimization.md b/dmezentsev/postgresql/homework/Optimization.md new file mode 100755 index 0000000..9ad8eb6 --- /dev/null +++ b/dmezentsev/postgresql/homework/Optimization.md @@ -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 колонки вместо всех), ну и поэтому передаёт меньше байт по сети \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Query_order.md b/dmezentsev/postgresql/homework/Query_order.md new file mode 100755 index 0000000..d8ec3ca --- /dev/null +++ b/dmezentsev/postgresql/homework/Query_order.md @@ -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 — за один проход. \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Transactions.md b/dmezentsev/postgresql/homework/Transactions.md new file mode 100755 index 0000000..c767031 --- /dev/null +++ b/dmezentsev/postgresql/homework/Transactions.md @@ -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. Почему хорошо, что база сама откатывает незавершённые изменения?** + +Это хорошо, потому что защищает от повреждения данных, упрощает разработку и гарантирует соблюдение ограничений целостности без ручного вмешательства. \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Views.sql b/dmezentsev/postgresql/homework/Views.sql new file mode 100755 index 0000000..8454a63 --- /dev/null +++ b/dmezentsev/postgresql/homework/Views.sql @@ -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, запрос встанет в очередь или получит ошибку. +*/ \ No newline at end of file diff --git a/dmezentsev/postgresql/homework/Window_functions.sql b/dmezentsev/postgresql/homework/Window_functions.sql new file mode 100644 index 0000000..e2b773c --- /dev/null +++ b/dmezentsev/postgresql/homework/Window_functions.sql @@ -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 показывает общий рост +или падение категории за период собранных данных + */ \ No newline at end of file