04 сент. 2025 г.·8 мин. чтения

Приложение, созданное ИИ, работает медленно: исправьте N+1‑запросы и отсутствующие индексы

Приложение, созданное ИИ, работает медленно? Начните с основных проблем базы данных: N+1‑запросов, отсутствующих индексов, неограниченных сканов и шумных ORM — и быстрых исправлений.

Приложение, созданное ИИ, работает медленно: исправьте N+1‑запросы и отсутствующие индексы

Что обычно значит «медленно» (и почему база данных часто виновата)

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

Частая неожиданность: база данных часто доминирует во времени запроса. Сервер может отрисовать страницу за миллисекунды, но если он ждёт данных, то всё ждёт. Один медленный запрос может заблокировать весь запрос. Набор средне-медленных запросов делает то же самое.

Если ваше приложение, созданное ИИ, работает медленно, стоит проверить базу данных одним из первых шагов: автоматически сгенерированный код часто выглядит разумно, но при реальных данных ведёт себя неэффективно. Он может каждый раз подтягивать одни и те же данные, вытягивать гораздо больше строк, чем нужно, или выполнять запросы, которые работают при 50 строках, но рушатся при 50 000.

Почему база данных часто побеждает (и в хорошем, и в плохом смысле)

Большинство страниц сводятся к простой схеме: читать данные, подготовить их и отправить обратно. Именно чтение «съедает» время.

Обычные способы, как база данных захватывает время:

  • Слишком много запросов на загрузку страницы (каждый добавляет время ожидания).
  • Запросы, которые сканируют много строк, потому что не могут использовать индекс.
  • Запросы, которые возвращают огромные результаты, а затем фильтруют их в коде.
  • Блокировки или длинные транзакции, из‑за которых другие запросы ставятся в очередь.
  • Ограничения пула соединений, из‑за которых запросы ждут свободного подключения.

Цель не в «исправить всё сразу». Начните с самого большого узкого места, который можно измерить, исправьте его и проверьте снова. Одно хорошее изменение может сократить время загрузки вдвое, а погоня за десятью мелкими проблемами часто отнимает целый день.

В FixMyMess это частая картина для прототипов, созданных ИИ: на демо всё выглядит нормально, а при реальных пользователях и данных становится невыносимо медленно. Быстрый прогресс обычно приходит от изоляции одного запроса (или небольшой группы запросов), который лежит на критическом пути для самой медленной страницы.

Быстрые признаки, что узким местом является база данных

Когда приложение, созданное ИИ, работает медленно, базу данных стоит проверить в первую очередь — это общий ресурс, от которого зависит каждый запрос. Один медленный запрос может блокировать многих пользователей одновременно.

Вот распространённые симптомы, указывающие на проблемы с базой данных:

  • Конкретные конечные точки медленные, в то время как другие работают нормально (обычно списки, поиск, панели).
  • При реальном трафике вы видите таймауты или ошибки «request took too long».
  • CPU веб‑сервера в норме, а CPU базы или операции чтения на базе высоки.
  • Производительность ухудшается с ростом данных (больше пользователей, больше строк, больше JOIN).
  • Одна и та же страница становится медленнее из недели в неделю без новых функций.

Чтобы отделить медлительность сервера приложения от медлительности базы, подумайте, где тратится время. Проблемы с сервером обычно выглядят так: все маршруты тормозят, CPU веб‑сервера высок, или память растёт до краха. Проблемы с базой обычно проявляются так: несколько маршрутов мучительно медленные, замедление связано с конкретными таблицами, и проблема сильнее проявляется при большем объёме данных.

«Быстро локально, медленно в продакшене» — большой намёк. На вашем ноутбуке база маленькая, тёплая (вкэширована) и без конкуренции. В продакшене реальные размеры данных, конкурентность и часто более строгие сетевые/безопасные настройки. Если конечная точка мгновенна локально, но тормозит в продакшене, часто это значит, что план запроса делает лишнюю работу в масштабе: отсутствующие индексы, N+1‑запросы или полный скан таблицы.

Быстрая проверка: откройте медленную страницу, затем попробуйте версию с меньшим объёмом данных (меньший диапазон дат, меньше фильтров, меньше строк). Если она внезапно стала быстрой, вы, вероятно, платите налог за объём данных. Команды, которые приводят такие случаи в FixMyMess, часто обнаруживают одинаковую закономерность: прототип «работает», но тормозит, как только приходят реальные пользователи и таблицы.

Простой пошаговый поток поиска причин

Когда приложение, созданное ИИ, медленное, хочется править код везде одновременно. Сопротивляйтесь этому. Выберите одно пользовательское действие и пройдите его от начала до конца, чтобы измерять реальные эффекты изменений.

