Семь ошибок индексации БД, которые убивают производительность SaaS на корню
Индексы — не волшебная таблетка. Неправильные индексы тормозят запросы, раздувают хранилище и превращают INSERT в катастрофу. Разбираем семь типичных ошибок и показываем, как их избежать.
Ваш код чист, архитектура продумана, а запросы на staging укладываются в 12 мс. Но при 500 000 записей всё встаёт: панели мониторинга тормозят, пользователи жалуются, а дежурный инженер в полночь разглядывает план выполнения запроса и не понимает, что пошло не так. В девяти случаях из десяти причина — не отсутствие индексов, а неправильные индексы.
Индекс в базе данных — это вспомогательная структура (чаще всего B-дерево; хеш-индексы, GiST, GIN и другие типы применяются в узких специфических сценариях), которая ускоряет выборку строк по заданным колонкам. По аналогии с оглавлением книги: вместо перелистывания всех страниц СУБД сразу переходит к нужной главе. Но если оглавление построено плохо, пользы от него нет — а вот накладные расходы на поддержку остаются.
В этой статье разберём семь самых разрушительных ошибок индексации в production-SaaS: от параноидального создания индексов «на всякий случай» до игнорирования раздутия и особенностей мультитенантных схем. Каждая ошибка — с примерами SQL, метриками и конкретным фиксом.
Ключевые выводы
Индексы — это налог на запись: каждый INSERT, UPDATE и DELETE обновляет все индексы таблицы.
Индекс на колонке с низкой селективностью (boolean, статус) почти бесполезен — используйте частичные (partial) индексы.
В составном индексе порядок колонок критичен: PostgreSQL может использовать (a, b, c) только если условие начинается с a.
Раздутые индексы на высокоизменяемых таблицах могут занимать в 8 раз больше места, чем нужно — планируйте REINDEX CONCURRENTLY.
В мультитенантных системах индекс по tenant_id один часто недостаточен: добавляйте диапазон времени или тип события.
Внешние ключи в PostgreSQL не индексируются автоматически — после каждого FOREIGN KEY создавайте индекс вручную.
Без EXPLAIN ANALYZE индексы создаются вслепую. Проверяйте реальный план выполнения перед деплоем.
Ошибка 1. Индексировать всё подряд «на всякий случай»
Самая распространённая ошибка — не недостаток индексов, а их избыток из тревожности. Особенно часто в неё попадают junior-разработчики: добавляют индекс на каждую колонку, которая встречается в WHERE, «на всякий случай». Кажется ответственным, но на самом деле вредно.
Каждый индекс — это налог на запись. При INSERT, UPDATE и DELETE PostgreSQL (или MySQL) вынуждена обновлять все индексы таблицы. На таблице с 8 индексами каждая запись трогает 8 структур данных. При низкой нагрузке это невидимо, но при 10 000 записей в минуту это становится узким местом.
Аудит неиспользуемых индексов в PostgreSQL:
Запрос кpg_stat_user_indexesпокажет, сколько раз каждый индекс применялся с момента сброса статистики. Еслиidx_scan = 0— индекс кандидат на удаление.
Не удаляйте индекс сразу — сначала убедитесь, что он не нужен для редких, но критичных отчётов. Но если он месяцами не используется, смело избавляйтесь.
Ошибка 2. Не понимать селективность
Индекс на булеву колонку — почти всегда бесполезен. Селективность измеряет, сколько различных значений содержится относительно общего числа строк. У boolean всего два значения. Если 95% строк имеют is_active = true, планировщик запросов проигнорирует индекс и сделает Seq Scan — и будет прав.
Правило большого пальца: если у колонки меньше 10–20 уникальных значений относительно размера таблицы, простой индекс по ней один не справится. Используйте частичные или составные индексы.
Ошибка 3. Неверный порядок колонок в составном индексе
Составные индексы мощны, но часто неправильно понимаются. PostgreSQL может использовать индекс (a, b, c) для фильтрации по a, a и b, a, b и c. Но не может эффективно использовать его, если запрос фильтрует только по b или c — ведущая колонка пропущена.
Решение: первыми ставьте колонки с equality-условиями, затем — по селективности, и проектируйте индексы вокруг реальных паттернов запросов, а не вокруг схемы таблицы. Также учитывайте ORDER BY и необходимость покрывающего индекса. Перед созданием обязательно запускайте EXPLAIN ANALYZE.
Ошибка 4. Игнорировать раздутие индексов
Индексы деградируют со временем. Многие инженеры воспринимают их как «поставил и забыл», но это заблуждение. В PostgreSQL при обновлении или удалении строки старые записи в индексе устаревают и накапливаются как раздутие (bloat); их физическое удаление происходит при выполнении VACUUM. На высокоизменяемых таблицах (заказы, события, логи, сессии) раздутие накапливается стремительно.
Таблица с 1 млн живых строк может иметь индекс, раздутый до размеров 8 млн записей. Каждый запрос через такой индекс делает в 8 раз больше работы, чем должен.
REINDEX CONCURRENTLY — ключевое слово. Обычный REINDEX блокирует таблицу, а в production-SaaS это прямой путь к инциденту. CONCURRENTLY перестраивает индекс без блокировок, хотя и медленнее.
Также убедитесь, что autovacuum настроен под вашу реальную нагрузку на запись. Значения по умолчанию в PostgreSQL консервативны и часто недостаточны для SaaS с высокой интенсивностью записи.
Ошибка 5. Индексы на колонках с малым числом уникальных значений в мультитенантных системах
В мультитенантной архитектуре почти каждый запрос фильтрует по tenant_id. Естественное желание — проиндексировать эту колонку. Но для крупных тенантов индекс по tenant_id вернёт слишком много строк, и планировщик предпочтёт Seq Scan. Для маленьких тенантов отдельный индекс по tenant_id может быть полезен.
На серьёзном масштабе правильное решение — партиционирование таблиц по tenant_id, но это архитектурное решение. Практический первый шаг — составные индексы с временными диапазонами.
Ошибка 6. Не индексировать внешние ключи
В PostgreSQL внешние ключи не индексируются автоматически. При удалении родительской строки СУБД должна проверить все дочерние таблицы на наличие ссылающихся записей — и без индекса на внешнем ключе это Seq Scan по каждой дочерней таблице. На таблице orders с 50 млн строк удаление пользователя вызывает полное сканирование.
Сделайте это командным соглашением: в чек-листе миграций обязательный пункт «после каждого FOREIGN KEY — CREATE INDEX».
Ошибка 7. Не использовать EXPLAIN ANALYZE перед деплоем
Большинство решений об индексах принимаются интуитивно. Интуиция ошибается достаточно часто, чтобы это стало проблемой. EXPLAIN ANALYZE показывает, что именно делает планировщик: какие индексы использует, какие игнорирует, сколько строк реально прочитал против оценки, где тратится время.
На что обращать внимание:
• Seq Scan на большой таблице при выборке малой доли строк — возможно, пропущен индекс или планировщик не может использовать существующий индекс.
• Rows Removed by Filter в десятки тысяч — индекс есть, но неправильные колонки или низкая селективность.
• Buffers: shared hit=0 read=45000 — данные не закэшированы (cold cache), страницы читаются с диска. Для диагностики раздутия смотрите общее число буферов и сравнивайте размер индекса с ожидаемым.
• Высокое actual time — проверьте раздутие, актуальность статистики, количество буферов, дисковую подсистему и наличие блокировок. Запустите ANALYZE tablename, чтобы обновить статистику планировщика.
Чек-лист индексации для SaaS
- У каждой колонки внешнего ключа есть индекс?
- Составные индексы упорядочены по селективности, а не по удобству?
- Булевы и низкокардинальные фильтры используют частичные индексы вместо полных?
- Вы запускали EXPLAIN ANALYZE на 10 самых медленных запросов за неделю?
- Есть процесс поиска и удаления неиспользуемых индексов?
- Высокоизменяемые таблицы регулярно проходят REINDEX CONCURRENTLY?
- Autovacuum настроен под реальный объём записи, а не под значения по умолчанию PostgreSQL?
- В мультитенантных таблицах индексы начинаются с tenant_id и включают диапазон времени?
Часто задаваемые вопросы
Что такое селективность индекса?
Селективность — это доля уникальных значений колонки от общего числа строк. Чем выше селективность, тем меньше строк возвращает индекс при одном значении, и тем эффективнее он работает. Например, индекс по email почти уникален (высокая селективность), а индекс по статусу заказа с тремя вариантами — низкая.
Как часто нужно запускать REINDEX CONCURRENTLY?
REINDEX выполняйте при подтверждённом раздутии (например, когда реальный размер индекса существенно превышает теоретический, измеряется через pgstattuple или аналоги), а не по фиксированному расписанию. Для стабильных справочников частая перестройка обычно не требуется.
Почему PostgreSQL не индексирует FOREIGN KEY автоматически?
Внешний ключ — это ограничение целостности, а не структура доступа. PostgreSQL проверяет целостность при DELETE и UPDATE родительской таблицы, но не предполагает, что вы будете часто искать по этой колонке. Поэтому индекс создавать нужно вручную — и это стандартная практика в production.
Можно ли слишком много индексов?
Да. Каждый индекс замедляет INSERT, UPDATE и DELETE, занимает диск и RAM. Оптимальное количество — не абсолютная цифра, а баланс между скоростью чтения и записи. Регулярно проверяйте pg_stat_user_indexes и удаляйте те, что не используются.
Какой инструмент использовать для анализа планов запросов?
В PostgreSQL — встроенный EXPLAIN ANALYZE. Для визуализации используйте explain.depesz.com, pgAdmin или pghero. В MySQL — EXPLAIN FORMAT=JSON. Для автоматического мониторинга медленных запросов подойдут pg_stat_statements (PostgreSQL) или Performance Schema (MySQL).
Выводы
Индексы — не фича производительности, которую добавляют, когда всё начинает тормозить. Это проектное решение, которое принимается вместе со схемой, и пересматривается по мере эволюции паттернов запросов. Команды, которые уверенно масштабируются, — не те, у кого больше всего индексов, а те, кто понимает, что каждый индекс стоит, что даёт и когда его пора убирать.
База данных, которая быстра на 10 000 строках и быстра на 50 миллионах, — не случайность. Это результат того, что кто-то считал планирование запросов первоклассной инженерной задачей, а не рутинным дополнением.
Проверьте свои самые медленные запросы этой недели — возможно, одна из этих семи ошибок уже сидит в вашей production-базе.
Источник и материалы:
• Database Indexing Mistakes That Kill SaaS Performance at Scale — оригинальная статья OutworkTech
• PostgreSQL Index Types — официальная документация
• REINDEX — документация по перестроению индексов