Как стать автором
Поиск
Написать публикацию
Обновить

Оптимизация view с пользовательскими функциями в MS SQL

Приветствую.

Намедни пришлось разбираться с проблемой оптимизации view — таблиц MS SQL Server 2008, содержащих пользовательские функции. Так как на основных ресурсах, посвящённых этой тематике, готовых рекомендаций найдено не было, ниже делюсь своим опытом.


Описание системы и постановка задачи

Имеется некая ERP-система, интегрированная с бухгалтерией. Ядро системы содержит таблицу Passes с данными по проводкам и аналитическим разрезам (одну на все счета). По некоторым причинам обращения к таблице на чтение производятся через view-таблицу Passes_view.
Исходная таблица хорошо оптимизирована таким образом, что на текущем объёме (10 млн записей-проводок) операции как записи, так и чтения с фильтрацией по аналитикам происходят практически моментально.

Основная фильтрация во время выборок производится по полям UDN_D_Acc и UDN_C_Acc, в которых хранятся, соответственно, ID счетов дебета и кредита.

Имеется также сервер аналитических отчётов (SSAS), который периодически закачивает данные на базе предварительно созданных view-таблиц. Факт по понятным причинам базируется на выборках из таблицы проводок, так что основная задача состоит в том, чтобы оптимизировать view фактов.

Далее для примера используется часть оптимизируемой view (результирующая view будет состоять из нескольких аналогичных запросов). Код view:
create view OLAP_ProfitLoss_view
as

select S.UDN_Doc Doc_UDN, S.d1 CFU_UDN, S.d2 Descr_UDN, S.d3 ReqTrip_UDN, S.d4 AccUser_UDN, S.d5 Val_UDN, S.c2 Customer_UDN,
- S.s Sum_RUR, - S.v Sum_Val,
convert(datetime, convert(varchar(25), S.Date_Pass, 4), 4) Date_Pass,
S.od2 CustomerCustomer_UDN
from dbo.Passes_view S
where S.UDN_D_Acc = dbo.AccPlan_getAccUDN('80') and S.UDN_C_Acc in (dbo.AccPlan_getAccUDN('60'), dbo.AccPlan_getAccUDN('61'))


Выполнение запроса — минута на 150 000 записей, что примерно на порядок превосходит ожидаемое время. Причина очевидна после запуска профайлера — функция dbo.AccPlan_getAccUDN, возвращающая ID счёта по номеру, выполняется для каждой строки запроса.

Оптимизация


Первым делом проверяю свойство детерменированности функции:
select OBJECTPROPERTY (object_id ('[dbo].[AccPlan_getAccUDN]'), 'isDeterministic')

Получаю на выходе 0, перерабатываю функцию, после чего она становится детерминированной.
Время выполнения — также около минуты. Не помогло.

Вторым шагом переделываю запрос с тем, чтобы заставить оптимизатор вычислять функцию однократно:
select S.UDN_Doc Doc_UDN, S.d1 CFU_UDN, S.d2 Descr_UDN, S.d3 ReqTrip_UDN, S.d4 AccUser_UDN, S.d5 Val_UDN, S.c2 Customer_UDN,
- S.s Sum_RUR, - S.v Sum_Val,
convert(datetime, convert(varchar(25), S.Date_Pass, 4), 4) Date_Pass,
S.od2 CustomerCustomer_UDN
from dbo.Passes_view S
where S.UDN_D_Acc in (select dbo.AccPlan_getAccUDN('80'))
and S.UDN_C_Acc in (select dbo.AccPlan_getAccUDN('60') union all select dbo.AccPlan_getAccUDN('61'))


20 секунд, что всё равно слишком много. При этом появился забавный эффект: если из двух условий оставить одно (любое), время выполнения сокращается до менее чем одной секунды. Явно, проблема в построении плана запроса — надо использовать прямые инструкции планировщику.

Третий вариант:
select S.UDN_Doc Doc_UDN, S.d1 CFU_UDN, S.d2 Descr_UDN, S.d3 ReqTrip_UDN, S.d4 AccUser_UDN, S.d5 Val_UDN, S.c2 Customer_UDN,
- S.s Sum_RUR, - S.v Sum_Val,
convert(datetime, convert(varchar(25), S.Date_Pass, 4), 4) Date_Pass,
S.od2 CustomerCustomer_UDN
from dbo.Passes_view S
where S.UDN_D_Acc in (select dbo.AccPlan_getAccUDN('80'))
and S.UDN_C_Acc in (select dbo.AccPlan_getAccUDN('60') union select dbo.AccPlan_getAccUDN('61'))
option (hash join)