Начните с выбора одного медленного действия, которое делает реальный пользователь, например «открыть Заказы» или «поиск клиентов». Затем найдите конечную точку, которая за это отвечает (точный API‑маршрут или обработчик). Это делает тест воспроизводимым.

Дальше — выполните действие и захватите активность базы. Используйте то, что у вас есть: лог запросов в разработке, APM‑трейс в продакшене или временный лог вокруг запроса. Ищите топ‑запросы, которые выполняются во время этого действия, а не общий снимок всего приложения.

Измеряйте два значения вместо догадок: сколько запросов выполняется и сколько времени в сумме база тратит на них. Страница, которая триггерит 120 мелких запросов, может быть так же медленной, как страница с двумя огромными запросами.

Практический поток, который остаётся сфокусированным:

  1. Воспроизведите медленное действие и зафиксируйте конечную точку.
  2. Захватите запросы, выполняющиеся во время этого действия.
  3. Зафиксируйте число запросов и суммарное время БД для запроса.
  4. Отсортируйте по влиянию: чаще всего сначала идут самый медленный запрос и самый повторяющийся.
  5. Исправьте одну проблему и снова прогоните то же действие, чтобы подтвердить улучшение.

Пример: если «открыть Заказы» занимает 6 секунд, и вы находите 80 запросов с 4.8 секунды DB‑времени, сначала исправьте самый плохой (обычно неиндексированный фильтр или N+1‑петлю). Если при повторном тесте время упадёт до 2 секунд, вы на верном пути.

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

5 быстрых проверок перед изменением кода

Если ваше приложение, созданное ИИ, медленное, не начинайте с переписывания функций. Сначала получите чёткую картину того, что делает база на одной медленной странице или запросе.

Быстрые проверки, которые обычно выявляют проблему

Начните с этих пяти проверок в логах, APM или истории запросов БД:

  • Посчитайте запросы на запрос. Если простая страница триггерит 50, 100 или 500+ запросов, производительность рухнет при росте трафика.
  • Ищите повторения. Если вы видите одинаковую форму SQL снова и снова с меняющимся только ID — вероятно N+1.
  • Ранжируйте по общей стоимости, а не только по «самому медленному». Запрос 30 мс, выполненный 1000 раз, вреднее одиночного 2‑секундного. Смотрите duration x count.
  • Найдите случайные «слишком большой результат» запросы. Ищите SELECT * по большим таблицам, отсутствие WHERE, отсутствие LIMIT или загрузку больших текстовых/blob колонок, которые не нужны.
  • Проверьте, используются ли индексы на самом деле. Запрос может выглядеть нормально, но план показывает полный скан вместо индексного поиска.

Небольшой пример: панель загружает список из 50 клиентов, затем в цикле запрашивает «последний счёт» для каждого. Вы увидите один запрос для списка и 50 почти одинаковых запросов счетов. Каждый по‑отдельности быстр, но вместе они превращают один запрос в пробку.

Что зафиксировать до изменений

Запишите три числа для медленного запроса: общее число запросов, топ‑3 запросов по суммарному времени и используют ли эти топ‑запросы индексы или сканируют таблицу. Это даст базу, чтобы подтвердить, что каждое исправление действительно помогает.

В унаследованных кодовых базах, сгенерированных ИИ, эти проблемы часто спрятаны под ORM и шумными логами. В FixMyMess мы именно такие данные собираем во время бесплатного аудита, чтобы первое изменение было правильным.

N+1 запросы: как быстро найти и остановить

Fix missing indexes fast
We’ll match indexes to real filters and sorts so queries stop scanning huge tables.

N+1 возникает, когда приложение выполняет 1 запрос, чтобы загрузить список, а затем ещё по одному запросу для каждой строки в этом списке. Пример: вы загружаете 50 пользователей (1 запрос), затем получаете заказы каждого пользователя по одному (еще 50 запросов).

Это часто происходит в ORM из‑за ленивой загрузки — это удобно, но дорого. Вы итерируете пользователей, обращаетесь к user.orders, и ORM тихо бьёт по базе каждый раз. Автоматически сгенерированный код часто опирается на такие дефолты, поэтому «приложение, созданное ИИ, работает медленно» часто означает «страница делает сотни мелких запросов».

Как быстро заметить

Ищите повторяющийся шаблон в логах или APM: одна и та же форма SQL снова и снова с меняющимся только ID. Другой признак — страница, которая становится медленнее с ростом объёма данных, хотя код не менялся.

