2

I have an Inline Table Valued Function and for some reason it runs incredibly slow, to the point where I just have to cancel it.

When I run the same exact SQL on an ad hoc basis, and create the parameters by declaring and setting them, it runs in about 3 minutes, as expected. How is it possible that simply running it as an ITVF vs ad hoc is creating a difference in performance?!

An added wrinkle that I recently discovered is if I run it as a ITVF, and pass it x value it runs in about 3 minutes, and when I pass it y value, it takes forever. When I run it ad hoc, I'm using the allegedly troublesome y value, and it runs in 3 minutes! I don't want this to detract too much from the main question which is, how can SQL run faster ad hoc vs ITVF, I just wanted to mention it in case it would offer some insight.

Here's the ITVF signature:

[aop].[itvfOpsProductivity](@Interval varchar(7),@ToMonth tinyint = 12,@AOPYear char(4),@PLANNING_EstimateType varchar(4),@ACTYear char(4))

Here's how I called the ITVF

select * from [aop].[itvfOpsProductivity]('YTD',10,'2017','AOP','2016') 

When I call the ITVF with '2016' instead of '2017' in the 3rd parameter, it runs fine.

Here's how I ran the SQL inline / ad hoc (it ran in fine):

declare @Interval varchar(7)
set @Interval='YTD'
declare @ToMonth tinyint
set @ToMonth=10
declare @AOPYear char(4)
set @AOPYear='2017'
declare @PLANNING_EstimateType varchar(4)
set @PLANNING_EstimateType='AOP'
declare @ACTYear char(4)
set @ACTYear='2016'


SELECT
    Financial.Segment,
    Financial.EstimateRegionName as Region, 
    Financial.EstimateClusterName as Cluster, 
    Financial.EstimateCountryName as Country, 
    Financial.SAPCountryCode, 
    Operational.BU, 
    Operational.[Equipment Class], 
    Operational.ProductGroupCode, 
    Operational.ProductGroupCodeDesc, 
    Operational.ServiceCode, 
    Operational.ServiceCodeDesc, 
    Operational.[TEU or Install_ACT], 
    Operational.[TEU or Install_AOP], 
    Operational.[Hours_ACT], 
    Operational.[Hours_AOP], 
    Operational.[OriginalHours_AOP], 
    Operational.[Part Cost_ACT], 
    Operational.[Part Cost_AOP],
    Operational.[OriginalPart Cost_AOP],
    IsNull(Operational.TotalHoursPerCountry_ACT,0) as TotalHoursPerCountry_ACT,
    IsNull(Operational.TotalHoursPerCountry_AOP,0) as TotalHoursPerCountry_AOP,
    Case When Operational.TotalHoursPerCountry_ACT<>0
        Then ( IsNull(Financial.TotalCostPerCountry_ACT,0) - Operational.FEPartCostPerCountry_ACT ) / Operational.TotalHoursPerCountry_ACT
    End as FullyBurdenedBHR_ACT,
    Case When Operational.TotalHoursPerCountry_AOP<>0
        Then ( IsNull(Financial.TotalCostPerCountry_AOP,0) - Operational.FEPartCostPerCountry_AOP ) / Operational.TotalHoursPerCountry_AOP
    End as FullyBurdenedBHR_AOP,
    Operational.FEPartCostPerCountry_ACT,
    Operational.FEPartCostPerCountry_AOP,
    Financial.TotalCostPerCountry_ACT,
    Financial.TotalCostPerCountry_AOP
