OFFSET 和 FETCH 会对查询造成巨大的性能影响 - 包括当 OFFSET = 0 时

The*_*Man 5 sql-server paging performance fetch

我有一个相当复杂的 SQL 查询,涉及从大量联接返回大约 20 列,用于填充 UI 中的结果网格。它还使用几个 CTE 来预过滤结果。我已经包含了下面查询的近似值(我已经注释掉了修复性能的行)

随着数据库中数据量的增加,查询性能大幅下降,主表“Contract”中只有大约 2500 行。

通过实验,我发现仅通过删除最后的顺序和偏移获取,性能就从大约 30 秒缩短到仅 1 秒!

order by 1 OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY
Run Code Online (Sandbox Code Playgroud)

这对我来说毫无意义。最后一行应该非常便宜,即使 OFFSET 为零也是免费的,那么为什么它会增加我的查询时间 29 秒呢?

为了保持 SQL 的相同功能,我对其进行了调整,以便首先选择 #TEMP,然后对临时表执行上述 order-offset-fetch,然后删除临时表。这将在大约 2-3 秒内完成。

我的“优化”感觉很错误,肯定有更明智的方法来达到相同的速度吗?

我还没有针对更大的数据集对此进行广泛测试,这本质上是目前恢复性能的快速修复。我怀疑随着数据大小的增长它是否会有效。

除了主键上的聚集索引之外,表上没有任何索引。查询执行计划似乎没有显示任何主要瓶颈,但我不是解释它的专家。

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
Run Code Online (Sandbox Code Playgroud)

The*_*Man 0

我还没有答案,所以我将尝试自己解释它,承认我对 SQL 工作原理的理解很差,并且上面的评论中来自 Jeroen 的一些指示。这可能不正确,但从我发现的情况来看,它可能是正确的,而且我确实知道如何解决我眼前的问题,以便它可以帮助其他人。

我将用一个类比来解释它,因为我认为这可能会发生:

想象一下,您是一家餐厅的厨师,您必须准备大量饭菜 ( rows in results)。您知道将会有很多事情,因为您的前台已告诉您这一点 ( TOP 10 or FETCH 10)。

您花时间列出所需的多种原料 ( table joins) 和您需要的设备,当第一个订单收到时,您确保自己的效率非常高。将第一个订单所需的部分切碎,放入小碗中,以备后续订单使用。第一个订单需要花费相当长的时间 ( 30 secs),因为您正在提前计划并希望后续菜肴尽快上桌。

然而,当你坐在厨房里等待下一个订单时……那就不要来了。就这样,只需一个命令。好吧,那是浪费时间!如果你只是想把一道菜拿出来,你本来可以做得更快(1sec),但你正在提前计划一些从来不需要的东西。

第二天晚上,你放弃了之前的策略,只一次做每个盘子。然而这次,有数百名顾客。你无法以足够快的速度一次交付一个。如果您像前一天晚上那样提前计划,那么交付所有订单的时间会快得多。(我还没有测试这个假设,但我预计这可能会发生)。

对于我的查询,我不知道是否会有 1 个结果或 100 个结果,尽管我可以根据用户输入的搜索条件预先进行一些分析,但我可能必须调整我的 UI 以提供更多结果信息,以便我可以更好地预测这一点,这意味着我可以为 SQL 选择合适的策略来预先使用。事实上,我针对少量结果进行了优化,目前效果很好 - 但我需要进行一些更广泛的测试,以了解随着数据集的增长性能如何受到影响。

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