Если можете, посчитайте запросы для одного запроса. Если число растёт вместе с количеством элементов на странице (20 элементов -> ~21 запрос, 100 элементов -> ~101 запрос), вы, вероятно, нашли N+1.

Быстрые исправления, которые обычно помогают

Выберите минимальное изменение, которое убирает выборку по каждой строке:

  • Eager load связей (предзагрузите пользователей с их заказами за один запрос)
  • Выбирайте пачками по ID (один запрос для всех orders WHERE user_id IN (...))
  • Используйте JOIN, когда вам действительно нужны поля из обеих таблиц
  • Возвращайте только нужные колонки (не загружайте большие blobs)

Проверьте исправление двумя способами: число запросов должно резко упасть, и время страницы должно улучшиться при повторном тесте (те же данные, тот же запрос).

Осторожно: «исправление» N+1 путём джойна всего подряд может обернуться плохо. Переуплотнённые JOIN приводят к большим результатам, дублирующим строкам и росту памяти в приложении. Загружайте только то, что реально отображается.

В унаследованных прототипах, сгенерированных ИИ, аудит кода часто быстро выявляет N+1‑горячие точки, особенно в списках, дашбордах и админках.

Отсутствующие индексы: самый быстрый выигрыш для многих медленных приложений

Индекс — это как указатель в книге. Без него вы листаете страницу за страницей, пока не найдёте нужный абзац. С базой данных так же: без индекса часто приходится читать много строк, чтобы найти нужные совпадения.

Наиболее распространённые пропуски скучны — и это хорошая новость, потому что их легко исправить. Проверьте колонки, которые часто используются в WHERE, JOIN и ORDER BY. Внешние ключи часто оказываются причиной, особенно в схемах, созданных автоматически, где связи есть в коде, но база не получила соответствующих ограничений и индексов.

Простой пример: если приложение загружает заказы пользователя с WHERE user_id = ? ORDER BY created_at DESC, обычно нужен индекс, соответствующий способу поиска. Часто достаточно одиночного индекса на user_id, но если вы одновременно фильтруете и сортируете, выгоднее составной индекс (user_id, created_at).

Короткие правила при выборе индекса:

  • Индексируйте колонки, которые часто появляются в WHERE, JOIN или ORDER BY для «горячих» запросов.
  • Предпочитайте составные индексы, если часто фильтруете по нескольким колонкам вместе.
  • Убедитесь, что колонки внешних ключей индексированы, если по ним делаются JOIN.
  • Не полагайтесь на то, что ORM сам добавил правильные индексы.

Распространная ловушка — индексирование колонок с низкой кардинальностью (значения часто повторяются), например status с несколькими состояниями. Такие индексы часто не помогают, потому что БД всё равно трогает большую часть таблицы. Они также замедляют записи, потому что при вставке/обновлении требуется обновлять индекс.

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

Неограниченные сканы: когда база читает гораздо больше, чем кажется

Неограниченный скан возникает, когда запрос слишком общий и базе приходится читать огромную часть таблицы (иногда всю), чтобы вернуть небольшой результат. Если ваше приложение, созданное ИИ, медленно и становится хуже с ростом данных, часто это причина.

Типичная схема: запрос не сузит поиск достаточно рано, поэтому база читает строки, пока не найдёт нужные. Это проявляется высокой загрузкой CPU базы, долгими запросами и страницами, которые в dev выглядят нормально, а в продакшене ползут.

Тревожные признаки

В код‑ревью и логах обычно видно:

  • Запрос без WHERE по большой таблице
  • Отсутствие LIMIT на эндпоинтах списка
  • Пагинация с OFFSET (page=2000) по таблице, которая растёт
  • Выбор многих колонок (или SELECT *), когда нужно лишь несколько полей
  • Запросы «последних» элементов без ограничения по времени или категории

Если вы видите это на горячем пути (дашборд, лента), принимайте как вероятную причину.

Быстрые исправления, которые обычно помогают

Сделайте запрос менее затратным на каждый вызов. Небольшие изменения существенно снижают нагрузку:

  • Добавьте реальный фильтр (status, user_id, tenant_id, created_at) и убедитесь, что он отражает поведение пользователей
  • Перейдите с OFFSET‑пагинации на keyset (используйте last_seen id или timestamp)
  • Возвращайте меньше полей (только то, что нужно UI)
  • Добавьте временное окно для таблиц логов (последние 7 дней) и архивируйте старые данные

Осторожно с «поиском по всему». Наивный LIKE '%term%' по большим текстовым полям часто вынуждает сканы. Если поиск важен, используйте возможности полнотекстового поиска базы или ограничьте область поиска индексированными полями.

