4

I have a fairly complex SQL query that involves returning about 20 columns from a large number of joins, used to populate a grid of results in a UI. It also uses a couple of CTEs to pre-filter the results. I've included an approximation of the query below (I've commented out the lines that fix the performance)

As the amount of data in the DB increased, the query performance tanked pretty hard, with only about 2500 rows in the main table 'Contract'.

Through experimentation, I found that by just removing the order, offset fetch at the end the performance went from around 30sec to just 1 sec!

order by 1 OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY

This makes no sense to me. The final line should be pretty cheap, free even when the OFFSET is zero, so why is it adding 29secs on to my query time?

In order to maintain the same function for the SQL, I adapted it so that I first select into #TEMP, then perform the above order-offset-fetch on the temp table, then drop the temp table. This completes in about 2-3 seconds.

My 'optimisation' feels pretty wrong, surely there's a more sane way to achieve the same speed?

I haven't extensively tested this for larger datasets, it's essentially a quick fix to get performance back for now. I doubt it will be efficient as the data size grows.

Other than the Clustered Indexes on the primary keys, there are no indexes on the tables. The Query Execution plan didn't appear to show any major bottlenecks, but I'm not an expert on interpreting it.

WITH tableOfAllContractIdsThatMatchRequiredStatus(contractId) 
AS (
    SELECT DISTINCT c.id
    FROM contract c 
    INNER JOIN site s ON s.ContractId = c.id
    INNER JOIN SiteSupply ss ON ss.SiteId = s.id AND ss.status != 'Draft'
    WHERE 
        ISNULL(s.Deleted, '0') = 0 
        AND ss.status in ('saved')
)
,tableOfAllStatusesForAContract(contractId, status) 
AS (
    SELECT DISTINCT c.id, ss.status
    FROM contract c 
    INNER JOIN site s ON s.ContractId = c.id
    INNER JOIN SiteSupply ss ON ss.SiteId = s.id AND ss.status != 'Draft'
    WHERE ss.SupplyType IN ('Electricity') AND ISNULL(s.Deleted, '0') = 0 
)

SELECT 
     [Contract].[Id]
    ,[Contract].[IsMultiSite]
    ,statuses.StatusesAsCsv
    ... lots more columns
    ,[WaterSupply].[Status] AS ws

--INTO #temp

FROM 
(
    SELECT 
        tableOfAllStatusesForAContract.contractId, 
        string_agg(status, ', ') AS StatusesAsCsv  
    FROM 
        tableOfAllStatusesForAContract
    GROUP BY 
        tableOfAllStatusesForAContract.contractId
) statuses

JOIN contract ON Contract.id = statuses.contractId
JOIN tableOfAllContractIdsThatMatchRequiredStatus ON tableOfAllContractIdsThatMatchRequiredStatus.contractId = Contract.id
JOIN Site ON contract.Id = site.contractId and site.isprimarySite = 1 AND ISNULL(Site.Deleted,0) = 0
... several more joins
JOIN [User] ON [Contract].ownerUserId = [User].Id

WHERE isnull(Deleted, 0) = 0 
AND
 (
 [Contract].[Id] = '12659' 
 OR [Site].[Id] = '12659'
 ... often more search term type predicates here
  )

