Проблема с производительностью при использовании функции IsNull в инструкции Select

#sql #sql-server #performance #tsql #isnull

#sql #sql-сервер #Производительность #tsql #isnull

Вопрос:

У меня есть финансовое приложение. У меня есть ViewHistoricInstrumentValue , в котором есть такие строки

 instrument1, date1, price, grossValue, netValue
instrument2, date1, price, grossValue, netValue
...
instrument1, date2, price, grossValue, netValue
...
 

Мои представления сложны, но сама база данных невелика (4000 транзакций). ViewHistoricInstrumentValue был выполнен менее чем за 1 секунду до того, как я добавил следующий CTE в представление. После этого требуется 26 секунд. ActualEvaluationPrice это цена для instrumentX на сегодняшний день. Если это значение отсутствует в HistoricPrice таблице, я нахожу предыдущую цену для instrumentX.

 , UsedEvaluationPriceCte AS (
SELECT *
    , isnull(ActualEvaluationPrice, 
        (select top 1 HistoricPrice.Price -- PreviousPrice
           from HistoricPrice JOIN ValidDate 
            on HistoricPrice.DateId = ValidDate.Id 
                and HistoricPrice.InstrumentId = StartingCte.InstrumentId
                and ValidDate.[Date] < StartingCte.DateValue
            order by ValidDate.[Date])) 
       as UsedEvaluationPrice
FROM StartingCte
)
 

Моя проблема в том, что время выполнения увеличилось без необходимости. Прямо сейчас таблица HistoricPrice не имеет пропущенного значения, поэтому ActualEvaluationPrice никогда не имеет значения null, поэтому предыдущая цена никогда не должна определяться.

ViewHistoricInstrumentValue возвращает 1815 строк. Еще одна загадка заключается в том, что первый запрос занимает 26 секунд, а второй — только 2 секунды.

 SELECT * FROM [ViewHistoricInstrumentValue]
SELECT top(2000) * FROM [ViewHistoricInstrumentValue]
 

Приложение

План выполнения: https://www.dropbox.com/s/5st69uhjkpd3b5y/IsNull.sqlplan?dl=0

Тот же план: https://www.brentozar.com/pastetheplan/?id=rk9bK1Wiv

Представление:

 ALTER VIEW [dbo].[ViewHistoricInstrumentValue] AS 
WITH StartingCte AS (
    SELECT
        HistoricInstrumentValue.DateId
        , ValidDate.Date as DateValue
        , TransactionId
        , TransactionId AS [Row]
        , AccountId
        , AccountName
        , ViewTransaction.InstrumentId
        , ViewTransaction.InstrumentName
        , OpeningDate
        , OpeningPrice
        , Price AS ActualEvaluationPrice
        , ClosingDate
        , Amount        
        , isnull(ViewTransaction.FeeValue, 0) as FeeValue
        , HistoricInstrumentValue.Id AS Id
    FROM ViewBriefHistoricInstrumentValue as HistoricInstrumentValue 
    JOIN ValidDate on HistoricInstrumentValue.DateId = ValidDate.Id
    JOIN ViewTransaction ON ViewTransaction.Id = HistoricInstrumentValue.TransactionId
    left JOIN ViewHistoricPrice ON ViewHistoricPrice.DateId = HistoricInstrumentValue.DateId AND
        ViewHistoricPrice.InstrumentId = ViewTransaction.InstrumentId
)
, UsedEvaluationPriceCte AS (
    SELECT *
        , isnull(ActualEvaluationPrice, 
            (select top 1 HistoricPrice.Price -- PreviousPrice
               from HistoricPrice JOIN ValidDate 
                on HistoricPrice.DateId = ValidDate.Id 
                    and HistoricPrice.InstrumentId = StartingCte.InstrumentId
                    and ValidDate.[Date] < StartingCte.DateValue
                order by ValidDate.[Date])) 
           as UsedEvaluationPrice
    FROM StartingCte
)
, GrossEvaluationValueCte AS (
    SELECT *
        , Amount * UsedEvaluationPrice AS GrossEvaluationValue
        , (UsedEvaluationPrice - OpeningPrice) * Amount AS GrossCapitalGains
    FROM UsedEvaluationPriceCte
)
, CapitalGainsTaxCte AS (
    SELECT *
        , dbo.MyMax(GrossCapitalGains * 0.15, 0) AS CapitalGainsTax
    FROM GrossEvaluationValueCte    
)
, IsOpenCte AS (
    SELECT
        DateId
        , DateValue
        , TransactionId
        , [Row]
        , AccountId
        , AccountName
        , InstrumentId
        , InstrumentName
        , OpeningDate
        , OpeningPrice
        , ActualEvaluationPrice
        , UsedEvaluationPrice
        , ClosingDate
        , Amount
        , GrossEvaluationValue 
        , GrossCapitalGains
        , CapitalGainsTax 
        , FeeValue
        , GrossEvaluationValue - CapitalGainsTax - FeeValue AS NetEvaluationValue
        , GrossCapitalGains - CapitalGainsTax - FeeValue AS NetUnrealizedGains
        , CASE WHEN ClosingDate IS NULL OR DateValue < ClosingDate
        THEN CAST(1 AS BIT)
        ELSE CAST(0 AS BIT)
        END 
        AS IsOpen
        , convert(NVARCHAR, DateValue, 20)   cast([Id] AS NVARCHAR(MAX)) AS Temp
        , Id    
    FROM CapitalGainsTaxCte
)
Select * from IsOpenCte
 

Комментарии:

1. Пожалуйста, покажите свой план выполнения.

2. Дорогой @DaleK Я добавил план выполнения, но я не знаю, помогает ли это, потому что это слишком сложно (по крайней мере, для меня).

3. Дорогой @DaleK Я также поделился этим планом с брентозаром.

4. Обязательно ли использовать представление? Я бы предложил вместо использования CTE материализовать промежуточные результаты во временной таблице, а затем выбрать из нее. Разделив ваш запрос таким образом, вы получите два более простых плана запросов вместо одного сложного плана.

5. @DaleK Если ViewTransaction помещается во временную таблицу, то время составляет менее 1 секунды. В этом случае план выполнения ViewHistoricInstrumentValue более понятен: brentozar.com/pastetheplan/?id=S1F-gebsw Мне кажется, что если не используется временная таблица, то выполняется много ненужных вычислений.

Ответ №1:

Я понятия не имею, что должен делать ваш запрос. Но этот процесс:

ActualEvaluationPrice — это цена для instrumentX на сегодняшний день. Если это значение отсутствует в таблице исторических цен, я нахожу предыдущую цену для instrumentX.

легко обрабатывается с помощью lag() :

 select vhiv.*
       coalesce(vhiv.ActualEvaluationPrice,
                lag(vhiv.ActualEvaluationPrice) over (partition by vhiv.InstrumentId order by DateValue)
               ) as UsedEvaluationPrice
from ViewHistoricInstrumentValue vhiv;
 

Примечание: Если вам нужно отфильтровать определенные даты путем присоединения к ValidDates , вы можете включить JOIN их в запрос. Однако это не является частью инструкции о проблеме.

Комментарии:

1. Привет @Gordon Linoff ! Мне потребовалось некоторое время, но я понял ваше предложение. Это помогло мне сделать еще один шаг вперед. Оказалось, что проблема не в определении предыдущей цены. Если я пропущу эту часть и перечислю только результат первого cte ( StartingCte ), то он будет завершен сразу. Если я сделаю то же самое, но теперь, order by DateId то потребуется 26 секунд, если order by Id используется, то это снова 0 секунд. Это очень странно. Я знаю, что это другой вопрос, но у вас есть идеи?