SQL код скопирован в буфер обмена
EN PT FR

Урок 10.2 · Время чтения: ~10 мин

Этот урок посвящен основам написания производительных SQL-запросов. Вы узнаете, как избегать лишней нагрузки на базу данных, почему SELECT * может замедлять работу и как правильно фильтровать данные. Мы разберем практические приемы, которые помогут вашим запросам выполняться быстрее даже на больших объемах данных. К концу урока вы научитесь писать эффективный код, который бережно относится к ресурсам сервера.

Урок 10.2: Написание эффективных SQL-запросов

В предыдущем уроке мы говорили о том, как сделать код понятным для человека. Однако SQL-код должен быть «понятным» и эффективным и для самой базы данных. Даже идеально отформатированный запрос может работать медленно, если он заставляет сервер выполнять лишнюю работу.

Эффективность запроса напрямую влияет на скорость работы приложений и отчетов. В условиях работы с большими данными (Big Data) или высоконагруженными системами разница между «просто рабочим» и «оптимизированным» запросом может составлять минуты и даже часы времени выполнения.

Стоит отметить, что современные СУБД оснащены мощными оптимизаторами, которые умеют перестраивать ваши запросы «под капотом» для более быстрого выполнения. Однако оптимизатор не всесилен: он не может знать бизнес-логику процесса и не всегда способен исправить принципиально неэффективные архитектурные решения. Поэтому ответственность за качество кода всегда лежит на разработчике.

Схема приемов ускорения SQL-запросов: фильтрация, индексы, оптимизация JOIN и ограничение выборки


Золотое правило: извлекайте только то, что нужно

Самая частая причина медленной работы — передача лишних данных между сервером базы данных и вашим приложением.

Отказ от SELECT *

Хотя SELECT * удобен при быстрой проверке данных, в финальном коде его следует избегать.

  • Лишний трафик: вы передаете столбцы, которые вам не нужны (например, тяжелые текстовые описания или блобы).
  • Проблемы с индексами: СУБД сложнее использовать покрывающие индексы, если вы запрашиваете все поля.
  • Хрупкость кода: если в таблицу добавят новый столбец, ваш код может сломаться или начать работать медленнее неожиданно для вас.
-- Плохо
SELECT * FROM film;

-- Хорошо
SELECT film_id, title, release_year
FROM film;

Оптимизация фильтрации

То, как вы ограничиваете выборку, определяет, какой объем данных СУБД придется «перелопатить».

Фильтрация на стороне сервера

Всегда старайтесь максимально ограничить выборку с помощью WHERE, прежде чем данные попадут в агрегаты или будут возвращены пользователю. Чем раньше вы отсечете лишнее, тем быстрее отработают последующие шаги (например, JOIN или GROUP BY).

Избегайте функций в условии WHERE (SARGable запросы)

Чтобы СУБД могла эффективно использовать индексы, условия в WHERE должны быть SARGable (от англ. Search ARGumentable — способный быть аргументом поиска). Если применить функцию к индексированному столбцу, запрос перестает быть SARGable: СУБД часто не может заранее вычислить результат функции для всех строк через индекс и вынуждена сканировать всю таблицу (Full Table Scan).

-- Медленно (Non-SARGable: индекс по rental_date не сработает)
SELECT count(*) 
FROM rental 
WHERE YEAR(rental_date) = 2005;

-- Быстро (SARGable: индекс работает)
SELECT count(*) 
FROM rental 
WHERE rental_date >= '2005-01-01' AND rental_date < '2006-01-01';

Работа с соединениями (JOIN)

Соединение таблиц — одна из самых ресурсозатратных операций.

  • Сначала фильтруйте, потом соединяйте: если возможно, уменьшите объем данных в ведомых таблицах.
  • Проверяйте наличие индексов по полям связи: обычно это первичные и внешние ключи (customer_id, film_id и т.д.).
  • Избегайте CROSS JOIN без необходимости: декартово произведение таблиц может мгновенно «уложить» сервер при больших объемах данных.
  • Используйте EXISTS вместо JOIN для проверки существования: если вам нужно просто проверить наличие связанных записей в другой таблице, не извлекая из неё столбцы, EXISTS будет эффективнее. СУБД прекратит поиск, как только найдет первое совпадение, в то время как JOIN продолжит работу для всех строк.