--select * from #temp
order by 1
OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY
--drop table #temp
TheLogicMan
  • 371
  • 4
  • 12
  • This is not standard SQL. Please, per [tag:sql] tag, tag with a specific DB tag: "If your question relates solely to a specific DBMS (uses specific extensions/features), use that DBMS's tag instead. Answers to questions tagged with SQL should use ISO/IEC standard SQL." – Amadan Dec 04 '18 at 11:03
  • @TheLogicMan . . . The `ORDER BY` would be causing the performance problem. I doubt the data is large enough that the sorting would take close to 30 seconds. Instead, you'll have to look at the execution plans. – Gordon Linoff Dec 04 '18 at 11:55
  • 1
    @GordonLinoff I tested this theory, it's not the ORDER - if I include only the ORDER it's 1 sec, it's adding the OFFSET FETCH that incurs the addiitional 28 seconds. – TheLogicMan Dec 05 '18 at 13:47
  • Does `SELECT TOP(10) * FROM (... ) _` give the same perf hit? I'd expect the optimizer to consider this as equivalent to `OFFSET 0`, but you never know. Note that if you really need `OFFSET`s other than 0, you can expect a perf hit regardless, because it's simply not an efficient operation -- it's pretty much linear in the number of rows involved. Without any `TOP` or `FETCH`, the query will boil down to table scans, which can well produce a faster execution plan than one that tries to "optimize" for X rows (and fails). Your temp table has no indexes, so the optimizer can't get clever. – Jeroen Mostert Dec 05 '18 at 13:53
  • Note that "just removing the order" will produce a completely different result. How fast you can fetch from that, using temp tables or otherwise, isn't going to be very relevant to the user if the rows aren't correct (at least, I'm assuming the user won't be content with 10 arbitrary rows; if they are it's a great solution...) – Jeroen Mostert Dec 05 '18 at 13:54
  • "There are no indexes on the tables" may well be a pretty significant problem. With or without indexes the optimizer will use statistics on the columns being joined, but without indexes the number of effective join strategies are limited. This is a bad spot for the optimizer to be in: it's likely going to produce a plan that's not good because 1) it doesn't accurately estimate the rows that will result from the joins and 2) the `FETCH` limit will make it overly optimistic about the success of the strategy it does pick. Do consider indexing... – Jeroen Mostert Dec 05 '18 at 14:02
  • ```SELECT TOP (10) ...``` has the same performance hit as the offset. BTW I tested removing the ORDER in response to @GordonLinoff suggestion that it might be a factor. I do of course need the results ordering so this wouldn't be a vialble solution it was just experimentation. – TheLogicMan Dec 05 '18 at 15:32
  • "the FETCH limit will make it overly optimistic about the success of the strategy it does pick." - So does this explain why the query without TOP/FETCH returns so quick vs with? The presence of this restriction causes it to evaluate the query with a totally different strategy as it's expecting there to be at least 10, maybe more when in actual fact there is only a single result and as a result it takes much longer – TheLogicMan Dec 05 '18 at 15:48
  • The lack of indexes was an oversight, I believed the indexes would automatically be created for FK relationships - but I found today that they don't. I added indexes to all the FK cols on the tables being joined, this improved the query by 50% so it's now 15sec. It's still 14sec slower than my temp table hack however which seems pretty fishy to me! – TheLogicMan Dec 05 '18 at 16:05
  • Does your query include an "exists" by any chance? – Wili Whitelaw Sep 01 '20 at 14:56

1 Answers1

0

I've not had an answer, so I'm going to try and explain it myself, with my admittedly poor understanding of how SQL works and some pointers from Jeroen in comments above. It's probably not right, but from what I've discovered it could be correct, and I do know how to fix my immediate problem so it could help others.

I'll explain it with an analogy, as this is what I believe is probably happening:

Imagine you're a chef in a restaurant, and you have to prepare a large number of meals (rows in results). You know there's going to be a lot as you're front of house has told you this (TOP 10 or FETCH 10).

You spend time setting out the multitude of ingredients required (table joins) and equipment you'll need and as the first order comes in, you make sure you're going to be really efficient. Chopping up more that you need for the first order, putting it in little bowls ready to use on the subsequent orders. The first order takes you quite a while (30 secs) as you're planning ahead and want the subsequent dishes to go out as fast as possible.

However, as you're sat in the kitchen waiting for the next orders.. then don't arrive. That's it, just one order. Well that was a waste of time! If you'd just tried to get one dish out, you could have done it much faster (1sec) but you were planning ahead for something that was never needed.

The next night, you ditch your previous strategy and just do each plate at a time. However this time, there are 100s of customers. You can't deliver them fast enough doing them one at a time. The amount of time to deliver all the orders would have been much faster if you'd planned ahead like the previous night. (I've not tested this hypothesis, but I expect it is what would probably happen).

For my query, I don't know if there's going to be 1 result or 100s although I may be able to do some analysis up front based on the search criteria entered by the user, I may have to adapt my UI to give me more information so I can predict this better, which means I can pick the appropriate strategy for SQL to use upfront. As it is, I'm optimised for a small number of results which works fine for now - but I need to do some more extensive testing to see how performance is affected as the dataset grows.

"If you want a answer to something, post something that's wrong on the internet and someone will be sure to correct you"

TheLogicMan
  • 371
  • 4
  • 12