За обновлениями можно следить в telegram-канале https://t.me/quasiart

Так бывает, что с ростом функционала системы возрастает и нагрузка. Нагрузка может расти на различные части: БД, количество подключений, файловое хранилище и пр.

Так вышло, что на мою долю выпало оптимизировать взаимодействие с БД. Проекту 2 года, бизнес-логика имеет много разветвлений, и в какой-то момент получение списка основных сущностей (40 на страницу) стал занимать порядка 20 секунд. Специфика проекта прощала такую низкую производительность, но планировалось дальнейшее усложнение логики, которое повлекло бы за собой кратное увеличение времени генерации ответа сервером.

Попытка №0

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

Было:

// На каждую сущность делается отдельная выборка
for (const entity of entities) {
  entity.documentsCount = await entityRepository.getDocumentsCounts(entity.id);
}

Стало:

// Один запрос на каждую пачку данных
const entitiesDocumentsCounts = await entityRepository.getDocumentsCounts(entitiesIds);
for (const entity of entities) {
  entity.documentsCount = entitiesDocumentsCounts.get(entity.id);
}

Эта оптимизация дало лишь незначительный прирост скорости: примерно в 2 раза. Но такой результат всё равно не устраивал. 

Попытка №1

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

Случай наверняка редкий, но я с ним столкнулся и такое решение тоже увеличило скорость.

Попытка №2

Затем я почти случайно find заменил на QueryBuilder, и сначала не поверил глазам: скорость увеличилась раз в 5-10. 

Двоякое чувство: вроде бы задача решена, но я не приложил каких-либо усилий. Я привык, что чем выше уровень абстракции, тем хуже производительность, но всё равно такая разница удивила. Вероятно, при использовании find/findOne/findOneOrFail и пр. делается какая-то дополнительная работа. Ребята в команде тоже удивились, но бенчмарки не врали :-)

Вывод

Раньше мы в команде не использовали QueryBuilder, если запрос можно описать с помощью find, на что обращали внимание на Code Review, но теперь в пользу QueryBuilder выступает и производительность.