慢速UDF的SQL替代方案



具有两个UDT参数的查询需要0.3秒,但如果封装在内联表值函数中,则需要3.5秒以上。

我读过(为什么UDF比子查询慢得多?)但我很难解决如何修复/重写。

根据@JasonALong在下面的反馈,

SELECT语句在0.3秒内完成的执行计划:https://www.brentozar.com/pastetheplan/?id=HJnrqC53Z(请注意,SQL在此页面上可用)。

3.5秒内完成的功能代码粘贴在下面,执行计划在此链接:https://www.brentozar.com/pastetheplan/?id=BJZbqR93b

SELECT
SelectedContracts.MeasurableID,
SelectedContracts.EntityID,
EntityName,
EntityAbbrev,
EntityLogoURL,
EntityHex1,
EntityHex2,
EntitySportID,
MeasurableName,
MeasurableOrganizationID,
YearFilter,
SeasonFilter,
CategoryFilter,
ResultFilter,
Logo4Result,
MeasurableSportID,
MouseoverFooter,
ContractRank4Org,
ContractEndUTC,
HighContractPrice4Period,
HighTradeID,
HighTradeUTC,
HighTradeNumberOfContracts,
HighTradeCurrency,
LowContractPrice4Period,
LowTradeID,
LowTradeUTC,
LowTradeNumberOfContracts,
LowTradeCurrency,
LastTradePrice,
LastTradeID,
LastTradeUTC,
LastTradeNumberOfContracts,
LastTradeCurrency,
SecondLastTradePrice,
SecondLastTradeID,
SecondLastTradeUTC,
SecondLastTradeNumberOfContracts,
SecondLastTradeCurrency,
ContractPrice4ChangeCalc,
ContractID4ChangeCalc,
ContractUTC4ChangeCalc,
ContractsNumberTraded4ChangeCalc,
ContractCurrency4ChangeCalc,
HighestBidID,
HighestBidMemberID,
HighestBidPrice,
HighestBidAvailableContracts,
HighestBidCurrency,
LowestAskID,
LowestAskMemberID,
LowestAskPrice,
LowestAskAvailableContracts,
LowestAskCurrency

FROM
(
SELECT
dbo.Contracts.MeasurableID,
dbo.Contracts.EntityID
FROM
dbo.Contracts
WHERE
dbo.Contracts.MeasurableID IN ((2030),(2017),(2018),(2019),(2020),( 2028),(2024),(2027),(2029),(2022),( 4018),(4019),(4020),(4021))
GROUP BY
dbo.Contracts.MeasurableID,
dbo.Contracts.EntityID
) SelectedContracts

INNER JOIN 
(
SELECT
dbo.Entities.ID,
--dbo.Entities.OrganizationID, -- Get OrganizationID from Measurable since some Entities (European soccer teams) have multiple Orgs
dbo.Entities.EntityName,
dbo.Entities.EntityAbbrev,
dbo.Entities.logoURL AS EntityLogoURL,
dbo.Entities.Hex1 AS EntityHex1,
dbo.Entities.Hex2 AS EntityHex2,
dbo.Entities.SportID AS EntitySportID
FROM
dbo.Entities
) SelectedEntities ON SelectedContracts.EntityID = SelectedEntities.ID

INNER JOIN 
(
SELECT
dbo.Measurables.ID AS MeasurableID,
dbo.Measurables.Name AS MeasurableName,
dbo.Measurables.OrganizationID AS MeasurableOrganizationID,
dbo.Measurables.[Year] AS YearFilter,
dbo.Measurables.Season AS SeasonFilter,
dbo.Measurables.Category AS CategoryFilter,
dbo.Measurables.Result AS ResultFilter,
dbo.Measurables.Logo4Result,
dbo.Measurables.SportID AS MeasurableSportID,
dbo.Measurables.MouseoverFooter,
dbo.Measurables.ContractRank4Org,
dbo.Measurables.EndUTC AS ContractEndUTC
FROM
dbo.Measurables
) MEASURABLES_table ON SelectedContracts.MeasurableID = MEASURABLES_table.MeasurableID

LEFT JOIN 
(
SELECT
MeasurableID,
EntityID,
ContractPrice AS HighContractPrice4Period,
ID AS HighTradeID,
UTCMatched AS HighTradeUTC,
NumberOfContracts AS HighTradeNumberOfContracts,
CurrencyCode AS HighTradeCurrency
FROM
(
SELECT
*, ROW_NUMBER () OVER (
PARTITION BY MeasurableID,
EntityID
ORDER BY
ContractPrice DESC,
ID DESC
) RowNumber -- ID DESC means most recent trade of ties
FROM
Contracts
WHERE
MeasurableID IN ((2030),(2017),(2018),(2019),(2020),( 2028),(2024),(2027),(2029),(2022),( 4018),(4019),(4020),(4021))
AND dbo.Contracts.UTCmatched < DATEADD(DAY, -30, SYSDATETIME())
AND (           CurrencyCode IN (('GBP'), ('CAD'), ('INR'), ('BRL'), ('MXN'), ('CHF'), ('RUB')) 
)   
) AS InnerSelect4HighTrade
WHERE   
InnerSelect4HighTrade.RowNumber = 1
) HighTrades ON SelectedContracts.MeasurableID = HighTrades.MeasurableID AND SelectedContracts.EntityID = HighTrades.EntityID