Реалистичный пример: таблица активности растёт вечно. Главная страница просит «недавнюю активность», но не фильтрует по аккаунту и использует OFFSET. При 5 тысячах строк всё ок, при 5 миллионах — начинается сканирование. Добавление фильтра по аккаунту, keyset‑пагинации и окна в 30 дней часто превращает 3‑секундный запрос в 50 мс.

В сгенерированных ИИ кодах это частая проблема: эндпоинты списков быстро делаются открытыми и остаются такими. В FixMyMess мы часто находим несколько неограниченных сканов, которые объясняют большую часть замедления при росте пользователей и данных.

Шумные ORM: много маленьких запросов складываются в проблему

Make your app feel fast
Turn a sluggish AI-generated prototype into a fast, reliable production app in 48-72 hours.

Когда приложение, созданное ИИ, медленное, база не всегда «делает тяжёлую работу». Иногда она выполняет много мелкой работы, снова и снова. "Шумный" ORM — это когда код делает много маленьких обращений к базе вместо нескольких осмысленных.

Это часто незаметно, потому что каждый запрос выглядит безобидно. Но 200 «быстрых» запросов могут быть медленнее 5 хорошо спроектированных, особенно с учётом сетевого времени и пула соединений.

Как выглядит шумность

Появляются такие шаблоны:

  • Один запрос, чтобы загрузить список, затем ещё по запросу на строку для связанных деталей
  • Запросы по полю за полем (например, загрузили пользователя, затем ещё раз спрашиваем план, команду и настройки)
  • Вычисляемые свойства или геттеры, которые выполняют запросы и вызываются в цикле
  • «Загрузить всё» включения, которые подтягивают большие связанные таблицы, которые вы не показываете
  • Повторяющиеся запросы по одной и той же строке в рамках одного запроса (нет кеша на уровень запроса)

Обычная ловушка в сгенерированном ИИ коде — аккуратно выглядящий метод модели user.displayName(), который тихо делает дополнительный запрос. Вызвав его 100 раз, вы создаёте 100 лишних обращений.

Быстрые исправления, которые обычно помогают

Сделайте запрос так, чтобы данные загружались за один проход. Подтяните список и связанные данные вместе или выполните второй запрос, покрывающий все строки (не по одному запросу на строку). Держите полезную нагрузку маленькой: выбирайте только нужные колонки и избегайте крупных связей, если страница их не использует.

Если одно и то же обращение повторяется в рамках запроса (например, «текущая команда», «текущий план», «флаги фич»), добавьте простой кеш на время запроса, чтобы попасть в базу только один раз.

После любого изменения снова измеряйте. Считайте запросы на запрос и смотрите p95 задержку, а не только среднее. Хороший результат — меньше запросов, меньше круговых поездок и заметное падение p95.

При наследовании кода, сгенерированного ИИ, FixMyMess часто быстро находит шумные ORM‑места в ходе аудита, потому что они проявляются как повторяющиеся запросы, связанные с одной конечной точкой.

Реалистичный пример: страница, которая каждую неделю становилась медленнее

Типичная история для рынка, созданного ИИ: админ‑страница Заказы сначала работала нормально, а потом постепенно стала мучительно медленной. При 200 заказах загрузка 1 секунда. Через месяц, при 10 000 заказов, — 12–20 секунд и иногда таймаут. Ничего «большого» не поменялось, но база теперь делает гораздо больше работы.

Вот что делает приложение на этой странице:

  • Запрос 1: загрузить последние 50 заказов для таблицы (обычно с фильтрами status и диапазоном дат).
  • Затем для каждой строки в таблице UI показывает имя клиента и краткий список позиций.

Скрытая проблема — N+1. Вы получаете 1 запрос списка, затем N запросов на клиентов и N запросов на позиции. При 50 строках это может дать 101 запрос (или больше). Каждый отдельный запрос «быстрый», но итоговое время накапливается, и пул соединений базы занят.

Одновременно фильтрация замедляется из‑за отсутствующего индекса. Код фильтрует по status и сортирует/фильтрует по created_at, но у базы нет подходящего индекса. С ростом таблицы база начинает сканировать гораздо больше строк, прежде чем вернуть новейшие 50.

Практическая последовательность исправлений с минимальными изменениями:

  1. Сначала измерьте: зафиксируйте общее число запросов и суммарное DB‑время для одной загрузки страницы.
  2. Исправьте индекс: добавьте составной индекс, соответствующий фильтру и сортировке (например, status + created_at). Протестируйте.
  3. Исправьте N+1: подтяните клиентов одним запросом и позиции одним запросом (или используйте eager‑load/include). Протестируйте.
  4. Добавьте лимиты и защитные механизмы пагинации (жёсткий max page size). Протестируйте.

