在 Select 语句中使用 IsNull 函数的性能问题

Performance issue using IsNull function in the Select statement

我有一份财务申请。我有 ViewHistoricInstrumentValue 有这样的行

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

我的观点很复杂,但数据库本身很小(4000 个事务)。在我将下一个 CTE 添加到视图之前,ViewHistoricInstrumentValue 在不到 1 秒的时间内执行。之后需要 26 秒。 ActualEvaluationPrice 是仪器 X 在日期 Y 的价格。如果 HistoricPrice table 中缺少此值,那么我会找到 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 table 没有缺失值,因此 ActualEvaluationPrice 永远不会为空,因此永远不会确定之前的价格。

ViewHistoricInstrumentValue returns 1815 行。另一个谜团是第一个查询需要26s,而第二个只需要2s。

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

我不知道你的查询应该做什么。但是这个过程:

ActualEvaluationPrice is the price for instrumentX at dateY. If this value is missing from HistoricPrice table then I find the previous price for 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。但是,这不是问题陈述的一部分。