BarsUp.Net - УНП - OR выше гор

BarsUp.Net - УНП - OR выше гор

Тимур Гизатулин

В нашей работе постоянно приходится сталкиваться с деградацией производительности на некоторых запросах данных. Некоторые люди говорят что всему виной ORM/Фреймворк/Звезды - дескать невозможно контролировать сгенерированный фреймворком sql-запрос, остается надеятся только на удачу и ответственность разработчиков. Однако это не совсем так - в любой ситуации нужно постараться определить исходную причину снижения скорости запросов. В этой короткой статье я хочу найти плохое место в проекте, и показать как можно его пофиксить - возможно тебе, уважаемый читатель, это пригодится в твоем проекте.

Рассматривать всегда лучше на живом (или еле живом) примере, и этот пример я нашел в моем любимом проекте УНП. Это хороший и интересный проект, в котором я и команда BarsUp.Net принимали участие практически со старта, мы сами писали не самый оптимальный код, и не исключено что рассматриваемый сегодня код - мой ))

В качестве примера я взял раздел 10.2 Паспорта регионального проекта

Реестр рисков проекта создан в Конструкторе, однако его сервис переопределен в кастомной части проекта так как сбор данных для него не удается (пока что) выразить в виде фильтров в настройках реестра. В кастомном сервисе есть следующий код:

Метод GetAdditionalIdsAsync

Метод GetAdditionalIdsAsync предназначен для получения идентификаторов записей, которые следует загрузить в раздел паспорта проекта, причем получение этих идентификаторов довольно нетривиально - выполняется несколько запросов в БД, результаты этих промежуточных запросов используются в качестве аргументов для следующих запросов и т.д. В целом, это обусловлено жесткой и нетривиальной логикой определения относится ли конкретный Риск к указанному периоду, имеется ли по нему исполнение, связан ли он с Контрольными точками результатов или мероприятий и вот это вот все.

Изменить требования мы не можем, риски должны собираться именно так! Однако, мы можем изменить свой подход к сбору этих данных.

Поехали!

Для начала - замерим скорость получения доп.идентификаторов. По колхозному бережно раскидав Stopwatch внутри сочного тела метода, получим следующие показатели:

  resultExecutions    : 00:00:14.1929042
  onlyApprovedResExecIds : 00:00:00.1937081
  resultRisks      : 00:00:00.2484064
  cpRisks1        : 00:00:04.6721426
  cpRisks2        : 00:00:09.2017699
  budgetRisks      : 00:00:01.9781154
  eventRisks       : 00:22:38.8843383
  resultIds       : 00:00:00.9148345

Как мы видим, больше всего времени занимает получение Рисков Мероприятий. Для начала, нам нужно понять почему запрос работает аж ДВАДЦАТЬ ДВЕ минуты. Рассмотрим код, выполняющий запрос этих данных:

Можно заметить, что в самый большой по размеру предикат содержит условие ИЛИ, причем каждая из сторон в себе содержит обращение к executionQuery. При генерации Sql-запроса каждое из этих обращений будет заменено на select exists(подзапрос_по_executionQuery). К тому же, при использовании условия ИЛИ ухудшается работа анализатора, так как чем больше разных условий, тем меньше вероятность что оптимизатор сможет выбрать эффективную стратегию запроса данных. Обратите внимание, что конкретно в этой ситуации можно разделить один запрос с ИЛИ на два отдельных запроса, в каждом из которых будут только условия И, что улучшит селективность. Давайте это проверим!

Большой запрос с ИЛИ разделен на два запроса

Cколько времени теперь занимает выполнение метода ?

Было:
  eventRisks       : 00:22:38.8843383

Стало:
  eventRisks1      : 00:07:41.4033819
  eventRisks2      : 00:01:07.3075186

Теперь запрос выполняется почти втрое быстрее, да и в планах есть позитивные изменения - оригинальный план, после разделения - план первой части, и план второй.


В целом, мы получили неплохой прирост в производительности этого куска кода, однако это совсем не предел! Каждый выполненный запрос несет в себе накладные расходы на поднятие соединения, ожидание, создание ридера, reflection на создание объектной модели (кешируется, но все же) и т.д.

Давайте же воспользуемся инструментами NHibernate и постараемся выжать из запроса еще сколько-нибудь секунд! Быстренько, по крестьянски, был создан метод-расширение для IQueryable<TEntity> который выглядит примерно вот так:

Сначала для каждого из предикатов мы формируем отдельный Future - "обещание" что в будущем этот элемент вернет что-то из БД. Полезность этого механизма в том, что разработчик может создать несколько таких "обещаний", а выполнены они ВСЕ будет при первом обращении к любому из них за одно обращение к БД.

Сравним результаты выполнения:

Было:
  eventRisks1      : 00:07:41.4033819
  eventRisks2      : 00:01:07.3075186

Стало:
  eventRisks       : 00:04:00.1174783

Итак, мы получили дополнительное двукратное ускорение и в итоге имеем снижение времени формирования рисков мероприятий с 22 минут до 4. При этом написали всего пару строк кода.

Но и это еще не предел! В следующем посте мы постараемся добиться дополнительного ускорения.






Report Page