简体   繁体   中英

Performance tuning of row-based subqueries: LEFT OUTER JOIN and OUTER APPLY, alternatives?

The performance of a certain query (on a Dynamics CRM 2011 database) was abysmal. Since it is a normalized datamodel, but a flattened view on this data (an SSRS report) is required, I did a lot (12) of LEFT OUTER JOINs with a SELECT TOP (1) subquery, eg:

LEFT JOIN Filterednew_rates FRates ON FRates.new_ratesid =
    (SELECT TOP (1)
        FRR.new_ratesid
     FROM Filterednew_rates FRR
     WHERE
        FRR.new_contractid = FContract.contractid
        AND FRR.statuscode <> 803270000 -- NOT Obsolete
     ORDER BY FRR.new_startdate DESC
    )

This worked for a small number of result rows (like 10 seconds for 3 rows), but I've had it run for 45 minutes on about 100 expected result rows (the amount of source data is the same, just different WHERE clause). So I started looking for ways to "force" SQL Server to run the subqueries per row (since logically to me, that would scale linearly).

Then I read The power of T-SQL's APPLY operator and managed to change the above to

OUTER APPLY (
    SELECT TOP (1)
        FRR.*
     FROM Filterednew_rates FRR
     WHERE
        FRR.new_contractid = FContract.contractid
        AND FRR.statuscode <> 803270000 -- NOT Obsolete
     ORDER BY FRR.new_startdate DESC
) AS FRates

Which made the execution time scale about linearly with the number of result records (about 3:30 minutes for 100 rows, still about 6 seconds for 3 rows). Somehow this made SQL Server decide to change the query execution plan for the better!

Is there any other way in SQL to "flatten" a normalized datamodel without resorting to Integration/Analysis Services?

EDIT:

Thanks for the input @Aaron and @BAReese. I'll try to apply PIVOT/UNPIVOT and the Windowing Functions and report back on query performance differences.

And by popular request, a larger part of the query. I've tried to "anonymize" the query a bit, so the actual query properties are more descriptive.

OUTER APPLY (
    SELECT TOP (1)
        FCO.*
     FROM Filterednew_contractoption FCO
     WHERE
        FCO.new_contractid = FContract.contractid
        AND FCO.new_included = 1 -- Is Included
        AND FCO.new_optionidname = 'SomeOption1'
) AS FOptionSomeOption1
OUTER APPLY (
    SELECT TOP (1)
        FCO.*
     FROM Filterednew_contractoption FCO
     WHERE
        FCO.new_contractid = FContract.contractid
        AND FCO.new_included = 1 -- Is Included
        AND FCO.new_optionidname = 'SomeOption2'
) AS FOptionSomeOption2
OUTER APPLY (
    SELECT TOP (1)
        FCD.*
     FROM FilteredContractDetail FCD
     JOIN FilteredProduct FProd ON FCD.productid = FProd.productid
     WHERE
         FContract.contractid = FCD.contractid
         AND FCD.new_included = 1 -- Is Included
         AND FProd.productnumber IN ('COLDEL1', 'COLDEL2', 'COLDEL3', 'COLDEL4')
) AS FColDelContractDetail
LEFT JOIN FilteredProduct FColDelProduct ON FColDelContractDetail.productid = FColDelProduct.productid
OUTER APPLY (
    SELECT TOP (1)
        FCO.*
     FROM Filterednew_contractoption FCO
     JOIN Filterednew_contractdetail_new_contractoptions FCD_CO ON FCO.new_contractoptionid = FCD_CO.new_contractoptionid
     WHERE
        FCD_CO.contractdetailid = FColDelContractDetail.contractdetailid
        AND FCO.new_included = 1 -- Is Included
        AND FCO.new_optionidname LIKE 'Input1'
) AS FColDelInput1Option
OUTER APPLY (
    SELECT TOP (1)
        FCO.*
     FROM Filterednew_contractoption FCO
     JOIN Filterednew_contractdetail_new_contractoptions FCD_CO ON FCO.new_contractoptionid = FCD_CO.new_contractoptionid
     WHERE
        FCD_CO.contractdetailid = FColDelContractDetail.contractdetailid
        AND FCO.new_included = 1 -- Is Included
        AND FCO.new_optionidname LIKE 'Input2'
) AS FColDelInput2Option
OUTER APPLY (
    SELECT TOP (1)
        FCO.*
     FROM Filterednew_contractoption FCO
     JOIN Filterednew_contractdetail_new_contractoptions FCD_CO ON FCO.new_contractoptionid = FCD_CO.new_contractoptionid
     WHERE
        FCD_CO.contractdetailid = FColDelContractDetail.contractdetailid
        AND FCO.new_included = 1 -- Is Included
        AND FCO.new_optionidname LIKE 'Input3'
) AS FColDelInput3Option
OUTER APPLY (
    SELECT TOP (1)
        FCP.*
     FROM Filterednew_price FCP
     WHERE FCP.new_contractid = FContract.contractid
     AND FCP.statuscode <> 803270000 -- NOT Obsolete
     ORDER BY FCP.new_validfrom DESC
) AS FPrice
OUTER APPLY (
    SELECT TOP (1)
        FCFR.*
     FROM Filterednew_contractforecastresult FCFR
     WHERE FCFR.new_contractid = FContract.contractid
     ORDER BY FCFR.createdon DESC
) AS FForecastResult

Since you're using SQL Server, this would be an excellent opportunity to use windowing functions to improve efficiency.

something like this might help it run quicker:

LEFT JOIN
    (
    SELECT FRR.new_contractid, ROW_NUMBER() over(partition by FRR.new_contractid
                                            order by FRR.new_startdate DESC) as Last_ID
    FROM Filterednew_rates as FRR
    WHERE FRR.statuscode <> 803270000 -- NOT Obsolete
    ) AS FRates
    ON FRates.new_contractid = FContract.contractid
    and FRates.Last_ID = 1

What this should do is allow the derived table to produce a list of all contractids but give a priority list. In theory, it will be easier on the server and you won't be hitting the table more times than necessary. Another thing you can do is add SET STATISTICS IO ON and SET STATISTICS TIME ON to the top of your query (assuming you're testing this in SQL Server Management Studio). If in SSMS, you'll get a log on the [Messages] tab telling what the logical/physical read count of each table is, as well as the amount of time spent querying.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM