Query performance tuning - Slow execution time

  • ChrisM@Work - Wednesday, August 16, 2017 1:44 AM

    ffarouqi - Tuesday, August 15, 2017 3:37 PM

    Jonathan AC Roberts - Tuesday, August 15, 2017 1:52 PM

    ffarouqi - Tuesday, August 15, 2017 1:36 PM

    Can you suggest what index would benefit here on the temp table to have it fetch quickly when doing a join. Can we do something on the joining columns only or do we need to include other columns as well.

    Can you provide a list with the table name and how many rows are in each table?

    I have attached the list of tables with the rows in each. By the way I was asking about the temp table as well. Based on the message the table is holding 2181 records in temp table from that function.

    Here's what I suggest you do:
    Run the results of the function into a temp table and reference the temp table in your query instead of the function, as already mentioned. Save and post here an actual execution plan from the modified query.
    Post up the definitions of all of the views referenced by the query.
    Cheers

    Okay. I have attached the query plan and the script for the views referenced after making the said modification.

  • Try adding the following indexes
    CREATE INDEX IX_#ReportDealCore_1 ON #ReportDealCore(DealID)
    CREATE INDEX IX_Activities_1 ON Activities(DealID,Type)
    CREATE INDEX IX_DealProperties_1 ON DealProperties(DealID,IsMainProperty,DateRemoved)
    CREATE INDEX IX_Regions_1 ON Regions(CityID)
    CREATE INDEX IX_DealsToClients_1 ON (DealID,isPrimary,MemberType)
    CREATE INDEX IX_CacheClients_1 ON CacheClients(ClientID)
    CREATE INDEX IX_DealsToEmployees_1 ON DealsToEmployees(DealID,MemberType,isPrimary)

    Then check the query plan to see which indexes were used. (Drop the new ones that aren't used)
    Also, click the display estimated execution plan button in SSMS and see if that suggests any indexes.

  • Jonathan AC Roberts - Wednesday, August 16, 2017 2:17 PM

    Try adding the following indexes
    CREATE INDEX IX_#ReportDealCore_1 ON #ReportDealCore(DealID)
    CREATE INDEX IX_Activities_1 ON Activities(DealID,Type)
    CREATE INDEX IX_DealProperties_1 ON DealProperties(DealID,IsMainProperty,DateRemoved)
    CREATE INDEX IX_Regions_1 ON Regions(CityID)
    CREATE INDEX IX_DealsToClients_1 ON (DealID,isPrimary,MemberType)
    CREATE INDEX IX_CacheClients_1 ON CacheClients(ClientID)
    CREATE INDEX IX_DealsToEmployees_1 ON DealsToEmployees(DealID,MemberType,isPrimary)

    Then check the query plan to see which indexes were used. (Drop the new ones that aren't used)
    Also, click the display estimated execution plan button in SSMS and see if that suggests any indexes.

    Jonathan - For the index IX_Regions_1 on REGIONS(CityID) - Column name CityID is not valid. Did you meant City table or RegionID colum in Regions table

  • I'm not sure, I didn't mean Regions( RegionID) as that's already the PK.
    Just ignore that index for now.

  • Jonathan AC Roberts - Tuesday, August 15, 2017 11:52 AM

    With these type of query you can sometimes get a lot of success from breaking it up into several queries inserting the data into temporary tables, adding indexes to the temporary tables and then querying the temporary tables to get the results. You could start off by inserting the results of he function into a temporary table:
    IF OBJECT_ID('temdb..#ReportDealCore','U') IS NOT NULL
      DROP TABLE #ReportDealCore

    SELECT FDC.DealID,
       FDC.StageID,
       FDC.PlatformID
    INTO #ReportDealCore
    FROM report.fnReportDealCore(@StartDate, @EndDate, @Type) AS FDC

    Then query the able  #ReportDealCore instead of calling the function in the query.

    Then just select several tables from the query join them together and insert the results into another temporary table, then build up the whole query from joins to the temporary tables,

    Preach it, Brother! 😉  A whole lot of people don't understand that "set based" doesn't actually mean "all in one query" especially when a DISTICT has to be used to overcome the accidental many-to-many joins formed by such monsters.

    --Jeff Moden


    RBAR is pronounced "ree-bar" and is a "Modenism" for Row-By-Agonizing-Row.
    First step towards the paradigm shift of writing Set Based code:
    ________Stop thinking about what you want to do to a ROW... think, instead, of what you want to do to a COLUMN.

    Change is inevitable... Change for the better is not.


    Helpful Links:
    How to post code problems
    How to Post Performance Problems
    Create a Tally Function (fnTally)

  • -- Here's an alternative to vwReportMemberTypeResults_Employee
    -- which reads DealsToEmployees and Employees only once, instead of four times.
    -- Test it, when you're satisfied that it works, sub it into your query.
    CREATE VIEW [dbo].[vwReportMemberTypeResults_Employee_V2]
    AS
    SELECT
     D.DealID,
     D.DealName,
     x.InitialLead,
     x.Analyst,
     x.Producer,
     x.CrossProducer,
     x.InitialLeadInitals,
     x.AnalystInitials,
     x.ProducerInitials,
     x.CrossProducerInitials
    FROM dbo.Deals AS D
    OUTER APPLY (
     SELECT
      InitialLead  = MAX(CASE WHEN de.MemberType = 2 THEN e.FirstName + ' ' + e.LastName END),
      Analyst   = MAX(CASE WHEN de.MemberType = 3 THEN e.FirstName + ' ' + e.LastName END),
      Producer  = MAX(CASE WHEN de.MemberType = 1 THEN e.FirstName + ' ' + e.LastName END),
      CrossProducer = MAX(CASE WHEN de.MemberType = 9 THEN e.FirstName + ' ' + e.LastName END),
      InitialLeadInitals  = MAX(CASE WHEN de.MemberType = 2 THEN LEFT(e.FirstName, 1) + ISNULL(LEFT(e.MiddleName, 1), '') + LEFT(e.LastName, 1) END),
      AnalystInitials   = MAX(CASE WHEN de.MemberType = 3 THEN LEFT(e.FirstName, 1) + ISNULL(LEFT(e.MiddleName, 1), '') + LEFT(e.LastName, 1) END),
      ProducerInitials  = MAX(CASE WHEN de.MemberType = 1 THEN LEFT(e.FirstName, 1) + ISNULL(LEFT(e.MiddleName, 1), '') + LEFT(e.LastName, 1) END),
      CrossProducerInitials = MAX(CASE WHEN de.MemberType = 9 THEN LEFT(e.FirstName, 1) + ISNULL(LEFT(e.MiddleName, 1), '') + LEFT(e.LastName, 1) END)
     FROM dbo.DealsToEmployees de
     INNER JOIN dbo.Employees e
      ON e.EmployeeID = de.EmployeeID
     WHERE de.isPrimary = 1
      AND de.MemberType IN (1,2,3,9)
      AND de.DealID = d.DealID -- outer reference
    ) x

    -- Test that the new view returns the same results as the existing one:
    SELECT * FROM [dbo].[vwReportMemberTypeResults_Employee]
    EXCEPT
    SELECT * FROM [dbo].[vwReportMemberTypeResults_Employee_V2]
    -- and
    SELECT * FROM [dbo].[vwReportMemberTypeResults_Employee_V2]
    EXCEPT
    SELECT * FROM [dbo].[vwReportMemberTypeResults_Employee]
    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • ffarouqi - Wednesday, August 16, 2017 1:05 PM

    ChrisM@Work - Wednesday, August 16, 2017 1:44 AM

    ffarouqi - Tuesday, August 15, 2017 3:37 PM

    Jonathan AC Roberts - Tuesday, August 15, 2017 1:52 PM

    ffarouqi - Tuesday, August 15, 2017 1:36 PM

    Can you suggest what index would benefit here on the temp table to have it fetch quickly when doing a join. Can we do something on the joining columns only or do we need to include other columns as well.

    Can you provide a list with the table name and how many rows are in each table?

    I have attached the list of tables with the rows in each. By the way I was asking about the temp table as well. Based on the message the table is holding 2181 records in temp table from that function.

    Here's what I suggest you do:
    Run the results of the function into a temp table and reference the temp table in your query instead of the function, as already mentioned. Save and post here an actual execution plan from the modified query.
    Post up the definitions of all of the views referenced by the query.
    Cheers

    Okay. I have attached the query plan and the script for the views referenced after making the said modification.

    Oh good grief.
    The view vw_Deals is one of the finest examples of cowboy coding I've ever seen.
    It's going to take some time to refactor it to work properly.

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • Jonathan AC Roberts - Wednesday, August 16, 2017 4:14 PM

    I'm not sure, I didn't mean Regions( RegionID) as that's already the PK.
    Just ignore that index for now.

    Hi Jonathan,

    As per your suggestion I created the indexes but it hasn't really helped the query execution time a lot and only a couple of indexes are being used. However, the execution time is still around 4 odd minutes. I have also attached the query plan for further review.

    Indexes used by the query
    IX_Activities_1 (You will notice that it is IX_Activities_5 as I already had one with that number)
    IX_DealsToClients_1

    Kindly, let me know what else can we do to improve it even further to reduce the execution time to 2 odd minutes...the way you've helped me out before.

    Thanks again.

  • -- Here's an attempt to remove the complex view vwDeals from the query. Run it and compare with the original.
    -- Note that I can't find [AccountingDate] in the view. You will have to track this down.
    -- I can't find which table [HFFTotalFee] is from, the reference isn't fully qualified in vwDeals so you will have to find out where this is from too.
    -- There are some other changes in the FROM list.
    SELECT FDC.DealID,
       FDC.StageID,
       FDC.PlatformID
     INTO #ReportDealCore
     FROM report.fnReportDealCore(@StartDate, @EndDate, @Type) AS FDC
    Select Distinct
     D.DealID,
     D.DealName,
     D.Amount,
     --D.AccountingDate AS AccountingDate, -- not in vwDeals
     D.Description,
     --D.HFFTotalFee, -- don't know where this comes from
     D.DealTypeID, -- join column found in Deals table
     D.OfficeID, -- join column found in Deals table
     PT.GroupID,
     PT.PropertyType,
     DPF.PlatformID,
     DPF.Platform,
     DS.StageID,
     DS.Stage,
     DT.DealTypeID,
     DT.DealType,
     AT.ActivityTypeID,
     AT.ActivityType,
     LO.InvestorID,
     LO.Investor,
     --LO.Amount,
     C.City,
     C.State,
     case when C.CountryID <> 1 then 'Foreign' else R.Region end as Region,
     MTRE.Producer as PrimaryProducer,
     O.Name as HFFOffice
    FROM #ReportDealCore AS FDC
    INNER JOIN DealProperties AS DP ON DP.DealID = FDC.DealID AND DP.IsMainProperty = 1 AND DP.DateRemoved IS NULL
    --INNER JOIN report.vwDeals AS D ON D.DealID = FDC.DealID -- ##
    INNER JOIN Deals AS D ON D.DealID = FDC.DealID -- ##
    INNER JOIN Assets AS A ON DP.AssetID = A.AssetID
    LEFT JOIN Activities as ACT on ACT.DealID = FDC.DealID AND ACT.Type >= 2 AND ACT.Type <=7
    LEFT JOIN ActivityTypes AS AT ON AT.ActivityTypeID = ACT.Type -- ##
    LEFT JOIN DealStages AS DS ON DS.StageID = FDC.StageID -- ##
    LEFT JOIN DealTypes DT ON DT.DealTypeID = D.DealTypeID -- ##
    LEFT JOIN DealPlatforms AS DPF ON DPF.PlatformID = FDC.PlatformID -- ##
    LEFT JOIN PropertyTypes AS PT ON PT.PropertyTypeID = DP.PropertyTypeID -- ##
    LEFT JOIN Cities as C on C.CityID = A.CityID -- ##
    LEFT JOIN States as S on S.StateID = C.StateID -- ##
    --LEFT JOIN Regions as R ON isnull(C.RCARegionID, isnull(S.RCARegionID,S.RegionID)) = R.RegionID -- ##
    LEFT JOIN Regions as R ON R.RegionID = COALESCE(C.RCARegionID,S.RCARegionID,S.RegionID)
    LEFT JOIN Offices as O on O.OfficeID = D.OfficeID -- ##
    LEFT JOIN vwReportMemberTypeResults_Employee AS MTRE ON MTRE.DealID = D.DealID -- ##
    --LEFT JOIN vwReportMemberTypeResults_Client AS MTRC ON MTRC.DealID = D.DealID
    --LEFT JOIN vwReportOnlyLoserOffers AS LO ON LO.DealID = D.DealID -- ##
    INNER JOIN vwReportOnlyLoserOffers AS LO ON LO.DealID = D.DealID -- ##
    WHERE D.AccountingDate BETWEEN @StartDate AND @EndDate
     AND LO.isForeign = 1 -- this converts left join to inner join
    ORDER BY D.DealID ASC, LO.InvestorID ASC, AT.ActivityTypeID DESC
    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • ffarouqi - Thursday, August 17, 2017 3:51 PM

    Jonathan AC Roberts - Wednesday, August 16, 2017 4:14 PM

    I'm not sure, I didn't mean Regions( RegionID) as that's already the PK.
    Just ignore that index for now.

    Hi Jonathan,

    As per your suggestion I created the indexes but it hasn't really helped the query execution time a lot and only a couple of indexes are being used. However, the execution time is still around 4 odd minutes. I have also attached the query plan for further review.

    Indexes used by the query
    IX_Activities_1 (You will notice that it is IX_Activities_5 as I already had one with that number)
    IX_DealsToClients_1

    Kindly, let me know what else can we do to improve it even further to reduce the execution time to 2 odd minutes...the way you've helped me out before.

    Thanks again.

    If an equivalent index already exists then drop the new index.
    My idea is that you try splitting the query by populating temporary tables. For example I would start something like this:
    SELECT FDC.DealID,
       FDC.StageID,
       FDC.PlatformID
    INTO #ReportDealCore
    FROM report.fnReportDealCore(@StartDate, @EndDate, @Type) AS FDC

    CREATE INDEX IX_#ReportDealCore_1 ON #ReportDealCore(DeailID)

    SELECT D.DealID,
       D.DealName,
       D.Amount,
       D.Description,
       D.HFFTotalFee
      INTO #vwDeals
      FROM report.vwDeals D
     WHERE (D.AccountingDate BETWEEN @StartDate AND @EndDate)
      AND EXISTS(SELECT * FROM #ReportDealCore r WHERE r.DealID = D.DealID)

    CREATE INDEX IX_#vwDeals_1 ON #ReportDealCore(DealID)
    CREATE INDEX IX_#vwDeals_2 ON #ReportDealCore(AssetID)

    SELECT C.City,
       C.State,
       A.AssetId
    INTO #AssetCity
    FROM Assets AS A   
    LEFT JOIN Cities AS C
       ON C.CityID = A.CityID
    WHERE EXISTS(SELECT * FROM #ReportDealCore rdc WHERE rdc.AssetID = A.AssetID)
    CREATE INDEX IX_#AssetCity_1 ON #ReportDealCore(AssetID)

    And then update the main query to replace permanent tables with the temporary tables.
    If you started with the above temporary tables the joins would look like this (you'd also have to change the aliases in the select to match)
    FROM #ReportDealCore AS FDC
    INNER JOIN DealProperties AS DP
       ON DP.DealID = FDC.DealID
        AND DP.IsMainProperty = 1
        AND DP.DateRemoved IS NULL
    INNER JOIN #vwDeals AS D
       ON D.DealID = FDC.DealID
    INNER JOIN #AssetCity A
       ON DP.AssetID = A.AssetID
    LEFT JOIN Activities AS ACT
       ON ACT.DealID = FDC.DealID
        AND ACT.Type >= 2
        AND ACT.Type <= 7
    LEFT JOIN ActivityTypes AS AT
       ON AT.ActivityTypeID = ACT.Type
    LEFT JOIN DealStages AS DS
       ON DS.StageID = FDC.StageID
    LEFT JOIN DealTypes AS DT
       ON DT.DealTypeID = D.DealTypeID
    LEFT JOIN DealPlatforms AS DPF
       ON DPF.PlatformID = FDC.PlatformID
    LEFT JOIN PropertyTypes AS PT
       ON PT.PropertyTypeID = DP.PropertyTypeID
    LEFT JOIN States AS S
       ON S.StateID = C.StateID
    LEFT JOIN Regions AS R
       ON isnull(C.RCARegionID, isnull(S.RCARegionID, S.RegionID)) = R.RegionID
    LEFT JOIN Offices AS O
       ON O.OfficeID = D.OfficeID
    LEFT JOIN vwReportMemberTypeResults_Employee AS MTRE
       ON MTRE.DealID = D.DealID
    LEFT JOIN vwReportMemberTypeResults_Client AS MTRC
       ON MTRC.DealID = D.DealID
    --LEFT JOIN vwReportLosingOffers AS LO ON LO.DealID = D.DealID
    LEFT JOIN vwReportOnlyLoserOffers AS LO
       ON LO.DealID = D.DealID
    WHERE(LO.isForeign = 1)
     ORDER BY D.DealID ASC, LO.InvestorID ASC, AT.ActivityTypeID DESC

    If you take this approach it's very useful to have some sort of logging between each "select" to log a timestamp and a message. After the procedure has run you can then view a log and identify which parts took longest then tackle them. One way of doing this is to crate a new logging table and a stored procedure that you call with some parameters (e.g StoredProcedureName and Section of SP you are in) that inserts a row with a timestamp into your new table. Then just call this stored procedure after each "select into" statement and the final select that produces the report. 
    Also you should try ChrisM's suggestion of rewriting the views so they are faster.

Viewing 10 posts - 16 through 24 (of 24 total)

You must be logged in to reply to this topic. Login to reply