Sql-server – Can this query be optimized further

pagingsql serversql-server-2012t-sql

I have 100K records in my Documents table. This is my query which gives a page of 50 records in roughly 750 ms. I would like to further bring it down if possible. I am using Row_Number function to page.

exec sp_executesql N'
set arithabort off;
set statistics time on;
set transaction isolation level read uncommitted;

With cte as
(
    Select
        peta_rn = ROW_NUMBER() OVER (ORDER BY  d.LastStatusChangedDateTime  desc),
        d.DocumentID, d.IsReEfiled, d.IGroupID, d.ITypeID, d.RecordingDateTime,
        d.CreatedByAccountID, d.JurisdictionID,
        d.LastStatusChangedDateTime as LastStatusChangedDateTime,  d.IDate,
        d.InstrumentID, d.DocumentStatusID , u.Username, it.Abbreviation AS ITypeAbbreviation,
        ig.Abbreviation AS IGroupAbbreviation, d.DocumentDate
        From ITypes it
        Inner Join Documents d on it.ITypeID = d.ITypeID
        Inner Join Users u on d.UserID = u.UserID
        Inner Join IGroupes ig on ig.IGroupID = d.IGroupID
        Where
            1=1 AND
            (  d.DocumentStatusID = 9  ) And
            (CreatedByAccountID = @0 Or DocumentStatusID = @1 Or DocumentStatusID = @2)  And
            (  d.IGroupID = @3   Or d.IGroupID = @4   ) And
            (d.JurisdictionID = @5 Or DocumentStatusID = @6 Or DocumentStatusID = @7)
)
Select
    cte.DocumentID, cte.IsReEfiled, cte.IGroupID, cte.ITypeID, cte.RecordingDateTime,
    cte.CreatedByAccountID, cte.JurisdictionID,
    cte.LastStatusChangedDateTime as LastStatusChangedDateTime, cte.IDate, cte.InstrumentID,
    cte.DocumentStatusID,cte.IGroupAbbreviation, cte.Username, j.JDAbbreviation, inf.DocumentName,
    cte.ITypeAbbreviation, cte.DocumentDate, ds.Abbreviation as DocumentStatusAbbreviation,
    ds.Name as DocumentStatusName, (SELECT COUNT(*) FROM CTE) AS TotalRecords
    FROM cte
    Inner Join DocumentStatuses ds On cte.DocumentStatusID = ds.DocumentStatusID
    Left Join InstrumentFiles inf On cte.DocumentID = inf.DocumentID
    Inner Join Jurisdictions j on j.JurisdictionID = cte.JurisdictionID
    Where
        1=1 And
        peta_rn>@8 AND
        peta_rn<=@9
    Order by peta_rn',
    N'@0 int,@1 int,@2 int,@3 int,@4 int,@5 int,@6 int,@7 int,@8 int,@9 int',
    @0=44,@1=5,@2=9,@3=3,@4=4,@5=1,@6=5,@7=9,@8=90000,@9=90050

This is my statistics output:
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.

(50 row(s) affected)

 SQL Server Execution Times:
   CPU time = 717 ms,  elapsed time = 727 ms.

and this is my query plan XML. All indexes are created properly. I am unable to post here due to body limit of 30K.

https://www.dropbox.com/s/vrx5ec6cpi8vn33/plan.txt

Any help would be appreciated. Thanks!

Best Answer

First of all, as you are getting sub-second performance how much time do you want to spend on this? If you spend an hour on it, and you tune it to half a second, how long will it take to pay for itself? If this query runs several times a second, then I'm with you, let's tune it. Otherwise it's not really worth it.

Having said that, there are a number of warning signs in your query and plan which need closer attention, if you have time. This is a strange looking query - is it dynamically generated?

1) Rowcount Discrepancy

