13 KiB
Executable File
Задача 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 — Размер таблиц имеет значение
Требования:
Создай тестовые данные:
-- Маленькая таблица (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;
Теперь выполни:
-- Вариант 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 — Влияние сортировки
Требования:
Выполни три варианта:
-- Вариант 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 — Найди узкое место
Требования:
Дан медленный запрос:
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. Предложи, какие индексы нужно создать
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
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?
- После фильтра country = 'RU': users — 1 строка
- После первого LEFT JOIN (users и orders) — 1 строка
- После второго LEFT JOIN (orders и order_items) — 1 строка (у заказа 1 позиция)
- Результат: 1 строка с агрегатами (orders_count=1, products_count=1)
Задача 6 — LEFT JOIN vs INNER JOIN
Требования:
Сравни два запроса:
-- Вариант 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
Требования:
Сравни:
-- Вариант 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 + строк с помощью ИИ, не стал сюда копировать
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 хорош для 'большая + большая + нет индекса'. Но лучше всё-таки не отключать оптимизатору вариант выбора физического джойна)))