Урок 10.2 · Время чтения: ~10 мин
Этот урок посвящен основам написания производительных SQL-запросов. Вы узнаете, как избегать лишней нагрузки на базу данных, почему SELECT * может замедлять работу и как правильно фильтровать данные. Мы разберем практические приемы, которые помогут вашим запросам выполняться быстрее даже на больших объемах данных. К концу урока вы научитесь писать эффективный код, который бережно относится к ресурсам сервера.
В предыдущем уроке мы говорили о том, как сделать код понятным для человека. Однако SQL-код должен быть «понятным» и эффективным и для самой базы данных. Даже идеально отформатированный запрос может работать медленно, если он заставляет сервер выполнять лишнюю работу.
Эффективность запроса напрямую влияет на скорость работы приложений и отчетов. В условиях работы с большими данными (Big Data) или высоконагруженными системами разница между «просто рабочим» и «оптимизированным» запросом может составлять минуты и даже часы времени выполнения.
Стоит отметить, что современные СУБД оснащены мощными оптимизаторами, которые умеют перестраивать ваши запросы «под капотом» для более быстрого выполнения. Однако оптимизатор не всесилен: он не может знать бизнес-логику процесса и не всегда способен исправить принципиально неэффективные архитектурные решения. Поэтому ответственность за качество кода всегда лежит на разработчике.
Самая частая причина медленной работы — передача лишних данных между сервером базы данных и вашим приложением.
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.EXISTS вместо JOIN, если вам нужно проверить только факт наличия записи в другой таблице.LIMIT для ознакомления с данными.SELECT * вреден в рабочих запросах?SELECT * возвращает все столбцы, включая ненужные, что увеличивает трафик и нагрузку на сервер. Явное перечисление полей делает запрос быстрее и стабильнее при изменении структуры таблицы.
SARGable-условие позволяет СУБД использовать индекс для поиска строк. Если вы оборачиваете индексируемую колонку в функцию, оптимизатор часто переходит к полному сканированию таблицы.
EXISTS, а не JOIN?EXISTS удобен, когда нужно проверить сам факт наличия связанной записи, а не выбирать поля из второй таблицы. В таких сценариях СУБД может завершить поиск раньше, что уменьшает объем работы.
Сначала проверю, не использует ли запрос SELECT *, и оценю фильтрацию в WHERE. Затем проанализирую, можно ли сделать условия SARGable, сократить объем данных до JOIN и упростить логику выборки.
Раннее ограничение выборки уменьшает количество строк, которые участвуют в JOIN, сортировке и агрегации. Это снижает стоимость плана выполнения и сокращает время ответа.
JOIN и EXISTS в контексте производительности?JOIN объединяет строки из таблиц и подходит, когда нужны данные из обеих сторон. EXISTS проверяет наличие связанной строки и часто эффективнее для логики "есть/нет", потому что может остановиться на первом совпадении.
В следующем уроке мы углубимся в технические детали и узнаем, как именно индексы помогают ускорять наши запросы на физическом уровне.