The dbo.IGroupes table is showing Estimated Rows (7) * Estimated Executions (100.99), so ~707 rows, versus 630,346 Actual Rows. Rowcounts which are more of an order of magnitude difference tend to be a bit of a warning sign. So why do you think this is? What is the relationship between the dbo.Documents table and the dbo.IGroupes table? Should you be getting a distinct list from here instead? This would also mean the estimated cost for this operator is way off too. There are other rowcount discrepancies and Plan Explorer does a good job of showing this:

enter image description here

2) Optimizer Timeout

If you click the SELECT operator in the plan and look at the properties, you will see the property "Reason for Early Termination of Statement Optimization" marked as "Time Out". This means the optimizer ran out of "time" (more like iterations than time) and returned with the lowest estimated cost plan it had at the time. This is generally an indication the query is too complicated and the workaround is to simplify. Looking at your query you have the dummy 1=1 clause and a number of other OR clauses. Can this be simplified?

A few other points: you should be aware that if you have the "Include Actual Execution Plan" option on, this negatively affects the values from SET STATISTICS TIME ON. This is the Observer Effect. Are your values from a run with the plan on or off?

One of my normal approaches to query tuning is to break it up. This helps identify true bottlenecks. It may be that later on I put the query back together. Here's one example, but it's worth noting in my test rig, there's no real performance difference between the original query and this one:

IF OBJECT_ID('tempdb..#tmp') IS NOT NULL DROP TABLE #tmp

SELECT
      IDENTITY( INT, 1, 1 ) rn
    , CAST( d.DocumentID AS INT ) DocumentID
    , d.IsReEfiled
    , d.IGroupID
    , d.ITypeID
    , d.RecordingDateTime
    , d.CreatedByAccountID
    , d.JurisdictionID
    , d.LastStatusChangedDateTime
    , d.IDate
    , d.InstrumentID
    , d.DocumentStatusID
    , d.DocumentDate
    , d.UserId

INTO #tmp
FROM Documents d
WHERE ( d.DocumentStatusID = 9 )
  AND ( d.CreatedByAccountID = @0 OR d.DocumentStatusID = @1 OR d.DocumentStatusID = @2 )
  AND ( d.IGroupID = @3 Or d.IGroupID = @4 )
  AND (d.JurisdictionID = @5 Or d.DocumentStatusID = @6 Or d.DocumentStatusID = @7 )
ORDER BY d.LastStatusChangedDateTime DESC


SELECT
      t.DocumentID
    , t.IsReEfiled
    , t.IGroupID
    , t.ITypeID
    , t.RecordingDateTime
    , t.CreatedByAccountID
    , t.JurisdictionID
    , t.LastStatusChangedDateTime
    , t.IDate
    , t.InstrumentID
    , t.DocumentStatusID
    , ig.Abbreviation AS IGroupAbbreviation
    , u.Username
    , j.JDAbbreviation
    , inf.DocumentName
    , it.Abbreviation AS ITypeAbbreviation
    , t.DocumentDate
    , it.Abbreviation AS DocumentStatusAbbreviation
    , ds.Name AS DocumentStatusName
    , ( SELECT COUNT(*) FROM #tmp ) AS TotalRecords
FROM #tmp t
    INNER JOIN dbo.ITypes it ON t.ITypeID = it.ITypeID
    INNER JOIN dbo.Users u ON t.UserID = u.UserID
    INNER JOIN dbo.IGroupes ig ON t.IGroupID = ig.IGroupID
    INNER JOIN dbo.DocumentStatuses ds On t.DocumentStatusID = ds.DocumentStatusID 
    INNER JOIN dbo.Jurisdictions j on t.JurisdictionID = j.JurisdictionID 
    LEFT JOIN dbo.InstrumentFiles inf On t.DocumentID = inf.DocumentID
WHERE rn > @8 AND rn <= @9
ORDER BY rn
GO

If you want to carry on, please provide the schema and indexes from your tables, plus either the stats or an idea of the rowcounts.