FROM 
(
    SELECT IsNull(TotalCost_ACT.Segment,TotalCost_AOP.Segment) as Segment, IsNull(TotalCost_ACT.EstimateRegionName,TotalCost_AOP.EstimateRegionName) as EstimateRegionName, IsNull(TotalCost_ACT.EstimateClusterName,TotalCost_AOP.EstimateClusterName) as EstimateClusterName, IsNull(TotalCost_ACT.EstimateCountryName,TotalCost_AOP.EstimateCountryName) as EstimateCountryName, IsNull(TotalCost_ACT.SAPCountryCode,TotalCost_AOP.SAPCountryCode) as SAPCountryCode, 
        TotalCostPerCountry_ACT, 
        TotalCostPerCountry_AOP
    FROM (
        SELECT Segment, EstimateRegionName, EstimateClusterName, EstimateCountryName, SAPCountryCode, 
            sum(TotalCost) as TotalCostPerCountry_ACT
        FROM [aop].[itvfProductivityCost](@Interval,@ToMonth,@ACTYear,'ACT')
        GROUP BY Segment, EstimateRegionName, EstimateClusterName, EstimateCountryName, SAPCountryCode
    ) as TotalCost_ACT 
    FULL OUTER JOIN
    (
        SELECT Segment, EstimateRegionName, EstimateClusterName, EstimateCountryName, SAPCountryCode, 
            sum(TotalCost) as TotalCostPerCountry_AOP
        FROM [aop].[itvfProductivityCost](@Interval,@ToMonth,@AOPYear,@PLANNING_EstimateType)
        GROUP BY Segment, EstimateRegionName, EstimateClusterName, EstimateCountryName, SAPCountryCode
    ) as TotalCost_AOP 
    ON TotalCost_ACT.Segment=TotalCost_AOP.Segment and TotalCost_ACT.EstimateRegionName=TotalCost_AOP.EstimateRegionName and TotalCost_ACT.SAPCountryCode=TotalCost_AOP.SAPCountryCode
) as Financial
LEFT JOIN 
(
    SELECT  
        IsNull(vwHoursPartsPerTEU_ACT.Segment,vwHoursPartsPerTEU_AOP.Segment) as Segment, 
        IsNull(vwHoursPartsPerTEU_ACT.Region,vwHoursPartsPerTEU_AOP.Region) as Region, 
        IsNull(vwHoursPartsPerTEU_ACT.Cluster,vwHoursPartsPerTEU_AOP.Cluster) as Cluster, 
        IsNull(vwHoursPartsPerTEU_ACT.Country,vwHoursPartsPerTEU_AOP.Country) as Country, 
        IsNull(vwHoursPartsPerTEU_ACT.SAPCountryCode,vwHoursPartsPerTEU_AOP.SAPCountryCode) as SAPCountryCode, 
        IsNull(vwHoursPartsPerTEU_ACT.BU,vwHoursPartsPerTEU_AOP.BU) as BU, 
        IsNull(vwHoursPartsPerTEU_ACT.[Equipment Class],vwHoursPartsPerTEU_AOP.[Equipment Class]) as [Equipment Class], 
        IsNull(vwHoursPartsPerTEU_ACT.ProductGroupCode,vwHoursPartsPerTEU_AOP.ProductGroupCode) as ProductGroupCode, 
        IsNull(vwHoursPartsPerTEU_ACT.ProductGroupCodeDesc,vwHoursPartsPerTEU_AOP.ProductGroupCodeDesc) as ProductGroupCodeDesc, 
        IsNull(vwHoursPartsPerTEU_ACT.ServiceCode,vwHoursPartsPerTEU_AOP.ServiceCode) as ServiceCode, 
        IsNull(vwHoursPartsPerTEU_ACT.ServiceCodeDesc,vwHoursPartsPerTEU_AOP.ServiceCodeDesc) as ServiceCodeDesc, 
        IsNull(vwHoursPartsPerTEU_ACT.[TEU or Install],0) AS [TEU or Install_ACT], 
        IsNull(vwHoursPartsPerTEU_AOP.[TEU or Install],0) AS [TEU or Install_AOP], 
        IsNull(vwHoursPartsPerTEU_ACT.[Hours],0) AS [Hours_ACT], 
        IsNull(vwHoursPartsPerTEU_AOP.[Hours],0) AS [Hours_AOP],
        IsNull(vwHoursPartsPerTEU_AOP.[OriginalHours],0) AS [OriginalHours_AOP],
        Sum(IsNull(vwHoursPartsPerTEU_ACT.[Hours],0)) Over(Partition By IsNull(vwHoursPartsPerTEU_ACT.Segment,vwHoursPartsPerTEU_AOP.Segment),IsNull(vwHoursPartsPerTEU_ACT.SAPCountryCode,vwHoursPartsPerTEU_AOP.SAPCountryCode)) as [TotalHoursPerCountry_ACT],
        Sum(IsNull(vwHoursPartsPerTEU_AOP.[Hours],0)) Over(Partition By IsNull(vwHoursPartsPerTEU_ACT.Segment,vwHoursPartsPerTEU_AOP.Segment),IsNull(vwHoursPartsPerTEU_ACT.SAPCountryCode,vwHoursPartsPerTEU_AOP.SAPCountryCode)) as [TotalHoursPerCountry_AOP],
        IsNull(vwHoursPartsPerTEU_ACT.[Part Cost],0) AS [Part Cost_ACT],  
        IsNull(vwHoursPartsPerTEU_AOP.[Part Cost],0) AS [Part Cost_AOP],
        IsNull(vwHoursPartsPerTEU_AOP.[OriginalPart Cost],0) AS [OriginalPart Cost_AOP],
        Sum(IsNull(vwHoursPartsPerTEU_ACT.[Part Cost],0)) Over(Partition By IsNull(vwHoursPartsPerTEU_ACT.Segment,vwHoursPartsPerTEU_AOP.Segment),IsNull(vwHoursPartsPerTEU_ACT.SAPCountryCode,vwHoursPartsPerTEU_AOP.SAPCountryCode)) as [FEPartCostPerCountry_ACT],
        Sum(IsNull(vwHoursPartsPerTEU_AOP.[Part Cost],0)) Over(Partition By IsNull(vwHoursPartsPerTEU_ACT.Segment,vwHoursPartsPerTEU_AOP.Segment),IsNull(vwHoursPartsPerTEU_ACT.SAPCountryCode,vwHoursPartsPerTEU_AOP.SAPCountryCode)) as [FEPartCostPerCountry_AOP]
    FROM
    (
        -- Too much missing cost from PerTEU report, i.e. $200k part cost discrepancy between NonTEU and TEU report for Germany in 2015
        --SELECT Region, Cluster, Country, SAPCountryCode, BU, [Equipment Class], ProductGroupCode, ProductGroupCodeDesc, PrimaryServiceCode as ServiceCode, PrimaryServiceCodeDesc as ServiceCodeDesc, Sum([TEU or Install]) as [TEU or Install], Sum([Total Labor Hours]+[Total Travel Hours]) as [Hours], Sum([Total Part Cost]) as [Part Cost]
        --FROM [aop].[vwHoursPartsPerTEU]
        --WHERE [Product Date] between dbo.fnCurrYearStartDateForPrevMonth(getDate()) and (select max(FromMonth) from fin.vwFinancials where FromMonth>=dbo.fnCurrYearStartDateForPrevMonth(getDate()) and EstimateType='ACT' and (Account is null or Account<>'AA_BASICCOGS') and FinanceType In('NonTradeCost','TradeCost'))
        --GROUP BY Region, Cluster, Country, SAPCountryCode, BU, [Equipment Class], ProductGroupCode, ProductGroupCodeDesc, PrimaryServiceCode, PrimaryServiceCodeDesc

        select Segment, EstimateRegionName as Region, EstimateClusterName as Cluster, EstimateCountryName as Country, SAPCountryCode, BU, EquipmentClass as [Equipment Class], ProductGroupCode, ProductGroupCodeDesc, ServiceCode, ServiceCodeDesc, [TEU or Install], [Hours], [Parts] as [Part Cost]
        from [aop].[itvfHoursPartsPerPopulation](@Interval,@ToMonth,@ACTYear)

    )  AS vwHoursPartsPerTEU_ACT 

    FULL OUTER JOIN
    (
        -- Reset to Original Hours & Parts for assumptions resulting in a negative value
        select [Segment], Region, Cluster, Country, SAPCountryCode, BU, [Equipment Class], ProductGroupCode, ProductGroupCodeDesc, ServiceCode, ServiceCodeDesc
            ,[TEU or Install_CY] as [TEU or Install]
            ,[OriginalHours_CY] as [OriginalHours]
            ,case when [Hours_CY]/nullif([TEU or Install_CY],0)<0 
                then [OriginalHours_CY]
                else [Hours_CY]
                end as [Hours]
            ,[OriginalPart Cost_CY] as [OriginalPart Cost]
            ,case when [Part Cost_CY]/nullif([TEU or Install_CY],0)<0 
                then [OriginalPart Cost_CY]
                else [Part Cost_CY]
                end as [Part Cost]
        --from aop.[itvfOperationalProjections](@Interval,@AOPYear)
        from aop.[itvfOperationalProjections](@Interval,@ToMonth,@AOPYear)

    )  AS vwHoursPartsPerTEU_AOP 
    ON vwHoursPartsPerTEU_ACT.Segment = vwHoursPartsPerTEU_AOP.Segment AND vwHoursPartsPerTEU_ACT.SAPCountryCode = vwHoursPartsPerTEU_AOP.SAPCountryCode AND vwHoursPartsPerTEU_ACT.BU = vwHoursPartsPerTEU_AOP.BU AND vwHoursPartsPerTEU_ACT.[Equipment Class] = vwHoursPartsPerTEU_AOP.[Equipment Class] AND vwHoursPartsPerTEU_ACT.ProductGroupCodeDesc = vwHoursPartsPerTEU_AOP.ProductGroupCodeDesc AND vwHoursPartsPerTEU_ACT.ServiceCode = vwHoursPartsPerTEU_AOP.ServiceCode

) as Operational
ON  Financial.Segment=Operational.Segment and Financial.EstimateCountryName=Operational.Country

