Тот же запрос выполняется быстрее, когда вне процесса - PullRequest
0 голосов
/ 03 декабря 2018

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

Позвольте мне сначала заявить о том, что мы попробовали и потерпели неудачу:

  • Предотвращение перехвата параметров с помощью option (Recompile) иoption (optiizes for (@var UNKNOWN)
  • Этот поток . Переменные, которые кажутся проблемой, на самом деле являются локальными, а не параметрами proc.

Вот запрос, полученный изнутри proc / cursors.

 select @tpdim1 = dim1, @tpdim2 = dim2, @typecalc = typecalc
    from loyalty_policy where code=@loop2_loyalty_policy

Примечание: @ loop2_loyalty_policy - это переменная, взятая из результата внутреннего курсора, и имеет одно значение. code - это PK дляloyalty_policy таблица. Таким образом, @ tpdim1 и @ tpdim2 имеют по одному значению каждое.

SET STATISTICS PROFILE ON 
SET STATISTICS    xml on           
                  insert into @tbl_loyal_loop2 (cnt, store, map, pda, insdate, line, item, loyalty_policy_data, loyal_calc, loyalty_policy)
                  select @cnt, t.store, t.map, t.pda, t.insdate, t.line, t.item, ld.tab_id,  
                  case @typecalc
                        when 1 then convert(bigint,round(isnull(t.valueFromTran,0.00) * ld.value , 0 ) )
                        when 2 then convert(bigint,round(isnull(t.netvalue,0.00) * ld.value , 0 ) )
                        when 3 then convert(bigint,isnull(t.qty,0) * ld.value )
                        when 4 then convert(bigint,round(isnull(t.valueFromPrice2,0.00) * ld.value , 0 ) )
                        when 5 then convert(bigint,round(isnull(t.valueFromPrice3,0.00) * ld.value , 0 ) )
                        when 6 then convert(bigint,round(isnull(t.valueFromPrice4,0.00) * ld.value , 0 ) )
                  else 0 end
                  ,@loop2_loyalty_policy
                  from loyalty_policy_data ld-- with (index=ind_loyalty_policy_02)
                              inner join #tbl_data t on t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive
                  where ld.loyalty_policy = @loop2_loyalty_policy 
                  and ld.tdateactive >= @from_rundate and ld.fdateactive <= @to_rundate
                  and t.dbupddate > @loop1_dbupddate  
                  and
                        case when @tpdim1 is null then '' 
                        else  
                              case  @tpdim1 
                                    when 'STORE'            then t.store when 'BRAND' then t.brand  when 'CAT1' then t.cat1   when 'CAT2' then t.cat2   when 'CAT3' then t.cat3   when 'ITEM' then t.item    
                                    when 'CUSTGROUP'  then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
                                    when 'CUSTOMER'         then @customer
                              else '' end
                        end
                        = case when @tpdim1 is null then '' else ld.dim1 end
                  and 
                        case when @tpdim2 is null then '' 
                        else  
                              case  @tpdim2 
                                    when 'STORE'            then t.store when 'BRAND' then t.brand  when 'CAT1' then t.cat1   when 'CAT2' then t.cat2   when 'CAT3' then t.cat3   when 'ITEM' then t.item    
                                    when 'CUSTGROUP'  then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
                                    when 'CUSTOMER'         then @customer                     
                              else '' end
                        end
                        = case when @tpdim2 is null then '' else ld.dim2 end
SET STATISTICS    xml off    

SET STATISTICS XML для вышеуказанных возвратов этот план .

При попытке его отладки мы изолировали запрос в следующей форме (здесь вы также можете увидеть, как создается таблица #a, которая имеет точно такие же данные, что и предыдущая #tbl_data):

drop table #a;
select dt.dbupddate, dt.insdate, dt.map, dt.pda, pt.line, pt.item, 
( pt.exp_qty - pt.imp_qty)  as qty,  
( pt.exp_value + pt.imp_value )  as netvalue, 
( (document.exp_val - document.imp_val) * (pt.netvalue - pt.vat_value) )  as valueFromTran,  
( (document.exp_val - document.imp_val) * ( ( (pt.qty - pt.qty_gift) * isnull(pt.price2,0.00) ) * (1.00-( pt.disc_perc / 100)) ) ) as valueFromPrice2, 
( (document.exp_val - document.imp_val) * ( ( (pt.qty - pt.qty_gift) * isnull(pt.price3,0.00) ) * (1.00-( pt.disc_perc / 100)) ) ) as valueFromPrice3, 
( (document.exp_val - document.imp_val) * ( ( (pt.qty - pt.qty_gift) * isnull(pt.price4,0.00) ) * (1.00-( pt.disc_perc / 100)) ) ) as valueFromPrice4, 
dt.store, item.brand, item.cat1, item.cat2, item.cat3, customer.custgroup, customer.custgroup2, customer.custgroup3 
into #a
from document with (nolock) 
      inner join dt with (nolock) on dt.doccode = document.code 
      inner join store with (nolock) on store.code = dt.store and store.calc_loyal = 1 
      inner join customer with (nolock) on customer.code = dt.customer  
      inner join pt with (nolock) on dt.map = pt.map and dt.pda=pt.pda 
      inner join item with (nolock) on item.code = pt.item and item.itemtype in (select code from itemtype with (nolock) where vsales = 1)
where dt.canceled = 0 and document.is_opposite = 0 and document.type = 3 and dt.customer=N'EL4444444'
and dt.insdate >= '20180109' and dt.insdate <= '20190108' ;



SET STATISTICS PROFILE ON 
                  select t.store, t.map, t.pda, t.insdate, t.line, t.item, ld.tab_id,  
                  case 4
                        when 1 then convert(bigint,round(isnull(t.valueFromTran,0.00) * ld.value , 0 ) )
                        when 2 then convert(bigint,round(isnull(t.netvalue,0.00) * ld.value , 0 ) )
                        when 3 then convert(bigint,isnull(t.qty,0) * ld.value )
                        when 4 then convert(bigint,round(isnull(t.valueFromPrice2,0.00) * ld.value , 0 ) )
                        when 5 then convert(bigint,round(isnull(t.valueFromPrice3,0.00) * ld.value , 0 ) )
                        when 6 then convert(bigint,round(isnull(t.valueFromPrice4,0.00) * ld.value , 0 ) )
                  else 0 end
                  ,'003'
                  --select count(*)
                  from loyalty_policy_data ld with (index=ind_loyalty_policy_02)
                              inner join #a t on t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive
                  where ld.loyalty_policy = '003' 
                  --and ld.tdateactive >= '20180109' and ld.fdateactive <= '20190108'
                  and t.dbupddate > '20000101'
      and 
                        case when 'CUSTOMER' is null then '' 
                        else  
                              case  'CUSTOMER' 
                                    when 'STORE'            then t.store when 'BRAND' then t.brand  when 'CAT1' then t.cat1   when 'CAT2' then t.cat2   when 'CAT3' then t.cat3   when 'ITEM' then t.item    
                                    when 'CUSTGROUP'  then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
                                    when 'CUSTOMER'         then 'EL0134366'
                              else '' end
                        end
                        = case when 'CUSTOMER' is null then '' else ld.dim1 end
                  and 
                        case when 'BRAND' is null then '' 
                        else  
                              case  'BRAND' 
                                    when 'STORE'            then t.store when 'BRAND' then t.brand  when 'CAT1' then t.cat1   when 'CAT2' then t.cat2   when 'CAT3' then t.cat3   when 'ITEM' then t.item    
                                    when 'CUSTGROUP'  then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
                                    when 'CUSTOMER'         then 'EL0134366'

                              else '' end
                        end
                        = case when 'BRAND' is null then '' else ld.dim2 end
SET STATISTICS PROFILE off    

И здесь - это план выполнения. Он работает ОЧЕНЬ быстроэр.

Почему такая огромная разница?Из моих ограниченных знаний об анализе выполнения я заметил

  1. Первый (медленный) запрос операции index spool имеет оценочные строки ~ 9700, но фактические строки - 3 миллиона.
  2. Во втором запросе использовалось много операций с параллелизмом
  3. Единственная «реальная» разница, которую я вижу во втором запросе, - это подставляемые вручную значения значений @ tpdim1 и @ tpdim2. Конечно, , когда мы вошли в код процедуры первого запроса и заменили @ tpdim1 & @ tpdim2 на единственные значения, которые они должны получить, он работал так же быстро, как второй запрос .

Не могли бы вы объяснить эту разницу и предложить несколько советов по исправлению процедуры?


Редактировать: Как рекомендовал Смеющийся Вергилий, я заменил литералы во втором запросе переменнымиранее заявлено, и снова он работает медленно!


Редактировать 2: У меня есть дополнительная информация о дальнейших исследованиях.

Сначала я выделил проблему в этой строке:

case when @tpdim1 is null then '' <- Используется медленный план </p>

case when 'CUSTOMER' is null then '' <- Используется быстрый план </p>

Это верно для специального запроса, не нужно беспокоиться о spcs и / иликурсоры.

Это продолжает происходить, даже если я изменяю код на рекомендованную динамическую структуру, где.

Я еще не создал никаких данных выборки, но важную информацию (как можно видеть впланы), что loyalty_policy_data имеет около 720k строк, если мы фильтруем только по loyalty_policy = @loop2_loyalty_policy.Тем не менее, если мы оценим условие @ tpdim1, которое по существу равно dim1 = N'EL0134366 ', возвращаемых строк будет только 4.

Разница в плане, тогда, когда это условие оценивается в отношенииусловия проверки даты.

В быстром плане он оценивается первым - при поиске индекса для значения политики лояльности он добавляет предикат (без поиска).Хотя этот предикат находится за пределами индекса, возвращаемых строк равно 4, а все остальные операторы имеют «логические» размеры.

В отличие от этого, медленный план болезненно игнорирует этот предикат, пока не станет слишком поздно.Если я понял правильно, он создает вложенные циклы на loyalty_policy_data в качестве внешней таблицы (что безумие).Он передает необходимые столбцы в качестве внешних ссылок.Для каждого такого кортежа папка индекса сканирует таблицу # (~ 1k строк), находит около 250 результатов и передает ее фильтру, который окончательно выполняет фильтрацию tpdim1.Таким образом, 250 * 700 тыс. Строк передаются оператору фильтра.

Так что теперь я думаю, что знаю, что происходит.Но я не могу понять, почему.

Ответы [ 3 ]

0 голосов
/ 07 декабря 2018

после очистки запроса для удобства чтения, у меня есть следующее:

insert into @tbl_loyal_loop2 
( cnt, 
  store, 
  map, 
  pda, 
  insdate, 
  line, 
  item, 
  loyalty_policy_data, 
  loyal_calc, 
  loyalty_policy
)
select 
      @cnt, 
      t.store, 
      t.map, 
      t.pda, 
      t.insdate, 
      t.line, 
      t.item, 
      ld.tab_id,
      convert(bigint, round( coalesce(
         case @typecalc
               when 1 then t.valueFromTran
               when 2 then t.netvalue
               when 3 then t.qty
               when 4 then t.valueFromPrice2
               when 5 then t.valueFromPrice3
               when 6 then t.valueFromPrice4
               else 0 
            END,   0.00) * ld.value , 0 ) ),
      @loop2_loyalty_policy
   from 
      loyalty_policy_data ld  -- with (index=ind_loyalty_policy_02)
         inner join #tbl_data t 
            on t.insdate >= ld.fdateactive 
            and t.insdate <= ld.tdateactive
   where 
          ld.loyalty_policy = @loop2_loyalty_policy 
      and ld.tdateactive >= @from_rundate 
      and ld.fdateactive <= @to_rundate
      and t.dbupddate > @loop1_dbupddate  
      and (   @tpdim1 is null
           OR ld.dim1 = case @tpdim1
                           when 'STORE' then t.store 
                           when 'BRAND' then t.brand  
                           when 'CAT1' then t.cat1   
                           when 'CAT2' then t.cat2   
                           when 'CAT3' then t.cat3   
                           when 'ITEM' then t.item    
                           when 'CUSTGROUP' then t.custgroup 
                           when 'CUSTGROUP2' then t.custgroup2 
                           when 'CUSTGROUP3' then t.custgroup3
                           when 'CUSTOMER' then @customer
                           else ''
                          END )
      and (   @tpdim2 is null
           OR ld.dim2 = case when @tpdim1
                         when 'STORE' then t.store 
                         when 'BRAND' then t.brand  
                         when 'CAT1' then t.cat1
                         when 'CAT2' then t.cat2
                         when 'CAT3' then t.cat3
                         when 'ITEM' then t.item    
                         when 'CUSTGROUP' then t.custgroup 
                         when 'CUSTGROUP2' then t.custgroup2 
                         when 'CUSTGROUP3' then t.custgroup3
                         when 'CUSTOMER' then @customer
                         else '' 
                      END )

Кроме того, я хотел бы убедиться, что у вас есть составной индекс для вашей таблицы loyalty_policy_data ... индекс включен (loyalty_policy,tdateactive, fdateactive, dbupddate, dim1, dim2)

Таким образом, вы квалифицируете все поля, используемые в критериях фильтрации WHERE.Не полагайтесь только на индекс ключа ... но ключ ПЛЮС даты помогут оптимизировать конкретный диапазон дат без необходимости возвращаться к страницам необработанных данных, но могут оптимизировать условия СОЕДИНЕНИЯ запроса, основанные на значениях в INDEX..

Что касается вашей временной таблицы #tbl_data, убедитесь, что у вас есть индекс (insdate), так как это единственный базисный критерий JOIN (в случае, если у вас еще нет индекса для этой таблицы).

КОММЕНТАРИЙ -

Из вашего комментария о медленном и быстром запросе, основанном на нулевом значении

@ tpdim1 = NULL против 'CUSTOMER' = NULL

фиксированная строка 'CUSTOMER' НИКОГДА не равна нулю, поэтому она никогда не должна рассматривать ее по нулевому пути.Фиксированная строка 'CUSTOMER' против переменной @customer, равной нулю или сравниваемой с переменной / в случае, когда ld.dim1 и ld.dim2 соответственно сравниваются с нулем ... возможно, то, что нужно проверить, следует изменить с

  and (   @tpdim1 is null
               OR ld.dim1 = case @tpdim1
                               when 'STORE' then t.store 
                               when 'BRAND' then t.brand  ... end
     )

до

  and ld.dim1 = case @tpdim1
                when NULL then ''
                when 'STORE' then t.store 
                when 'BRAND' then t.brand  ... end

То же, что и в случае ld.dim2 / когда.Включите «NULL» в качестве первого проверенного значения для тестов @ tpdim1 (и @ tpdim2).

0 голосов
/ 10 декабря 2018

Вообще говоря, запрос с literal value быстрее, чем запрос с proc parameter или local variable.

Когда используется буквальное значение, Optimizer создаст специальный план только для этого значения, если Forced Parameterization не включен

Оптимизатор также может сделать Trivial Plan или Простой Parameterize Plan, но в вашем случае это не так.

Когда вы используете параметр, оптимизатор создаст план дляэтот параметр, который называется Parameter Sniffing, а затем повторно использовать этот план.

Option Recompile - это один из способов решения этой проблемы: создайте план для каждого отдельного значения переменной, чтобы поддерживать «оценку количества элементов».Это очень короткий

Поэтому запрос с литеральным значением всегда будет быстрее.

Позвольте мне сначала заявить, что мы попробовали и потерпели неудачу:

• Избегание перехвата параметровиспользуя option (перекомпилировать) и option (optiizes for (@var UNKOWN)

• Этот поток. Похоже, что переменные являются локальными, а не параметрами proc.

Вы терпите неудачу, потому что ваш запрос написан очень плохо (с должным уважением).

Не используйте курсор. Кажется, что в вашем случае можно избежать курсора

Опубликовать полный запрос proc с переменнойпараметр, потому что логика получения значения в @ loop2_loyalty_policy и т. д. не ясна. Это поможет дать правильное предложение «избегать курсора».

case when @tpdim1 is null: эту полную логику можно создать и вставить в саму таблицу Temp, так что новый столбец можно сразу использовать в join.Надеюсь, вы понимаете мою идею и язык.

1.Первый (медленный) запрос наоперация спулинга индекса имеет оценочные строки ~ 9700, но фактические строки составляют 3 млн.

Из-за высокой оценки мощности, выполненной optmizer, в случае неправильного соединения

я не являюськонечно, если это, безусловно, улучшит ваш запрос и оценку кардинальности, потому что я не на 100% понял ваш запрос.

Но изменение условия соединения часто помогает, например,

Прочитайте внимательно здесь, я не уверенкакие данные есть в столбцах loyalty_policy и t.insdate. Не похоже, что вам нужно такое сложное объединение, как показано ниже.

В случае, если вам действительно это нужно, вы можете alter join condition как показано ниже один раз.

from loyalty_policy_data ld with (nolock)
 inner join #tbl_data t on ld.loyalty_policy = @loop2_loyalty_policy
 and ld.tdateactive >= @from_rundate and ld.fdateactive <= @to_rundate 
and t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive
                  where t.dbupddate > @loop1_dbupddate 

Основная цель - избежать курсора.

0 голосов
/ 07 декабря 2018

Чтобы ответить на ваш вопрос:

Четкое и воспроизводимое объяснение того, как и почему анализатор запросов ведет себя по-разному в этих случаях

Оптимизатор запросов ведет себя по-разному в этих случаях.случаев, поскольку план с переменными должен быть действительным для любого возможного будущего значения параметров, поэтому оптимизатор создает сложный общий план, который будет давать правильные результаты, даже если параметры равны NULL.

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


Мартин Смит указал в комментарии, что вы используете версию сервера 10.0.2531.0, которая является пакетом обновления 1 (SP1) 2008 года и для которой не включена оптимизация встраивания параметров.Вам нужно было бы минимум SP1 CU5 в этой ветке, чтобы OPTION (RECOMPILE) работал правильно (как я ожидал, это сработает в пояснении ниже).

Эрланд Соммарског также говорит об этом вего статья упомянута ниже.Он говорит, что вам нужно быть как минимум с пакетом обновления 2 (SP2).

Если вы не можете обновить сервер, ознакомьтесь со старой версией статьи Эрланда Условия динамического поиска в T-SQL версии для SQL 2005и ранее , чтобы увидеть, как справиться с этой ситуацией, когда собственно OPTION (RECOMPILE) недоступно.


Вот мой оригинальный ответ.

Я знаю, что вы сказали, что выпопробовал, но я все равно прошу вас перепроверить.Глядя на ваши симптомы OPTION (RECOMPILE) должно помочь.

Вам необходимо добавить эту опцию в основной запрос.Не для всей хранимой процедуры.Например,

insert into @tbl_loyal_loop2 (cnt, store, map, pda, insdate, line, item, loyalty_policy_data, loyal_calc, loyalty_policy)
select @cnt, t.store, t.map, t.pda, t.insdate, t.line, t.item, ld.tab_id,  
case @typecalc
    when 1 then convert(bigint,round(isnull(t.valueFromTran,0.00) * ld.value , 0 ) )
    when 2 then convert(bigint,round(isnull(t.netvalue,0.00) * ld.value , 0 ) )
    when 3 then convert(bigint,isnull(t.qty,0) * ld.value )
    when 4 then convert(bigint,round(isnull(t.valueFromPrice2,0.00) * ld.value , 0 ) )
    when 5 then convert(bigint,round(isnull(t.valueFromPrice3,0.00) * ld.value , 0 ) )
    when 6 then convert(bigint,round(isnull(t.valueFromPrice4,0.00) * ld.value , 0 ) )
else 0 end
,@loop2_loyalty_policy
from loyalty_policy_data ld -- with (index=ind_loyalty_policy_02)
            inner join #tbl_data t on t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive
where ld.loyalty_policy = @loop2_loyalty_policy 
and ld.tdateactive >= @from_rundate and ld.fdateactive <= @to_rundate
and t.dbupddate > @loop1_dbupddate  
and
    case when @tpdim1 is null then '' 
    else  
            case  @tpdim1 
                when 'STORE'            then t.store when 'BRAND' then t.brand  when 'CAT1' then t.cat1   when 'CAT2' then t.cat2   when 'CAT3' then t.cat3   when 'ITEM' then t.item    
                when 'CUSTGROUP'  then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
                when 'CUSTOMER'         then @customer
            else '' end
    end
    = case when @tpdim1 is null then '' else ld.dim1 end
and 
    case when @tpdim2 is null then '' 
    else  
            case  @tpdim2 
                when 'STORE'            then t.store when 'BRAND' then t.brand  when 'CAT1' then t.cat1   when 'CAT2' then t.cat2   when 'CAT3' then t.cat3   when 'ITEM' then t.item    
                when 'CUSTGROUP'  then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
                when 'CUSTOMER'         then @customer                     
            else '' end
    end
    = case when @tpdim2 is null then '' else ld.dim2 end
OPTION(RECOMPILE);

OPTION (RECOMPILE) не для того, чтобы облегчить анализ параметров, а для того, чтобы оптимизатор мог вставить фактические значения параметров в запрос.Это дает оптимизатору свободу в упрощении логики запроса.

Тип вашего запроса выглядит как Условия динамического поиска , и я настоятельно рекомендую прочитать эту статью Эрланда Соммарскога.

Кроме того, вместо

and
    case when @tpdim1 is null then '' 
    else  
            case  @tpdim1 
                when 'STORE'            then t.store when 'BRAND' then t.brand  when 'CAT1' then t.cat1   when 'CAT2' then t.cat2   when 'CAT3' then t.cat3   when 'ITEM' then t.item    
                when 'CUSTGROUP'  then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
                when 'CUSTOMER'         then @customer
            else '' end
    end
    = case when @tpdim1 is null then '' else ld.dim1 end

я бы написал это немного по-другому:

and
(
    @tpdim1 is null
    OR
    (
            ld.dim1 =
            case @tpdim1
                when 'STORE'      then t.store 
                when 'BRAND'      then t.brand  
                when 'CAT1'       then t.cat1   
                when 'CAT2'       then t.cat2   
                when 'CAT3'       then t.cat3   
                when 'ITEM'       then t.item    
                when 'CUSTGROUP'  then t.custgroup 
                when 'CUSTGROUP2' then t.custgroup2 
                when 'CUSTGROUP3' then t.custgroup3
                when 'CUSTOMER'   then @customer
                else ''
            end
    )
)

С OPTION (RECOMPILE), когда @tpdim1 имеет значение CUSTOMER и @customerимеет значение EL0134366, оптимизатор должен преобразовать этот оператор в простой

and
(
    ld.dim1 = `EL0134366`
)

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

Обратите внимание, что option (optimize for UNKNOWN) здесь не может помочь.optimize for UNKNOWN должен будет сгенерировать общий план, действительный для любого возможного значения параметров.

...