-- Менее эффективно (JOIN заставляет СУБД сопоставлять все оплаты)
SELECT DISTINCT c.first_name, c.last_name
FROM customer c
JOIN payment p ON c.customer_id = p.customer_id;

-- Более эффективно (EXISTS остановится на первой найденной оплате)
SELECT c.first_name, c.last_name
FROM customer c
WHERE EXISTS (
    SELECT 1 FROM payment p WHERE p.customer_id = c.customer_id
);

Использование LIMIT для проверки

Когда вы только отлаживаете запрос, всегда используйте LIMIT. Это убережет вас от случайного вывода миллионов строк, который может заморозить вашу среду разработки.

SELECT customer_id, first_name, last_name
FROM customer
WHERE active = 1
LIMIT 10;

Практический пример: Оптимизация отчета

Допустим, нам нужно найти список фильмов, которые брали в прокат более 30 раз, но только для определенной категории.

Неэффективный подход:

SELECT f.title, COUNT(r.rental_id)
FROM film f
JOIN inventory i ON f.film_id = i.film_id
JOIN rental r ON i.inventory_id = r.inventory_id
JOIN film_category fc ON f.film_id = fc.film_id
JOIN category c ON fc.category_id = c.category_id
WHERE c.name = 'Action'
GROUP BY f.title
HAVING COUNT(r.rental_id) > 30;

Более эффективный подход: Если мы знаем ID категории, лучше использовать его напрямую, избегая лишнего JOIN с таблицей имен категорий:

SELECT f.title, COUNT(r.rental_id) AS rental_count
FROM film f
JOIN film_category fc ON f.film_id = fc.film_id
JOIN inventory i ON f.film_id = i.film_id
JOIN rental r ON i.inventory_id = r.inventory_id
WHERE fc.category_id = 1 -- Используем ID вместо поиска по строке 'Action'
GROUP BY f.film_id, f.title
HAVING COUNT(r.rental_id) > 30;

Примечание: Использование числовых идентификаторов в фильтрах работает быстрее, чем поиск по строковым названиям. Это связано с тем, что числа занимают меньше места, а СУБД сравнивает их быстрее текстовых строк. Кроме того, это часто позволяет избежать лишних соединений (JOIN) с таблицами-справочниками.


Ключевые выводы этого урока:

  • Никогда не используйте SELECT * в продакшн-коде; перечисляйте только нужные столбцы.
  • Фильтруйте данные как можно раньше с помощью WHERE.
  • Пишите SARGable условия: не оборачивайте колонки в функции, чтобы СУБД могла использовать индексы.
  • Используйте EXISTS вместо JOIN, если вам нужно проверить только факт наличия записи в другой таблице.
  • Используйте LIMIT для ознакомления с данными.
  • Отдавайте предпочтение фильтрации по числовым ключам, а не по строковым названиям.

Часто задаваемые вопросы

Почему SELECT * вреден в рабочих запросах?

SELECT * возвращает все столбцы, включая ненужные, что увеличивает трафик и нагрузку на сервер. Явное перечисление полей делает запрос быстрее и стабильнее при изменении структуры таблицы.

Что означает SARGable и зачем это важно?

SARGable-условие позволяет СУБД использовать индекс для поиска строк. Если вы оборачиваете индексируемую колонку в функцию, оптимизатор часто переходит к полному сканированию таблицы.

Когда лучше использовать EXISTS, а не JOIN?

EXISTS удобен, когда нужно проверить сам факт наличия связанной записи, а не выбирать поля из второй таблицы. В таких сценариях СУБД может завершить поиск раньше, что уменьшает объем работы.

Вопросы для собеседования

Какие первые шаги вы сделаете, если SQL-запрос работает медленно?

Сначала проверю, не использует ли запрос SELECT *, и оценю фильтрацию в WHERE. Затем проанализирую, можно ли сделать условия SARGable, сократить объем данных до JOIN и упростить логику выборки.

Почему фильтрация «как можно раньше» ускоряет запрос?

Раннее ограничение выборки уменьшает количество строк, которые участвуют в JOIN, сортировке и агрегации. Это снижает стоимость плана выполнения и сокращает время ответа.

Как вы объясните разницу между JOIN и EXISTS в контексте производительности?

JOIN объединяет строки из таблиц и подходит, когда нужны данные из обеих сторон. EXISTS проверяет наличие связанной строки и часто эффективнее для логики "есть/нет", потому что может остановиться на первом совпадении.

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

Урок 10.3: Понимание методов оптимизации запросов