I’ve been working on this query handed to me by a developer who wanted to reduce the query execution time even further. Currently, it takes almost around 40 secs and outputs 262K records. Based on the actual execution the query cost is more towards the clustered index scan on “CacheClients”. However, I am not sure if adding a non clustered covering index would benefit it. Also, I see a table scan at the extreme bottom right of the plan and as per the developer they were testing with an index on that table but they weren’t able to figure out an appropriate one that can fit the criteria which is basically they would join on the id fields and run queries based on date period.
USE [ABC] GO /****** Object: View [report].[vw_MonthlyDealAllocations] Script Date: 11/24/2017 9:54:10 PM ******/ --SET ANSI_NULLS ON --GO --SET QUOTED_IDENTIFIER ON --GO --CREATE VIEW [report].[vw_MonthlyDealAllocations] as SELECT PeriodStartDate, PeriodEndDate, D.DealID, Dl.DealName, D.StageID, CONVERT(varchar(1), D.StageID) + ' - ' + DS.Stage Stage, DP.Platform, ISNULL(D.AllocatedOfficeID, Dl.OfficeID) AS AllocatedOfficeID, ISNULL(D.AllocatedDepartmentID, Dl.DepartmentID) AS AllocatedDepartmentID, CASE WHEN ISNULL(D.AllocatedDepartmentID, Dl.DepartmentID) = 1 THEN 'Investment Advisory' WHEN ISNULL(D.AllocatedDepartmentID, Dl.DepartmentID) = 2 THEN 'Debt/EP' WHEN ISNULL(D.AllocatedDepartmentID, Dl.DepartmentID) = 6 THEN 'HFF Securities' ELSE DM.Department END AS AllocatedDepartment, AO.Name AllocatedOffice, CASE WHEN DP.DisplayWithOffice = 1 THEN AO.Name + ' ' + DP.ShortName ELSE AO.Name END AllocatedOfficeDept, Dl.DepartmentID DealDepartmentID, DX.Department DealDepartment, Dl.OfficeID DealOfficeID, DO.Name DealOffice, CASE WHEN DP.DisplayWithOffice = 1 THEN DO.Name + ' ' + DP.ShortName ELSE DO.Name END DealOfficeDept, DT.DealType, D.OfficeAllocVolumePipeline, D.OfficeAllocVolumeCompleted, D.OfficeAllocFeePipeline, D.OfficeAllocFeeCompleted, D.OfficeAllocVolumePipelineGBP, D.OfficeAllocVolumeCompletedGBP, D.OfficeAllocFeePipelineGBP, D.OfficeAllocFeeCompletedGBP, PTG.GroupName, PT.PropertyType, CASE WHEN C.CountryID <> 1 THEN 'Foreign' ELSE R.Region END AS Region, AP.Address1, C.City, S.StateLong, AP.Zip, Investor.ClientName MSA, Cl.ClientName RollupMSA, ITI.InvestorType PrimaryInvestor, ITC.InvestorType PrimaryClient, P.FirstName + ' ' + P.LastName AS PrimaryProducer, I.FirstName + ' ' + I.LastName AS IL_Producer, D.CountDealCompleted, CASE Dl.isSecuritized WHEN 0 THEN 'No' WHEN 1 THEN 'Yes' ELSE '' END AS isSecuritized, CASE Dl.isServiced WHEN 0 THEN 'No' WHEN 1 THEN 'Yes' ELSE '' END AS isServiced, --QCount.QtCt, /*IIF (CASE WHEN D.isCompleted = 1 THEN D.VolumeCompleted WHEN (D.isPipeline = 1 OR D.StageID = 0) THEN D.VolumePipeline ELSE 0 END >= 50000000,1,NULL ) AS Over50M, */ IIF(Dl.PortfolioID IS NOT NULL, 1, NULL) AS isPortfolio, --IIF (isMultitransactional.linktype IS NOT NULL,1,NULL) AS isMultiTransactional, DP.PlatformID, P.EmployeeID AS PrimaryProducerID, I.EmployeeID AS IL_ProducerID, --report.fnReportGetDealClients(D.DealID) Clients, --report.fnReportGetDealInvestors(D.DealID) Investors, DFM.FinancingMethod, IIF((Dl.isSecuritized = 1 AND ISNULL(ISNULL(ITID2C.InvestorType, ITI.InvestorType), 'Unknown') <> 'Agency'), 'Conduit/CMBS', ISNULL(ISNULL(ITID2C.InvestorType, ITI.InvestorType), 'Unknown')) AS InvestorType FROM report.MonthlyDealAllocations D INNER JOIN dbo.Deals Dl ON Dl.DealID = D.DealID INNER JOIN DealTypes DT ON DT.DealTypeID = Dl.DealTypeID INNER JOIN DealPlatforms DP ON DP.PlatformID = DT.PlatformID INNER JOIN DealStages DS ON DS.StageID = D.StageID LEFT JOIN Departments DM ON DM.DepartmentID = ISNULL(D.AllocatedDepartmentID, Dl.DepartmentID) LEFT JOIN Departments DX ON DX.DepartmentID = Dl.DepartmentID LEFT JOIN Offices AO ON AO.OfficeID = ISNULL(D.AllocatedOfficeID, Dl.OfficeID) LEFT JOIN Offices DO ON DO.OfficeID = Dl.OfficeID LEFT JOIN dbo.DealProperties DPP ON D.DealID = DPP.DealID AND DPP.IsMainProperty = 1 LEFT JOIN PropertyTypes PT ON PT.PropertyTypeID = DPP.PropertyTypeID LEFT JOIN PropertyTypeGroups PTG ON PTG.GroupID = PT.GroupID LEFT JOIN DealProperties AS DPZ ON D.DealID = DPZ.DealID AND DPZ.isMainProperty = 1 LEFT JOIN Assets AP ON DPZ.AssetID = AP.AssetID LEFT JOIN Cities C ON C.CityID = AP.CityID LEFT JOIN States S ON S.StateID = C.StateID LEFT JOIN Regions R ON ISNULL(C.RCARegionID, ISNULL(S.RCARegionID, S.RegionID)) = R.RegionID LEFT JOIN DealsToClients DTCC ON D.DealID = DTCC.DealID AND DTCC.isPrimary = 1 AND DTCC.MemberType IN (1, 3) LEFT JOIN DealsToClients DTCI ON D.DealID = DTCI.DealID AND DTCI.isPrimary = 1 AND DTCI.MemberType IN (2, 4) LEFT JOIN CacheClients Cl ON DTCC.ClientID = Cl.ClientID LEFT JOIN CacheClients Investor ON DTCI.ClientID = Investor.ClientID LEFT JOIN InvestorTypes ITI ON Investor.InvestorTypeID = ITI.InvestorTypeID LEFT JOIN InvestorTypes ITC ON Cl.InvestorTypeID = ITC.InvestorTypeID --LEFT JOIN (SELECT DISTINCT D2D.DealID, D2D.LinkType FROM DealsToDeals D2D WHERE LinkType = 2) isMultitransactional ON D.DealID = isMultitransactional.DealID LEFT JOIN DealsToEmployees EP ON D.DealID = EP.DealID AND EP.MemberType = 1 AND EP.isPrimary = 1 LEFT JOIN DealsToEmployees EI ON D.DealID = EI.DealID AND EI.MemberType = 2 AND EI.isPrimary = 1 LEFT JOIN Employees P ON EP.EmployeeID = P.EmployeeID LEFT JOIN Employees I ON EI.EmployeeID = I.EmployeeID LEFT JOIN DealFinancingMethods DFM ON Dl.DealFinancingMethodID = DFM.DealFinancingMethodsID LEFT JOIN InvestorTypes AS ITID2C ON DTCI.InvestorTypeID = ITID2C.InvestorTypeID --LEFT JOIN (SELECT DealID, COUNT(QuoteID) AS QtCt FROM Quotes WHERE DateRemoved IS NULL GROUP BY DealID) QCount ON D.dealid = Qcount.DealID go
Below is the index they created.
USE [ABC] GO /****** Object: Index [IX_MonthlyDealAllocations_DealID_AllocDeptID_AllocOfficeID] Script Date: 11/24/2017 11:43:00 PM ******/ CREATE NONCLUSTERED INDEX [IX_MonthlyDealAllocations_DealID_AllocDeptID_AllocOfficeID] ON [report].[MonthlyDealAllocations] ( [DealID] ASC, [AllocatedDepartmentID] ASC, [AllocatedOfficeID] ASC ) INCLUDE ( [PeriodStartDate], [PeriodEndDate], [CountDealCompleted], [StageID]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO
Execution plan link
SQL Script file (Includes schema and index DDL)
If you run your query with
SET STATISTICS IO ON; that’ll give you information about IO required for all of your joined tables. Based on estimated plan costs you’ll get the most benefit from adding a covering index on the
CacheClients table. That table has around 50 columns but you only use three columns in your query. Creating an index on
InvestorTypeID will reduce IO requirements for that table because SQL Server will be able to scan the index that contains only those three columns instead of the full table. You will see benefits even if the join type doesn’t change.
Other than that there’s not a lot going here. Your cardinality estimate starting at
MonthlyDealAllocations never changes, so it’s not like you should be joining to a specific table first to improve performance. If possible, changing some of the joins expected to change cardinality to
LEFT OUTER JOINS or defining unique indexes on some of the tables could improve your estimates to keep them at a constant 262336 throughout the plan. This could have a minor benefit in avoiding the tempdb spill at node id 61.
If you’re really desperate you could try a
MAXDOP 1 hint or disabling
MERGE JOINS via
OPTION (LOOP JOIN, HASH JOIN) just to see what happens to the query’s runtime. As is you’re asking for data from a bunch of different tables and SQL Server is going to take time to do all of those joins. There’s no magic query rewrite that’s going to change that. I recommend approaching this problem by defining a minimum acceptable query performance time target instead of just wanting to make it faster without a concrete goal in mind.
Your Query is simple and lengthy.
i) you need minor correction in your query.Hope your query is giving correct output.Also hope all other left join are justified.
LEFT JOIN Departments DM ON DM.DepartmentID = ISNULL(D.AllocatedDepartmentID, Dl.DepartmentID) LEFT JOIN Departments DX ON DX.DepartmentID = Dl.DepartmentID
This can be written as,
LEFT JOIN Departments DM ON DM.DepartmentID = D.AllocatedDepartmentID LEFT JOIN Departments DX ON DX.DepartmentID = Dl.DepartmentID LEFT JOIN Offices AO ON AO.OfficeID = ISNULL(D.AllocatedOfficeID, Dl.OfficeID) LEFT JOIN Offices DO ON DO.OfficeID = Dl.OfficeID
Rewrite it as,
LEFT JOIN Offices AO ON AO.OfficeID = D.AllocatedOfficeID LEFT JOIN Offices DO ON DO.OfficeID = Dl.OfficeID
Replace this line with,
LEFT JOIN Cities C ON C.CityID = AP.CityID LEFT JOIN States S ON S.StateID = C.StateID LEFT JOIN Regions R ON ISNULL(C.RCARegionID, ISNULL(S.RCARegionID, S.RegionID)) = R.RegionID LEFT JOIN Cities C ON C.CityID = AP.CityID LEFT JOIN States S ON S.StateID = C.StateID LEFT JOIN Regions R ON R.RegionID=C.RCARegionID LEFT JOIN Regions R1 R1.RegionID=(S.RCARegionID and S.RCARegionID is not NULL) or (S.RegionID and S.RCARegionID is NULL))
MAXDOP : I do not have much knowledge on MAXDOP.your current MAXDOP is 4.if it is reduce to MAXDOP=2 through query HINT then there might be boost in speed.I am not sure at the same time.
Index : IMHO,you should drop your existing composite NON CI on MonthlyDealAllocations,becasue composite index do not work in this manner.Create seperate NON CI for each and keep one covering index.Why there is no CI in MonthlyDealAllocations .if there is any CI and it us being use then no need of covering index.
Also there should be index on each table which has more than 400 rows.I am not sure about number.