CTE очень медленно при входе

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

Я работаю с CTE, который создает сумму сборов на основе родительской платы. SQL и детали можно увидеть здесь:

Рекомендации CTE в таблице с несколькими ключами

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

В таблице tblChargeShare содержится другая информация, которая мне нужна, например InvoiceID, поэтому я поместил CTE в представление vwChargeShareSubCharges и присоединил его к таблице.

Запрос:

Select t.* from vwChargeShareSubCharges t
inner join 
tblChargeShare  s 
on t.CustomerID = s.CustomerID 
and t.MasterChargeID = s.ChargeID 
Where  s.ChargeID = 1291094

Возвращает результат в несколько мс.

Запрос:

Select ChargeID from tblChargeShare Where InvoiceID = 1045854

Возвращает 1 строку:

1291094

Но запрос:

Select t.* from vwChargeShareSubCharges t
inner join 
tblChargeShare  s 
on t.CustomerID = s.CustomerID 
and t.MasterChargeID = s.ChargeID 
Where  InvoiceID = 1045854

Выполняется 2-3 минуты.

Я сохранил планы выполнения и загрузил их в SQL Sentry. Дерево для быстрого запроса выглядит следующим образом:

Fast Query

План медленного запроса:

Slow Query

Я попытался переиндексировать, выполнив запрос через советник по настройке и различные комбинации подзапросов. Всякий раз, когда соединение содержит что-либо, кроме ПК, запрос выполняется медленно.

У меня был аналогичный вопрос:

Тайм-аут запроса SQL Server в зависимости от предложения Where

Какие используемые функции выполняют summimg дочерних строк вместо CTE. Это переписать с помощью CTE, чтобы попытаться избежать той же проблемы, с которой я сейчас сталкиваюсь. Я прочитал ответы в этом ответе, но я не мудрее - я прочитал некоторую информацию о подсказках и параметрах, но я не могу заставить его работать. Я думал, что переписывание с использованием CTE решит мою проблему. Запрос выполняется быстро при запуске на tblCharge с несколькими тысячами строк.

Протестировано как в SQL 2008 R2, так и в SQL 2012

Edit:

Я сконфигурировал запрос в один оператор, но та же проблема сохраняется:

WITH RCTE AS
(
SELECT  ParentChargeId, s.ChargeID, 1 AS Lvl, ISNULL(TotalAmount, 0) as TotalAmount,  ISNULL(s.TaxAmount, 0) as TaxAmount,  
ISNULL(s.DiscountAmount, 0) as DiscountAmount, s.CustomerID, c.ChargeID as MasterChargeID
from tblCharge c inner join tblChargeShare s
on c.ChargeID = s.ChargeID Where s.ChargeShareStatusID < 3 and ParentChargeID is NULL

UNION ALL

SELECT c.ParentChargeID, c.ChargeID, Lvl+1 AS Lvl, ISNULL(s.TotalAmount, 0),  ISNULL(s.TaxAmount, 0),  ISNULL(s.DiscountAmount, 0) , s.CustomerID 
, rc.MasterChargeID 
from tblCharge c inner join tblChargeShare s
on c.ChargeID = s.ChargeID
INNER JOIN RCTE rc ON c.PArentChargeID = rc.ChargeID and s.CustomerID = rc.CustomerID  Where s.ChargeShareStatusID < 3 
)

Select MasterChargeID as ChargeID, rcte.CustomerID, Sum(rcte.TotalAmount) as TotalCharged, Sum(rcte.TaxAmount) as TotalTax, Sum(rcte.DiscountAmount) as TotalDiscount
from RCTE inner join tblChargeShare s on rcte.ChargeID = s.ChargeID and RCTE.CustomerID = s.CustomerID 
Where InvoiceID = 1045854
Group by MasterChargeID, rcte.CustomerID 
GO

Изменить: Больше играть, я просто не понимаю этого.

Этот запрос мгновен (2 мс):

Select t.* from
vwChargeShareSubCharges t
Where  t.MasterChargeID = 1291094

Если это занимает 3 минуты:

DECLARE @ChargeID int = 1291094

Select t.* from
vwChargeShareSubCharges t
Where  t.MasterChargeID = @ChargeID

Даже если я помещаю кучи чисел в "В", запрос все еще мгновен:

Where  t.MasterChargeID in (1291090, 1291091, 1291092, 1291093,  1291094, 1291095, 1291096, 1291097, 1291098, 1291099, 129109)

Изменить 2:

Я могу воспроизвести это с нуля, используя данные этого примера:

Я создал некоторые фиктивные данные для репликации проблемы. Это не так важно, так как я добавил только 100 000 строк, но плохой план выполнения все еще выполняется (выполняется в режиме SQLCMD):

