Привет .
Я Михаил, fullstack-разработчик, занимаюсь интернет-проектами: от сайтов до систем автоматизации.
Отправить мне сообщение

Производительность TypeORM: find vs QueryBuilder

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

Так вышло, что на мою долю выпало оптимизировать взаимодействие с БД. Проекту 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 выступает и производительность. 

Но за голый SQL пока никто не топит, потому что время программиста всё-таки дороже машинного времени.