.NET Разработчик: post #3294 — TG.ME

День 2751. #BestPractices #SQL
Как оптимизировать SQL-запросы. Часть 3
Часть 1
Часть 2

Часть III. Упростите операции соединения и подзапросы
Это место, где запросы становятся дорогостоящими и где скрываются самые большие возможности улучшения. Цель в том, чтобы заставить БД выполнять меньше работы и представить эту работу в форме, с которой она лучше всего справляется.

1. Уменьшайте сложность операций соединения
Каждая операция соединения — это дополнительная работа. Чем меньше таблиц базе нужно coединить, тем быстрее запрос. Распространённая ошибка — соединение таблицы, из которой вы фактически не читаете данные:
-- Плохо: соединение с suppliers, которая не используется 
SELECT p.product_name, c.category_name
FROM products p
JOIN categories c ON p.category_id = c.category_id
JOIN suppliers s ON p.supplier_id = s.supplier_id;

Удалите ненужное соединение:
SELECT p.product_name, c.category_name
FROM products p
JOIN categories c ON p.category_id = c.category_id;

Прочитайте столбцы в SELECT и WHERE и удалите все соединения с таблицами, на которые нет ссылок.

2. Выбирайте правильный тип соединения
Тип соединения влияет как на результат, так и на стоимость. Используйте INNER JOIN, когда нужны совпадающие строки с обеих сторон, LEFT JOIN только тогда, когда действительно нужны и несовпадающие строки, и EXISTS, когда просто нужно узнать, существует ли совпадение:
-- Проверка существования: EXISTS останавливается на первом совпадении
SELECT c.customer_id, c.name
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.customer_id
AND o.total > 100
);

LEFT JOIN часто используется там, где подошло бы INNER JOIN — оно заставляет базу сохранять несовпадающие строки, которые будут проигнорированы позже.

3. Заменяйте избыточные подзапросы соединениями (JOIN) или CTE
Подзапрос выполняется для каждой строки, что может быть крайне неэффективно при работе с большим набором результатов. В данном случае подзапрос выполняется для каждого заказа, просто чтобы найти имя клиента:
-- Плохо: подзапрос на каждую строку 
SELECT o.order_id,
(SELECT c.name FROM customers c
WHERE c.customer_id = o.customer_id) AS customer_name
FROM orders o;

Простое соединение выполнит ту же работу один раз:
-- Хорошо: одно соединение
SELECT o.order_id, c.name AS customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;


Когда один и тот же подзапрос требуется несколько раз в одном запросе, преобразуйте его в общее табличное выражение (CTE) с помощью оператора WITH, чтобы он был написан один раз и его было легче читать. Вот пример медленного запроса:
-- Плохо: подзапрос повторяется 
SELECT
c.customer_id,
c.name,
(
SELECT SUM(o.total_amount)
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.order_date >= DATE '2026-01-01'
) AS total_spent,
(
SELECT COUNT(*)
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.order_date >= DATE '2026-01-01'
) AS order_count
FROM customers c;


А вот более быстрый запрос, использующий CTE:
-- Хорошо: считаем результат один раз и переиспользуем 
WITH customer_order_totals AS (
SELECT
customer_id,
SUM(total_amount) AS total_spent,
COUNT(*) AS order_count
FROM orders
WHERE order_date >= DATE '2026-01-01'
GROUP BY customer_id
)
SELECT
c.customer_id, c.name,
COALESCE(t.total_spent, 0) AS total_spent,
COALESCE(t.order_count, 0) AS order_count
FROM customers c
LEFT JOIN customer_order_totals t ON t.customer_id = c.customer_id;


4. EXISTS лучше, чем IN
EXISTS может привести к прерыванию обработки: он останавливается на первой совпадающей строке, в то время как IN может сначала сформировать полный список значений:
SELECT p.product_id, p.product_name
FROM products p
WHERE EXISTS (
SELECT 1 FROM order_details od
WHERE od.product_id = p.product_id
);

Примечание: не следует воспринимать «EXISTS всегда лучше IN» как жёсткое правило. В современных версиях PostgreSQL запросы IN, EXISTS и даже некоторые соединения часто переписываются в один и тот же план выполнения, поэтому они могут работать идентично. Однако проблема всё ещё возникает с NOT IN в подзапросе, который может возвращать NULL — это приводит к неожиданным результатам и худшему плану выполнения, поэтому в этом случае предпочтительнее использовать NOT EXISTS. Как всегда, проверяйте план выполнения, а не гадайте.

Продолжение следует…

Источник:
https://antondevtips.com/blog/how-to-optimize-sql-queries-20-proven-best-practices
August 12, 2026 1.3K 3 14