【问题标题】:Optimize SQL query with pagination使用分页优化 SQL 查询
【发布时间】:2021-06-03 13:59:13
【问题描述】:

我有一个针对 SQL Server 数据库运行的查询需要 10 多秒才能执行。被查询的表有超过 1400 万行。

我想按日期顺序按给定的ServiceUserId 显示Notes 表中的Text 列。可能有数千个条目,所以我想将返回的值限制在可管理的水平。

SELECT Text
FROM   
    (SELECT    
         ROW_NUMBER() OVER (ORDER BY [DateDone]) AS RowNum, Text
     FROM      
         Notes
     WHERE     
         ServiceUserId = '6D33B91A-1C1D-4C99-998A-4A6B0CC0A6C2') AS RowConstrainedResult
WHERE   
    RowNum >= 40 AND RowNum < 60
ORDER BY 
    RowNum

以下是上述查询的执行计划。

  • 非聚集索引 - ServiceUserIdDateDone 列上的非聚集索引按升序排列。
  • 键查找 - 表的主键 NoteId

如果我第二次运行相同的查询但使用不同的行号,那么我会在毫秒内得到响应,我假设来自缓存的执行计划。为不同的ServiceUserId 运行的查询将需要大约 10 秒。

关于如何加快此查询的任何建议?

【问题讨论】:

  • 像这样的后端分页没有太大意义,除非DateDone 是一个唯一值。好像不是这样,所以后端分页会返回不一致的结果。请包括一个独特的订购标准,可能会添加 PK。

标签: sql sql-server pagination query-optimization keyset-pagination


【解决方案1】:

我建议使用 order by offset fetch : 从第x行开始,取z下一行,可以参数化

SELECT    
   Text
FROM      
   Notes
WHERE     
   ServiceUserId = '6D33B91A-1C1D-4C99-998A-4A6B0CC0A6C2'
Order by DateDone
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY

还要确保“DateDone”有正确的索引,如果你还没有的话,可以把它包含在你已经在“Notes”上的索引中

您可能需要在索引中包含文本列:

create index IX_DateDone on Notes(DateDone) INCLUDE (TEXT,ServiceUserId)

但是请注意,向索引添加如此巨大的列会影响您的插入/更新效率,当然它需要磁盘空间

【讨论】:

  • 这是我最初使用的查询格式。它的执行时间与我的问题中的查询相似,约为 10 秒。 “DateDone”上已经有一个非聚集索引,您建议将它与哪一列结合?
  • 查询计划应该看起来不同且简化了,将带有此查询的查询计划粘贴到brentozar.com/pastetheplan,看看我们能做什么
  • 但要回答您的问题,请包括 ServiceUserId 和 DoneDate 索引上的文本
  • 感谢您的链接。以下是请求的查询计划:brentozar.com/pastetheplan/?id=S1hgi00zd
  • @Tomsmith 我已将我的查询更新为由 DateDone 订购尝试更新的查询
【解决方案2】:

You should look into Keyset Pagination.

它比行集分页的性能要好得多。

它与它根本不同,它不是引用特定的行号块,而是引用起始点来查找索引键。

它更快的原因是您不关心特定键之前有多少行,您只需寻找一个键并向前(或向后)移动。

假设您按单个ServiceUserId 过滤,按DateDone 排序。您需要一个如下索引(如果它太大,您可以忽略 INCLUDE,它不会对数学产生太大影响):

create index IX_DateDone on Notes (ServiceUserId, DateDone) INCLUDE (TEXT);

现在,当您选择一些行时,不要给出开始和结束行号,而是给出开始键

SELECT TOP (20)
    Text,
    DateDone
FROM
    Notes
WHERE     
    ServiceUserId = '6D33B91A-1C1D-4C99-998A-4A6B0CC0A6C2'
    AND DateDone > @startingDate
ORDER BY 
    DateDone;

在下一次运行中,您传递您收到的最后一个 DateDone 值。这会让你获得下一批。

一个小缺点是您无法跳转页面。但是,用户想要跳转到第 327 页的情况比某些人想象的要少得多(从 UI 角度来看)。所以没关系。


键必须是唯一的。如果它不是唯一的,您就不能准确地寻找下一行。如果您需要使用额外的列来保证唯一性,它会变得有点复杂:

WITH NotesFiltered AS
(
    SELECT * FROM Notes
    WHERE     
        ServiceUserId = '6D33B91A-1C1D-4C99-998A-4A6B0CC0A6C2'
)
SELECT TOP (20)
    Text,
    DateDone
FROM (
    SELECT
        Text,
        DateDone,
        0 AS ordering
    FROM NotesFiltered 
    WHERE
        DateDone = @startingDate AND NoteId > @startingNoteId
    UNION ALL
    SELECT
        Text,
        DateDone,
        1 AS ordering
    FROM NotesFiltered 
    WHERE
        DateDone > @startingDate
) n
ORDER BY 
    ordering, DateDone, NoteId;

旁注

在支持行值比较的 RDBMS 中,多列示例可以通过以下方式简化为原始代码:

WHERE (DateDone, NoteId) > (@startingDate, @startingNoteId)

很遗憾,SQL Server 目前不支持此功能。
请投票给Azure Feedback request for this

【讨论】:

    猜你喜欢
    • 2021-06-08
    • 2023-02-15
    • 2011-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-17
    • 1970-01-01
    相关资源
    最近更新 更多