Здесь использовалась инструкция HASH — при этом ядро SQL SERVER вначале создаст в памяти слепок каждой выборки, после чего уже будет производить по ним join.
Время выполнения — 1 секунда. То, что надо, но есть одно «но»: выражение option (hash join) непонятно из каких соображений нельзя применить внутри view.
В принципе, select * from OLAP_ProfitLoss_view option (hash join) даёт также 1 секунду (что, кстати, непонятно), но OLAP-сервер самостоятельно строит обращения к БД и не позволяет вставлять в них предопределённые опции. То есть, вся оптимизация должна быть зашита внутри view. Кроме того, метод «некрасив»: у меня во view могут появиться и другие join'ы, которые могут дать провал по производительности при использовании хэширования. Надо научиться применять хинт прицельно на оптимизируемые условия.

Четвёртый вариант выглядел как
select S.UDN_Doc Doc_UDN, S.d1 CFU_UDN, S.d2 Descr_UDN, S.d3 ReqTrip_UDN, S.d4 AccUser_UDN, S.d5 Val_UDN, S.c2 Customer_UDN,
- S.s Sum_RUR, - S.v Sum_Val,
convert(datetime, convert(varchar(25), S.Date_Pass, 4), 4) Date_Pass,
S.od2 CustomerCustomer_UDN
from dbo.Passes_view S
inner hash join
(select dbo.AccPlan_getAccUDN('80') Acc_UDN) Acc80
on Acc80.Acc_UDN = S.UDN_D_Acc
inner hash join
(select dbo.AccPlan_getAccUDN('60') Acc_UDN union select dbo.AccPlan_getAccUDN('61') Acc_UDN) Acc60_80
on Acc60_80.Acc_UDN = S.UDN_C_Acc


Больше двух минут, и профайлер показывает 150 000 вычислений функций. То есть, в такой конструкции хинт на детерминированной функции просто не работает.
Мысль о том, что это очевидный баг движка СУБД, греет, но задача не решена. Из общих соображений надо два условия заменить одним:
select S.UDN_Doc Doc_UDN, S.d1 CFU_UDN, S.d2 Descr_UDN, S.d3 ReqTrip_UDN, S.d4 AccUser_UDN, S.d5 Val_UDN, S.c2 Customer_UDN,
- S.s Sum_RUR, - S.v Sum_Val,
convert(datetime, convert(varchar(25), S.Date_Pass, 4), 4) Date_Pass,
S.od2 CustomerCustomer_UDN
from dbo.Passes_view S
inner join
(select dbo.AccPlan_getAccUDN('80') Acc_D_UDN, dbo.AccPlan_getAccUDN('60') Acc_C_UDN
union
select dbo.AccPlan_getAccUDN('80') Acc_D_UDN, dbo.AccPlan_getAccUDN('61') Acc_C_UDN) Acc
on Acc.Acc_D_UDN = S.UDN_D_Acc and Acc.Acc_C_UDN = S.UDN_C_Acc


Результат — искомая 1 секунда. При этом, как видно по тексту запроса, даже не используются хинты: здесь оптимизатор сам догадался прохешировать подзапрос.

Выводы

1. Даже детерминированные функции от констант в условии where будут вызываться столько раз, сколько строк фильтруется в датасете.
2. В случае возможности менять окружение и при использовании запроса без других join'ов можно обойтись опцией хэширования, что принудительно вычислит значения функций один раз.
3. По непонятным причинам один подзапрос в where будет хэшироваться оптимизатором, два и более — нет.
4. Похоже на то, что вообще вызов пользовательских функций более чем в одном подзапросе сбивает оптимизатор с толку и заставляет его считать «в лоб».
5. В случае использования пользовательских функций в рамках единственного подзапроса / условия, оптимизатор способен определить детерминированность и отработать правильно.

По пунктам 3-5 официальный хелп молчит — надеюсь, статья будет полезна другим разработчикам.
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.