Obviously the code is very long and complex, so I was hoping we might be able to resolve this conceptually.

I tried optimize for unknown per a suggestion, but the ad hoc is still way faster than the ITVF. Here's how I tried it:

select * from [aop].[itvfOpsProductivity]('YTD',10,'2017','AOP','2016') 
option (optimize for unknown)

I did further isolation testing and found that it wasn't actually related to the fact that I was running it from an ITVF, because in fact, when I took the contents of the ITVF and replaced the variables with literals, it still ran poorly! Yet, when I run the contents of the ITVF ad hoc, with declared and set parameters, the performance was still fine. I ended up resolving the issue by tuning some of the queries under the hood, but it did not make sense why that worked. Both approaches should've performed the same, I would've thought.

rdg515
  • 75
  • 5
  • 3
    impossible to say without your code and possibly execution plan(s) – Mitch Wheat Nov 18 '16 at 03:29
  • 1
    It would help to see the exact code that you use to call the ITVF and a sample, actual execution plan from both when it runs fast and slow. https://www.brentozar.com/pastetheplan/ – mendosi Nov 18 '16 at 05:53
  • If you declare the variables in an ad-hoc SQL, then the query plan is optimized with unknown values. Being that complex, statistics might be totally of when estimated with actual values. You can compare with the ITVF if you add there option `optimize for unknown` for the variables. Your plan creation might also time out, check that from the actual plan. – James Z Nov 18 '16 at 17:05
  • Note, I simplified the SQL in my original post to further isolate the issue. I tried the optimize for unknown suggestion but no luck. The ITVF is still running a lot longer than the ad hoc. Here's how I tried it: select * from [aop].[itvfOpsProductivity]('YTD',10,'2017','AOP','2016') option (optimize for unknown) – rdg515 Nov 18 '16 at 19:28
  • 1
    Aaah ... the derived table within a derived table within a derived table mess... Throw in some views/IVTF - which will be expanded by the engine as if it were a derived table - and you get a query that is hard to optimize for the engine. I hate repeating myself, so I'll link you some remarks I made in an answer on another question: [here](http://stackoverflow.com/a/35264939/243373). – TT. Nov 19 '16 at 07:18

0 Answers0