CREATE TABLE [tblChargeTest](
[ChargeID] [int] IDENTITY(1,1) NOT NULL,
[ParentChargeID] [int] NULL,
[TotalAmount] [money] NULL,
[TaxAmount] [money] NULL,
[DiscountAmount] [money] NULL,
[InvoiceID] [int] NULL,
CONSTRAINT [PK_tblChargeTest] PRIMARY KEY CLUSTERED 
(
[ChargeID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,     ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
END
GO

Insert into tblChargeTest
(discountAmount, TotalAmount, TaxAmount)
Select ABS(CHECKSUM(NewId())) % 10, ABS(CHECKSUM(NewId())) % 100, ABS(CHECKSUM(NewId())) % 10
GO 100000

Update tblChargeTest
Set ParentChargeID = (ABS(CHECKSUM(NewId())) % 60000) + 20000
Where ChargeID = (ABS(CHECKSUM(NewId())) % 20000)
GO 5000

CREATE VIEW [vwChargeShareSubCharges] AS
WITH RCTE AS
(
SELECT  ParentChargeId, ChargeID, 1 AS Lvl, ISNULL(TotalAmount, 0) as TotalAmount,   ISNULL(TaxAmount, 0) as TaxAmount,  
ISNULL(DiscountAmount, 0) as DiscountAmount,  ChargeID as MasterChargeID
FROM tblChargeTest Where ParentChargeID is NULL

UNION ALL

SELECT rh.ParentChargeID, rh.ChargeID, Lvl+1 AS Lvl, ISNULL(rh.TotalAmount, 0),    ISNULL(rh.TaxAmount, 0),  ISNULL(rh.DiscountAmount, 0) 
, rc.MasterChargeID 
FROM tblChargeTest rh
INNER JOIN RCTE rc ON rh.PArentChargeID = rc.ChargeID --and rh.CustomerID =  rc.CustomerID 
)

Select MasterChargeID,  ParentChargeID, ChargeID, TotalAmount, TaxAmount, DiscountAmount , Lvl
FROM  RCTE r 
GO

Затем запустите эти два запроса:

--Slow Query:
Declare @ChargeID int = 60900

Select *
from [vwChargeShareSubCharges]
Where MasterChargeID = @ChargeID

--Fast Query:
Select *
from [vwChargeShareSubCharges]
Where MasterChargeID = 60900

Ответ 1

Лучший SQL Server для вас может сделать это, чтобы нажать фильтр на ChargeID вниз в анкерную часть рекурсивного CTE внутри представления. Это позволяет искать единственную строку, из которой вы должны построить иерархию. Когда вы предоставляете параметр как постоянное значение, SQL Server может сделать эту оптимизацию (используя правило, называемое SelOnIterator, для тех, кто заинтересован в таких вещах):

Pushed predicate with a constant value

Когда вы используете локальную переменную, она не может этого сделать, поэтому предикат на ChargeID застревает вне представления (который строит полную иерархию, начиная со всех NULL ids):

Stuck Predicate

Один из способов получить оптимальный план при использовании переменной - заставить оптимизатор компилировать новый план при каждом выполнении. Полученный план затем адаптируется к конкретному значению переменной во время выполнения. Это достигается добавлением подсказки OPTION (RECOMPILE):

Declare @ChargeID int = 60900;

-- Produces a fast execution plan, at the cost of a compile on every execution
Select *
from [vwChargeShareSubCharges]
Where MasterChargeID = @ChargeID
OPTION (RECOMPILE);

Второй вариант - изменить представление на встроенную функцию таблицы. Это позволяет явно указать положение предиката фильтрации:

CREATE FUNCTION [dbo].[udfChargeShareSubCharges]
(
    @ChargeID int
)
RETURNS TABLE AS RETURN
(
  WITH RCTE AS
  (
  SELECT  ParentChargeID, ChargeID, 1 AS Lvl, ISNULL(TotalAmount, 0) as TotalAmount,   ISNULL(TaxAmount, 0) as TaxAmount,  
  ISNULL(DiscountAmount, 0) as DiscountAmount,  ChargeID as MasterChargeID
  FROM tblChargeTest 
  Where ParentChargeID is NULL 
  AND ChargeID = @ChargeID -- Filter placed here explicitly

  UNION ALL

  SELECT rh.ParentChargeID, rh.ChargeID, Lvl+1 AS Lvl, ISNULL(rh.TotalAmount, 0),    ISNULL(rh.TaxAmount, 0),  ISNULL(rh.DiscountAmount, 0) 
  , rc.MasterChargeID 
  FROM tblChargeTest rh
  INNER JOIN RCTE rc ON rh.ParentChargeID = rc.ChargeID --and rh.CustomerID =  rc.CustomerID 
  )

  Select MasterChargeID,  ParentChargeID, ChargeID, TotalAmount, TaxAmount, DiscountAmount , Lvl
  FROM  RCTE r 
)

Используйте его следующим образом:

Declare @ChargeID int = 60900

select *
from dbo.udfChargeShareSubCharges(@ChargeID)

Запрос также может извлечь выгоду из индекса на ParentChargeID.

create index ix_ParentChargeID on tblChargeTest(ParentChargeID)

Вот еще один ответ о аналогичном правиле оптимизации в аналогичном сценарии. Оптимизация планов выполнения для параметризованных запросов T-SQL, содержащих функции окна

Ответ 2

Далее, чтобы найти решение, я бы рекомендовал ВЫБРАТЬ В CTE во временную таблицу и присоединиться оттуда. Из личного опыта, связанного с CTE, мой запрос возвращался в течение 5 минут, в то время как простая вставка данных, сгенерированных CTE, во временную таблицу снизила их до 4 секунд. Я фактически соединял два CTE вместе, но я думаю, что это применимо ко всем долго выполняющимся запросам, когда CTE присоединяется к таблице LONG (особенно к внешним соединениям).


    --temp tables if needed to work with intermediate values
    If object_id('tempdb..#p') is not null
    drop table #p

    ;WITH cte as ( 
    select * from t1
    )

    select * 
    into #p
    from cte

    --then use the temp table as you would normally use the CTE
    select * from #p