BarsUp.Net - УНП - OR выше гор
Тимур ГизатулинВ нашей работе постоянно приходится сталкиваться с деградацией производительности на некоторых запросах данных. Некоторые люди говорят что всему виной ORM/Фреймворк/Звезды - дескать невозможно контролировать сгенерированный фреймворком sql-запрос, остается надеятся только на удачу и ответственность разработчиков. Однако это не совсем так - в любой ситуации нужно постараться определить исходную причину снижения скорости запросов. В этой короткой статье я хочу найти плохое место в проекте, и показать как можно его пофиксить - возможно тебе, уважаемый читатель, это пригодится в твоем проекте.
Рассматривать всегда лучше на живом (или еле живом) примере, и этот пример я нашел в моем любимом проекте УНП. Это хороший и интересный проект, в котором я и команда BarsUp.Net принимали участие практически со старта, мы сами писали не самый оптимальный код, и не исключено что рассматриваемый сегодня код - мой ))
В качестве примера я взял раздел 10.2 Паспорта регионального проекта
Реестр рисков проекта создан в Конструкторе, однако его сервис переопределен в кастомной части проекта так как сбор данных для него не удается (пока что) выразить в виде фильтров в настройках реестра. В кастомном сервисе есть следующий код:
Метод 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. При этом написали всего пару строк кода.
Но и это еще не предел! В следующем посте мы постараемся добиться дополнительного ускорения.