LEFT JOIN 
(
SELECT
MeasurableID,
EntityID,
ContractPrice AS LowContractPrice4Period,
ID AS LowTradeID,
UTCMatched AS LowTradeUTC,
NumberOfContracts AS LowTradeNumberOfContracts,
CurrencyCode AS LowTradeCurrency
FROM
(
SELECT
*, ROW_NUMBER () OVER (
PARTITION BY MeasurableID,
EntityID
ORDER BY
ContractPrice ASC,
ID DESC
) RowNumber -- ID DESC means most recent trade of ties
FROM
Contracts
WHERE
MeasurableID IN ((2030),(2017),(2018),(2019),(2020),( 2028),(2024),(2027),(2029),(2022),( 4018),(4019),(4020),(4021))
AND dbo.Contracts.UTCmatched < DATEADD(DAY, -30, SYSDATETIME())
AND (           CurrencyCode IN (('GBP'), ('CAD'), ('INR'), ('BRL'), ('MXN'), ('CHF'), ('RUB')) 
)           
) AS InnerSelect4LowTrade
WHERE       InnerSelect4LowTrade.RowNumber = 1
) LowTrades ON SelectedContracts.MeasurableID = LowTrades.MeasurableID AND SelectedContracts.EntityID = LowTrades.EntityID

LEFT JOIN 
(
SELECT
MeasurableID,
EntityID,
ContractPrice AS LastTradePrice,
ID AS LastTradeID,
UTCMatched AS LastTradeUTC,
NumberOfContracts AS LastTradeNumberOfContracts,
CurrencyCode AS LastTradeCurrency
FROM
(
SELECT
*, ROW_NUMBER () OVER (
PARTITION BY MeasurableID,
EntityID
ORDER BY
ID DESC
) RowNumber -- ID DESC means most recent trade of ties
FROM
Contracts
WHERE
MeasurableID IN ((2030),(2017),(2018),(2019),(2020),( 2028),(2024),(2027),(2029),(2022),( 4018),(4019),(4020),(4021))
AND (           CurrencyCode IN (('GBP'), ('CAD'), ('INR'), ('BRL'), ('MXN'), ('CHF'), ('RUB')) 
)   
) AS InnerSelect4LastTrade
WHERE   InnerSelect4LastTrade.RowNumber = 1
) LastTrades ON SelectedContracts.MeasurableID = LastTrades.MeasurableID AND SelectedContracts.EntityID = LastTrades.EntityID

LEFT JOIN 
(
SELECT
MeasurableID,
EntityID,
ContractPrice AS SecondLastTradePrice,
ID AS SecondLastTradeID,
UTCMatched AS SecondLastTradeUTC,
NumberOfContracts AS SecondLastTradeNumberOfContracts,
CurrencyCode AS SecondLastTradeCurrency
FROM
(
SELECT
*, ROW_NUMBER () OVER (
PARTITION BY MeasurableID,
EntityID
ORDER BY
ID DESC
) RowNumber -- ID DESC means most recent trade of ties
FROM
Contracts
WHERE
MeasurableID IN ((2030),(2017),(2018),(2019),(2020),( 2028),(2024),(2027),(2029),(2022),( 4018),(4019),(4020),(4021))
AND (           CurrencyCode IN (('GBP'), ('CAD'), ('INR'), ('BRL'), ('MXN'), ('CHF'), ('RUB')) 
)   
--need time filter???
) AS InnerSelect4SecondToLastTrade
WHERE InnerSelect4SecondToLastTrade.RowNumber = 2
) SecondToLastTrade ON SelectedContracts.MeasurableID = SecondToLastTrade.MeasurableID AND SelectedContracts.EntityID = SecondToLastTrade.EntityID

