Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11

SQL-оптимизация: 5 запросов, которые ломают базу

Типовые SQL-запросы, которые тормозят базу: от JOIN без условий до DISTINCT в лоб. Почему они опасны и как их переписать правильно, чтобы база работала быстро и стабильно.

1К открытий4К показов
SQL-оптимизация: 5 запросов, которые ломают базу

Оптимизация SQL-запросов — это набор вполне конкретных правил, нарушение которых может положить любую базу. Даже один неудачный JOIN или ORDER BY без индекса способны превратить быструю систему в тормозящую. Мы собрали типовые запросы, которые чаще всего убивают производительность, объяснили, почему они опасны, и показали, как переписать их правильно.

1. Запросы без ограничений

Запрос:

			SELECT * FROM users;
		

Почему он опасен

Такие запросы тянут абсолютно все данные из таблицы, даже если нужны 2–3 поля. На больших таблицах это превращается в тяжелую нагрузку на сервер: растет объём передаваемых данных, замедляется отклик, падает производительность. Если запрос используют в проде (например, при каждом открытии страницы), он легко может «задушить» базу под пиковыми нагрузками.

Как переписать правильно

			SELECT id, name, email FROM users;

		

или

			SELECT id, name, email FROM users LIMIT 100;
		

Важно явно указывать необходимые поля и добавлять LIMIT, если не нужны все строки сразу. Это снижает нагрузку и ускоряет ответ.

2. WHERE без индексов

Запрос:

			SELECT * FROM orders WHERE status = 'completed';

		

Почему он опасен

Если по полю status нет индекса, база будет проходить всю таблицу построчно (full table scan), чтобы найти нужные строки. На таблицах с миллионами записей это приводит к заметной деградации производительности, особенно при частых запросах. Если таких фильтров несколько, нагрузка на сервер растет лавинообразно.

Как переписать правильно

Добавить индекс по фильтруемому полю:

			CREATE INDEX idx_orders_status ON orders(status);

		

Переписать запрос, чтобы он использовал индекс:

			SELECT id, user_id, total_amount 
FROM orders 
WHERE status = 'completed';

		

Если поле часто используется в фильтрах или джоинах — индексировать его почти всегда оправдано. Это резко ускоряет поиск и снижает нагрузку.

3. GROUP BY и агрегаты по большим таблицам

Запрос:

			SELECT country, COUNT(*) 
FROM users
GROUP BY country;

		

Почему он опасен

GROUP BY заставляет базу данных сначала обработать все строки, а потом сгруппировать их в памяти или на диске. Если таблица содержит миллионы записей и нет подходящих индексов, такой запрос приводит к сортировке или хэшированию огромных объёмов данных. Итог — долгие выполнения, рост потребления оперативной памяти и, в худшем случае, временные таблицы на диске (disk spill). Дополнительную нагрузку дают агрегаты (COUNT, SUM, AVG и др.) без фильтров — база пересчитывает их для всех строк, даже если вам нужна малая часть.

Как переписать правильно

Ограничить выборку с помощью фильтров до агрегации:

			SELECT country, COUNT(*) 
FROM users
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY country;

		

Добавить индекс по ключу группировки:

			CREATE INDEX idx_users_country ON users(country);

		

Если данные статичны — использовать материализованные представления или промежуточные агрегации, чтобы не пересчитывать заново каждый раз.

GROUP BY хорошо работает на подготовленных и отфильтрованных данных. Если он упирается в «сырые» миллионы строк — это сигнал к пересмотру логики запроса.

4. Подзапросы в WHERE (особенно некоррелированные)

Запрос

			SELECT *
FROM orders
WHERE customer_id IN (
    SELECT id FROM customers WHERE country = 'Germany'
);

		

Почему он опасен

Подзапрос в WHERE нередко превращается в лишний проход по таблице. Если оптимизатор не умеет эффективно преобразовать подзапрос в JOIN, он будет сначала выполнять подзапрос (часто без индексов), а потом сверять результаты с основной таблицей. При больших объёмах это означает десятки тысяч сравнений и скачки производительности. Особенно опасны некоррелированные подзапросы, которые не зависят от внешнего запроса — они могут выполняться повторно или создавать временные таблицы.

Как переписать правильно

Заменить подзапрос на JOIN:

			SELECT o.*
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.country = 'Germany';

		

Убедиться, что по ключам соединения и фильтрации (customer_id, country) есть индексы:

			CREATE INDEX idx_customers_country ON customers(country);
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

		

Если подзапрос всё же нужен — использовать EXISTS, а не IN, чтобы оптимизатор мог «коротко замыкать» проверку:

			SELECT *
FROM orders o
WHERE EXISTS (
    SELECT 1 FROM customers c
    WHERE c.id = o.customer_id
    AND c.country = 'Germany'
);

		

Подзапросы — удобный синтаксис, но не всегда эффективный. JOIN и индексы почти всегда работают быстрее, особенно на больших таблицах.

5. ORDER BY без индекса

Запрос

			SELECT * FROM orders
ORDER BY created_at DESC;

		

Почему он опасен

Когда вы используете ORDER BY по колонке без индекса, база вынуждена сортировать всю выборку в памяти или на диске. Это означает дополнительную нагрузку на CPU, а при больших объёмах данных — полную деградацию производительности. Часто такие запросы встречаются в продакшене в виде пагинации по миллионам строк. В итоге простой «листинг заказов» превращается в многосекундный или даже минутный ответ.

Как переписать правильно

Добавить индекс по полю сортировки:

			CREATE INDEX idx_orders_created_at ON orders (created_at DESC);

		

Ограничить выборку с помощью LIMIT и пагинации:

			SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 100;

		

Использовать составные индексы, если сортировка идёт вместе с фильтром:

			SELECT * FROM orders
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 100;

		
			CREATE INDEX idx_orders_status_created_at 
ON orders (status, created_at DESC);

		

А какие запросы убивают вашу базу? пишите в комментариях!

Следите за новыми постами
Следите за новыми постами по любимым темам
1К открытий4К показов