Необычная медлительность представления SQL Server - PullRequest
1 голос
/ 06 марта 2019

Я пишу приложение, которое запрашивает базу данных стороннего приложения.

Мое приложение создает «библиотеку» представлений SQL в сторонних БД, поэтому мои последующие запросы гораздо проще писать и читать. (Не только из-за логики домена моего приложения, но и потому, что сторонняя БД использует ужасные имена для таблиц и столбцов.)

Я заметил, что одно из моих представлений (которое объединяет другие представления, которые в свою очередь объединяют другие представления ...) демонстрирует необычную медлительность в системе клиента. Я разбил его на более мелкие части, но не смог понять ни одного подозреваемого.

Ниже приведена уменьшенная версия представления, при этом другие виды, на которые оно обычно ссылается, превращаются в CTE, что все еще точно так же медленно, как и исходное представление. Если я разобью его на более мелкие части, они будут выполняться очень быстро. Я также добавил несколько комментариев, показывающих примеры небольших изменений, которые делают запрос намного быстрее.

-- The query takes about 5s when the server has no other load
-- That's too slow because the UI of the app needs the results
with
orderLines as (
    select r.DocEntry as rdrDocId,
           r1.LineNum as rdrLineId
    from rdr1 r1
    join ordr r on r.DocEntry = r1.DocEntry
    -- If I filter only by LineStatus or only by DocStatus here, query takes <1s
    where r1.LineStatus = 'O' and r.DocStatus = 'O'
),
picklistDetails as (
    select U_KommNr as pklDocId,
           max(cast(U_Space as int)) as maxPlace
    from [@PICKING]
    where U_DeletedF = 'N'
    group by U_KommNr
),
picklistDocs as (
    select p.AbsEntry      as pklDocId,
           case
           when pd.maxPlace is null then 0
           else pd.maxPlace
           end             as pklDocMaxPlace
    from opkl p
    left join picklistDetails pd on pd.pklDocId = p.AbsEntry
),
picklistDocLines as (
    select AbsEntry   as pklDocId,
           PickEntry  as pklLineId,
           OrderEntry as rdrDocId,
           OrderLine  as rdrLineId
    from PKL1
)
select p.pklDocMaxPlace
from picklistDocs p
join picklistDocLines p1 on p.pklDocId = p1.pklDocId
join orderLines r1 on r1.rdrDocId = p1.rdrDocId
                  and r1.rdrLineId = p1.rdrLineId
-- If I force parallelism by using the following option, query takes <1s
--option(querytraceon 8649)

В дополнение к тому факту, что все части запроса выполняются довольно быстро в изоляции, я также получаю гораздо более быстрое время выполнения (опять же <1 с в общем), когда я использую <code>#temp таблицы вместо CTE, как показано ниже:

-- This batch execution returns the same result but takes <1s

select r.DocEntry as rdrDocId,
        r1.LineNum as rdrLineId
into #orderLines
from rdr1 r1
join ordr r on r.DocEntry = r1.DocEntry
where r1.LineStatus = 'O' and r.DocStatus = 'O'

select U_KommNr as pklDocId,
       max(cast(U_Space as int)) as maxPlace
into #picklistDetails
from [@PICKING]
where U_DeletedF = 'N'
group by U_KommNr

select p.AbsEntry      as pklDocId,
       case
       when pd.maxPlace is null then 0
       else pd.maxPlace
       end             as pklDocMaxPlace
into #picklistDocs
from opkl p
left join #picklistDetails pd on pd.pklDocId = p.AbsEntry

select AbsEntry   as pklDocId,
       PickEntry  as pklLineId,
       OrderEntry as rdrDocId,
       OrderLine  as rdrLineId
into #picklistDocLines
from PKL1

select p.pklDocMaxPlace
from #picklistDocs p
join #picklistDocLines p1 on p.pklDocId = p1.pklDocId
join #orderLines r1 on r1.rdrDocId = p1.rdrDocId
                   and r1.rdrLineId = p1.rdrLineId

Может кто-нибудь разобраться в поведении SQL Server здесь? На мой взгляд, это похоже на ошибку / сбой в оптимизаторе запросов.

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

1 Ответ

4 голосов
/ 06 марта 2019

Может кто-нибудь разобраться в поведении SQL Server здесь? Для меня это похоже на ошибку / сбой в оптимизаторе запросов.

Нет, это не ошибка.

Задача разделена на несколько более мелких единиц:

Подход с использованием временных таблиц представляет собой не что иное, как разделение этого большого плана запросов на более мелкие части и выполнение их независимо.

Чем меньше фрагмент, тем больше вероятность того, что SQL Server Query Optimizer не выполнит какое-либо существенное несоответствие в оценке количества элементов и выберет правильные физические операторы и типы объединений, поэтому меньше вероятность увидеть вложенный цикл на миллионах строк или какая-то другая неприятная вещь.

Когда время запускает фрагмент кода, указанный ниже, оптимизатор запросов знает, сколько строк в каждой задействованной временной таблице и как они распределены:

select p.pklDocMaxPlace
from #picklistDocs p
join #picklistDocLines p1 on p.pklDocId = p1.pklDocId
join #orderLines r1 on r1.rdrDocId = p1.rdrDocId
                   and r1.rdrLineId = p1.rdrLineId

Одна единица работы:

Подход CTE, как было упомянуто Лукашем и Робертом в комментариях, является своего рода синтаксическим сахаром, похожим на представление «взгляд на вид». Однако в конце оптимизатор запросов должен объединить все CTE в один консолидированный, а иногда и большой план запросов и выполнить его как одна единица . Поэтому, чем больше план, тем больше шансов на сюрпризы, связанные с производительностью.

Таким образом, в отличие от предыдущего фрагмента, оптимизатор запросов компилирует план в тот момент, когда количество строк просто угадывается оценкой мощности с использованием статистики:

select p.pklDocMaxPlace
from picklistDocs p
join picklistDocLines p1 on p.pklDocId = p1.pklDocId
join orderLines r1 on r1.rdrDocId = p1.rdrDocId
                  and r1.rdrLineId = p1.rdrLineId

querytraceon 8649:

Когда вы включаете option(querytraceon 8649), вы просто заставляете оптимизатор запросов изменять поведение, так же, как другие подсказки запроса или подобные трассировки, подобные 4199. Так что вынужденный параллелизм, возможно, иногда генерировал лучший план, но вы вряд ли можете на это рассчитывать.

Некоторые идеи, как это можно решить:

  • Обновление статистики по задействованным таблицам
  • Игра с переключением новых и устаревших оценок кардинальности
  • (imho) Переписать CTE в производную таблицу?
  • Если задействованы большие наборы данных, то разбиение логики на более мелкие куски с использованием подхода #temp table - это то, что может быть последовательным обходным решением для выбора.
  • и т. Д.

Существует одно исключение:

  • Индексированные представления. С помощью подсказки NOEXPAND (или если используется Enterprise Edition). Логика представления не должна быть сведена к общему плану запроса, который его включает.
...