Что обычно видно после каждого шага: изменение индекса сокращает основной запрос списка с секунд до десятков миллисекунд, но страница всё ещё может казаться медленной. Устранение N+1 обычно сокращает число запросов с ~100 до <10, и загрузка становится предсказуемой.

Учитывайте ограничения: меняйте по одному пункту, каждый раз прогоняйте тот же запрос и сверяйте результаты на реальном объёме данных. В сгенерированном ИИ коде легко «улучшить» производительность, случайно изменив поведение, поэтому важны маленькие шаги и быстрые проверки. Если код грязный (шумные ORM‑вызовы разбросаны по view‑коду), команды часто делают небольшой аудит, чтобы карту запросов составить перед масштабными рефакторами.

Частые ошибки, которые отнимают время (и иногда делают хуже)

Speed up and secure it
While improving performance, we also repair common security issues in AI-generated code.

Когда приложение, созданное ИИ, медленное, хочется взять кажущееся быстрое исправление. Проблема в том, что «быстрые» правки часто маскируют истинную причину, и замедление возвращается (или перемещается).

Исправления, которые кажутся полезными, но обычно отбрасываются назад

Типичная ловушка — добавлять кеширование до того, как вы доказали, что запрос здоров. Если страница 2 секунды из‑за 120 запросов, кеширование ответа может временно скрыть проблему, но как только кеш истечёт (или данные изменятся), всплеск вернётся.

Другой пустой путь — добавление индексов вслепую. Индекс может сильно помочь, но неправильный индекс ничего не даст или замедлит записи и увеличит объём хранения. Всегда подтверждайте, что база делает, и потом выбирайте минимальный индекс, который помогает реальному фильтру или сортировке.

Переизбыточная загрузка данных повсюду в сгенерированном коде — ещё одна проблема. Если UI нужен только имя пользователя и план, а ORM тащит всю запись пользователя с вложенными связями, вы платите за лишние чтения, память и сеть.

Наконец, «один мегазапрос» тоже может быть плохим. Люди объединяют всё в огромный JOIN, чтобы избежать N+1, и получают запрос, который сложно поддерживать, тяжело править и который всё ещё медленный из‑за больших результатов.

Пропуск верификации — самая большая ошибка

Если вы не задали базовую линию, вы не узнаете, что улучшили. До изменения кода снимите простые before/after‑метрики: время запроса, число запросов и самый медленный запрос.

Пять красных флагов, которые показывают, что вы действуете наугад:

  • Нет базовых метрик (только «кажется быстрее»)
  • Нет проверки плана запроса перед добавлением индекса
  • Нет лимитов/пагинации на эндпоинтах, которые растут со временем
  • Нет регрессионного теста для медленного пути (чтобы он не вернулся)
  • Нет повторной проверки на объёмах продакшен‑данных

В FixMyMess мы часто видим, как команды тратят дни на кеши и рефакторы, а потом обнаруживают, что реальная проблема — один отсутствующий индекс и ORM‑вызов в цикле. Быстрый путь — скучный: измерить, проверить, изменить одно, измерить снова.

Следующие шаги: закрепите выигрыш и попросите помощи при необходимости

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

Начните с небольшой «задачи», к которой можно возвращаться еженедельно. Делайте её конкретной, не теоретической.

  • Топ‑3 медленных пользовательских действия (например: вход, поиск, оформление) и их худшие запросы
  • Для каждого действия: среднее время, p95 и число запросов
  • Точные шаблоны запросов, которые это вызывают (N+1, отсутствующий индекс, неограниченный скан, много мелких ORM‑вызовов)

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

  • Задайте бюджет по числу запросов на ключевые страницы
  • Включите оповещения по медленным запросам в базе и регулярно их проверяйте
  • Прогоняйте базовый нагрузочный тест по топ‑3 действиям после каждого релиза
  • Добавьте простую проверку производительности в код‑ревью ("не добавили ли мы запросов?")
  • Поддерживайте общий набор одобренных хелперов для запросов, чтобы все использовали одинаковые паттерны

После этого планируйте рефакторы, которые убирают корень проблемы: очистку доступа через ORM (подтягивать связи разом, а не в циклах), стандартизацию пагинации и создание общих query‑helper'ов, чтобы та же ошибка не повторялась.

Если ваше приложение, созданное ИИ, медленно и было сгенерировано инструментами вроде Lovable, Bolt, v0, Cursor или Replit, подумайте о профессиональной диагностике. Такие кодовые базы часто скрывают повторяющуюся логику запросов в разных файлах: вы исправляете один эндпоинт, а ещё три продолжают тормозить.

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