LEFT JOIN 
(
SELECT
MeasurableID,
EntityID,
ContractPrice AS ContractPrice4ChangeCalc,
ID AS ContractID4ChangeCalc,
UTCMatched AS ContractUTC4ChangeCalc,
NumberOfContracts AS ContractsNumberTraded4ChangeCalc,
CurrencyCode AS ContractCurrency4ChangeCalc
FROM
(
SELECT
*, ROW_NUMBER () OVER (
PARTITION BY MeasurableID,
EntityID
ORDER BY
ID DESC  -- ID DESC equals the most recent trade if ties
) RowNumber 
FROM
Contracts
WHERE
MeasurableID IN ((2030),(2017),(2018),(2019),(2020),( 2028),(2024),(2027),(2029),(2022),( 4018),(4019),(4020),(4021))
AND (           CurrencyCode IN (('GBP'), ('CAD'), ('INR'), ('BRL'), ('MXN'), ('CHF'), ('RUB')) 
)   
AND dbo.Contracts.UTCmatched < DATEADD(Day ,-30, SYSDATETIME())
) AS InnerSelect4ChangeCalcPerPeriod
WHERE   InnerSelect4ChangeCalcPerPeriod.RowNumber = 1
) Trade4ChangeCalcPerPeriod ON SelectedContracts.MeasurableID = Trade4ChangeCalcPerPeriod.MeasurableID AND SelectedContracts.EntityID = Trade4ChangeCalcPerPeriod.EntityID

LEFT JOIN 
(
SELECT
MeasurableID,
EntityID,
ID AS HighestBidID,
MemberID AS HighestBidMemberID,
BidPrice AS HighestBidPrice,
AvailableContracts AS HighestBidAvailableContracts,
CurrencyCode AS HighestBidCurrency
FROM
(
SELECT
*, ROW_NUMBER () OVER (
PARTITION BY MeasurableID,
EntityID
ORDER BY
BidPrice DESC,
ID DESC
) RowNumber
FROM
dbo.Interest2Buy
WHERE
MeasurableID IN ((2030),(2017),(2018),(2019),(2020),( 2028),(2024),(2027),(2029),(2022),( 4018),(4019),(4020),(4021))
AND AvailableContracts > 0
AND (           CurrencyCode IN (('GBP'), ('CAD'), ('INR'), ('BRL'), ('MXN'), ('CHF'), ('RUB')) 
)   
) AS InnerSelect4HighestBid
WHERE   InnerSelect4HighestBid.RowNumber = 1
) HighestBids ON SelectedContracts.MeasurableID = HighestBids.MeasurableID AND SelectedContracts.EntityID = HighestBids.EntityID

LEFT JOIN 
(
SELECT
MeasurableID,
EntityID,
ID AS LowestAskID,
MemberID AS LowestAskMemberID,
AskPrice AS LowestAskPrice,
AvailableContracts AS LowestAskAvailableContracts,
CurrencyCode AS LowestAskCurrency
FROM
(
SELECT
*, ROW_NUMBER () OVER (
PARTITION BY MeasurableID,
EntityID
ORDER BY
AskPrice ASC,
ID DESC
) RowNumber
FROM
dbo.Interest2Sell
WHERE
MeasurableID IN ((2030),(2017),(2018),(2019),(2020),( 2028),(2024),(2027),(2029),(2022),( 4018),(4019),(4020),(4021))
AND AvailableContracts > 0
AND (           CurrencyCode IN (('GBP'), ('CAD'), ('INR'), ('BRL'), ('MXN'), ('CHF'), ('RUB')) 
)   
) AS InnerSelect4BestAsk
WHERE   InnerSelect4BestAsk.RowNumber = 1
) BestAsks ON SelectedContracts.MeasurableID = BestAsks.MeasurableID AND SelectedContracts.EntityID = BestAsks.EntityID

正如您的问题中所提到的,标量函数和muli语句表值函数(mTVF)对优化器来说都是"黑匣子"。。。

所以,我认为这个问题是,"为什么这么糟糕?"。答案是,为了制定一个尽可能高效执行的好计划,它需要了解特定需求的某些细节,以及从中提取数据的表的信息(这就是为什么过时的静态数据也会严重影响性能)。所以…当您使用标量函数或mTVF时,优化器无法像使用内联代码那样评估所有需求。它的反应是简单地假设函数只执行一次,并根据该假设制定计划。

由于假设是错误的,就会产生错误的计划,最终导致糟糕的表现。

解决方案是重写有问题的函数。。。关键是要达到#1,确保将它们重写为"内联表值函数"(iTVF)。这些是优化器将看到的唯一函数,就好像它们的代码是直接键入到外部查询中一样(因此称为"内联")。如果你不熟悉iTVF,它们有2个要求。。。1它们必须是表函数(无论出于何种原因,MS仍然没有可用的标量版本)。。。和2这是最重要的。。。函数体必须是单个语句。

那么,如果你不需要一个表值函数,你需要一个标量函数呢?没有什么能说明多值函数不能返回单个(标量)值。。。这就是为什么那些知道这种情况的人把他们的所有功能都编码为iTVF。

好的方面是,网络上不乏关于创建"内联标量函数"的信息,即使用编码为在网络上返回标量值的表函数。

希望这能帮助。。。

使用联接而不是"IN"子句帮助很大。(尽管我也将表var更改为临时表,这也有很大帮助。)

最新更新