Введение
В заключительной статье блока производительности собираем воедино несколько практических антипаттернов, упоминавшихся по отдельности в подводных камнях предыдущих статей (включая саму статью 394) — рассматриваем их систематически как единый чек-лист типичных, легко допускаемых проблем производительности SQL-кода.
Концепция
SELECT * передаёт больше данных по сети, чем реально нужно приложению, и делает запрос хрупким к изменениям схемы (статья 394). Неиспользуемые (или, что хуже, активно мешающие, но не помогающие) индексы замедляют операции записи без компенсирующей выгоды для чтения. Неявные преобразования типов (статья 416, пример с order_date::text) могут «ломать» возможность использования существующего индекса, незаметно превращая ожидаемый эффективный поиск по индексу в полный просмотр таблицы.
Пример кода: Антипаттерны производительности
-- Антипаттерн: SELECT * вместо явного перечисления нужных столбцов
SELECT * FROM orders WHERE customer_id = 5; -- передаёт ВСЕ столбцы, включая ненужные приложению
-- Лучше — явное перечисление только реально нужных столбцов
SELECT id, order_date, total_amount FROM orders WHERE customer_id = 5;
-- Поиск неиспользуемых индексов (синтаксис PostgreSQL, через системную статистику)
SELECT indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 -- индекс ни разу не был использован с момента последнего сброса статистики
ORDER BY indexrelname;
-- такие индексы — кандидаты на удаление, если отсутствие использования подтверждено
-- за достаточно длительный, представительный период наблюдения
-- Антипаттерн: неявное преобразование типа в условии WHERE, "ломающее" использование индекса
CREATE INDEX idx_orders_customer ON orders(customer_id); -- customer_id типа INTEGER
SELECT * FROM orders WHERE customer_id = '5'; -- '5' как строка — может вызвать неявное приведение,
-- в зависимости от СУБД, потенциально мешая использованию индекса
-- Явное соответствие типов — гарантированно избегает этой проблемы
SELECT * FROM orders WHERE customer_id = 5; -- 5 как число, соответствует типу столбца
-- Ещё один частый антипаттерн — функция, применённая к индексированному столбцу в WHERE
CREATE INDEX idx_customers_email ON customers(email);
-- НЕэффективно — UPPER(email) не покрывается обычным индексом по email
SELECT * FROM customers WHERE UPPER(email) = 'IVAN@EXAMPLE.COM';
-- Решение 1: функциональный индекс непосредственно по UPPER(email) (поддерживается многими СУБД)
CREATE INDEX idx_customers_email_upper ON customers(UPPER(email));
-- Решение 2: нормализация данных при сохранении (хранить email уже в нижнем регистре)
-- и сравнение без UPPER() в запросе вовсе
Пояснения к коду
Поиск неиспользуемых индексов через pg_stat_user_indexes/idx_scan = 0 показывает практичный, основанный на реальных, накопленных данных способ выявления индексов-кандидатов на удаление — в отличие от попытки угадать «нужен ли этот индекс» чисто умозрительно, реальная статистика использования (накопленная за представительный период работы production-системы) даёт объективное подтверждение того, действительно ли конкретный индекс приносит практическую пользу или просто замедляет операции записи без какой-либо компенсирующей выгоды для чтения. Функциональный индекс UPPER(email) — практичное решение проблемы «функция над индексированным столбцом» — вместо обычного, прямого индекса по самому столбцу, индекс строится непосредственно по результату применения функции, что позволяет запросам, использующим ту же функцию в условии, эффективно использовать такой специально подготовленный индекс.
Подводные камни
- Удаление индекса на основе кратковременного наблюдения
idx_scan = 0, не учитывающего, что некоторые индексы используются редко, но критично важны именно в эти редкие моменты (например, специфичный отчёт, выполняемый раз в квартал, или индекс, поддерживающий ограничение уникальности, важное независимо от того, использовался ли он для ускорения чтения) — перед удалением индекса на основании статистики использования стоит явно убедиться, что период наблюдения достаточно представителен и охватывает все потенциально релевантные, хотя бы редкие сценарии использования этого индекса. - Чрезмерное увлечение
SELECT столбец1, столбец2вместоSELECT *там, где разница в объёме передаваемых данных пренебрежимо мала (таблица с небольшим количеством столбцов умеренного размера) — хотя явное перечисление столбцов в целом хорошая практика (защита от хрупкости к изменениям схемы, как отмечено в статье 394), не стоит излишне фокусироваться на этом как на «главной» оптимизации производительности там, где реальный выигрыш от этого конкретного изменения практически отсутствует, в то время как более значимые проблемы (отсутствие нужного индекса, неэффективный JOIN) остаются незамеченными. - Создание функционального индекса (как
UPPER(email)) без обновления ВСЕХ мест кода приложения, где выполняется аналогичный поиск, что приводит к тому, что индекс реально помогает лишь части запросов, написанных именно с той же функцией в условии, тогда как другие, написанные иначе (например, со схожей, но не идентичной по записи логикой сравнения без учёта регистра) продолжают не использовать этот специфичный индекс эффективно — для функциональных индексов важна согласованность точной формы условияWHEREпо всему кодовой базе, использующей этот индекс. - Слепое следование чек-листу антипаттернов без анализа конкретной ситуации и реального измерения (статья 416) — как и любой обобщённый список рекомендаций, эти антипаттерны — отправная точка для анализа, а не безусловные правила, применимые механически без понимания конкретного контекста; иногда
SELECT *действительно безвреден для конкретного небольшого запроса, а «неиспользуемый» индекс на самом деле нужен для редкого, но важного случая — окончательное решение должно опираться на понимание конкретной ситуации, а не механическое применение общих правил.
Третий блок цикла «Углублённый цикл SQL» (статьи 413-422, производительность и индексы) завершён. Далее следует блок 4 — конкретные СУБД и их особенности (423-432).