【问题标题】:Pagination in SQL - Performance issueSQL 中的分页 - 性能问题
【发布时间】:2013-10-07 18:02:01
【问题描述】:

我正在尝试使用分页,我在 SO 中找到了完美的链接

https://stackoverflow.com/a/109290/1481690

SELECT  *
FROM    ( SELECT    ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum, *
          FROM      Orders
          WHERE     OrderDate >= '1980-01-01'
        ) AS RowConstrainedResult
WHERE   RowNum >= 1
    AND RowNum < 20
ORDER BY RowNum

我正在尝试将完全相同的查询与我的内部查询中的几个表的附加连接一起使用。

在以下场景中很少遇到性能问题

WHERE   RowNum >= 1
    AND RowNum < 20  ==>executes faster approx 2 sec


    WHERE   RowNum >= 1000
    AND RowNum < 1010      ==>  more time  approx 10 sec

    WHERE   RowNum >= 30000
    AND RowNum < 30010    ==> more time approx 17 sec

每次我选择 10 行但时间差很大。有什么想法或建议吗?

我选择这种方法作为动态绑定列并形成查询。有没有其他更好的方法可以在 SQl Server 2008 中组织分页查询。

有什么方法可以提高查询的性能吗?

谢谢

【问题讨论】:

  • 显示您正在运行的完整查询并提供CREATE TABLE,包括所涉及表的索引。
  • @Peru,orders 表的结构是什么?

标签: sql performance sql-server-2008 pagination


【解决方案1】:

您的查询的主要缺点是它对整个表进行排序并为每个查询计算 Row_Number。您可以通过在排序阶段使用更少的列(例如 Anup Shah 的建议)来简化 SQL Server 的工作。但是,您仍然可以读取、排序和计算每个查询的行号。

动态计算的替代方法是读取之前计算的值。

根据数据集的波动性以及排序和过滤的列数,您可以考虑:

  1. 添加一个 rownumber 列(或 2-3 列)并将其作为第一列包含在聚集索引中或创建非聚集索引)。

  2. 为最常见的组合创建视图,然后为这些视图编制索引。它被称为索引(物化)视图。

这将允许读取行号,性能几乎不依赖于卷。虽然维护这些会,但比为每个查询排序整个表要少。

注意,这是一次性查询,与所有其他查询相比运行频率较低,最好只坚持查询优化:创建额外列/视图的努力可能不会得到回报。

【讨论】:

    【解决方案2】:

    令人难以置信的是,没有其他答案提到在所有 SQL Server 版本中进行分页的最快方法,特别是关于 OP 的问题,即对于大页码,偏移量可能非常慢,就像 @987654321 @。

    有一种完全不同的、更快的方式在 SQL 中执行分页。这通常被称为this blog post here 中描述的“seek 方法”。

    SELECT TOP 10 *
    FROM Orders
    WHERE OrderDate >= '1980-01-01'
    AND ((OrderDate > @previousOrderDate)
      OR (OrderDate = @previousOrderDate AND OrderId > @previousOrderId))
    ORDER BY OrderDate ASC, OrderId ASC
    

    @previousOrderDate@previousOrderId 值分别是上一页最后一条记录的值。这允许您获取“下一页”。如果ORDER BY 方向为DESC,则只需使用&lt; 即可。

    使用上述方法,您不能在没有先获取之前的 40 条记录的情况下立即跳转到第 4 页。但通常,无论如何你都不想跳那么远。相反,您将获得更快的查询,该查询可能能够在恒定时间内获取数据,具体取决于您的索引。此外,无论基础数据是否发生变化(例如,在第 1 页上,当您在第 4 页上时),您的网页都会保持“稳定”。

    这是在 Web 应用程序中延迟加载更多数据时实现分页的最佳方式。

    注意,“seek 方法”也称为keyset paging

    【讨论】:

      【解决方案3】:

      ROW_NUMBER 是一种糟糕的分页方式,因为操作成本大幅增长。

      您应该使用双 ORDER BY 子句。

      假设您想使用ROW_NUMBER between 1200 and 1210 获取记录。而不是使用ROW_NUMBER() OVER (...) 并稍后将结果绑定到WHERE 您应该:

      SELECT TOP(11) *
      FROM (
          SELECT TOP(1210) *
          FROM [...]
          ORDER BY something ASC
      ) subQuery
      ORDER BY something DESC.
      

      请注意,此查询将以相反的顺序给出结果。这不应该 - 一般而言 - 是一个问题,因为它很容易在 UI 中反转集合,即 C#,特别是因为结果集合应该相对较小。

      后者通常要快得多。请注意,后一种解决方案将通过在用于对查询进行排序的列上进行聚类 (CREATE CLUSTERED INDEX ...) 大大改进。

      希望对您有所帮助。

      【讨论】:

        【解决方案4】:

        @peru,关于是否有更好的方法以及基于@a1ex07提供的解释,请尝试以下 -

        如果表有一个唯一标识符,例如可以执行比较(大于、小于)操作的数字(订单 ID)或(订单日期,订单索引),则将其用作偏移量而不是行号。

        例如,如果订单表将“order_id”作为主键,那么 -
        获得前十个结果 -
        1.

        select RowNum, order_id from   
        ( select 
        ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum, 
        o.order_id 
        from orders o where  o.order_id > 0 ;  
        ) 
        tmp_qry where RowNum between 1 and 10 order by RowNum; // first 10 
        

        假设最后返回的order-id是17,那么

        要选择接下来的 10 个,
        2.

        select RowNum, order_id from   
        ( select 
        ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum, 
        o.order_id 
        from orders o where  o.order_id > 17 ;  
        ) 
        tmp_qry where RowNum between 1 and 10 order by RowNum; // next 10 
        

        请注意,row-num 的值没有改变。其比较的 order-id 值已更改。

        如果不存在这样的密钥,请考虑添加一个!

        【讨论】:

          【解决方案5】:
          declare @pageOffset int
          declare @pageSize int
          -- set variables at some point
          
          declare @startRow int
          set @startRow = @pageOffset * @pageSize
          
          declare @endRow int
          set @endRow + @pageSize - 1
          
          SELECT 
              o.*
          FROM
          (
              SELECT 
                  ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum
                  , OrderId
              FROM      
                  Orders
              WHERE     
                  OrderDate >= '1980-01-01'
          ) q1
          INNER JOIN Orders o
              on q1.OrderId = o.OrderId
          where
              q1.RowNum between @startRow and @endRow
          order by
              o.OrderDate
          

          【讨论】:

            【解决方案6】:

            我总是检查我在查询中访问了多少数据,并尝试消除不必要的列和行。 好吧,这些只是您可能已经检查过的明显点,但只是想指出以防万一。 在您的查询中,性能缓慢可能是因为您执行“Select *”。从表中选择所有列不允许有好的执行计划。 检查您是否只需要选定的列,并确保表 Orders 上有正确的覆盖索引。

            由于 SQL 2008 版本中没有显式的 SKIPP 或 OFFSET 函数,我们需要创建一个,我们可以通过 INNER JOIN 创建。 在一个查询中,我们将首先使用 OrderDate 生成 ID,该查询中将没有其他内容。 我们在第二个查询中执行相同的操作,但如果您需要 ALL 列,我们还会从表 ORDER 或 ALL 中选择一些其他感兴趣的列。 然后我们加入它以按 ID 和 OrderDate 查询结果,并为第一个查询添加 SKIPP 行过滤器,其中数据集处于所需的最小大小。 试试这个代码。

                SELECT q2.*
                FROM
                (
                    SELECT ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum, OrderDate
                    FROM      Orders
                    WHERE     OrderDate >= '1980-01-01'
                )q1
                INNER JOIN 
                (
                    SELECT ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum, *
                    FROM      Orders
                    WHERE     OrderDate >= '1980-01-01'
                )q2
                    ON q1.RowNum=q2.RowNum AND q1.OrderDate=q2.OrderDate AND q1.rownum BETWEEN 30000 AND 30020
            

            为了给你估计,我用以下测试数据尝试了这个,无论你查询哪个窗口,结果都在 2 以内 秒,注意表是HEAP(无索引)表总共有2M 行。测试选择正在查询从 50,000 到 50,010 的 10 行

            下面的插入花了大约 8 分钟。

                IF object_id('TestSelect','u') IS NOT NULL
                    DROP TABLE TestSelect
                GO
                CREATE TABLE TestSelect
                (
                    OrderDate   DATETIME2(2)
                )
                GO
            
                DECLARE @i bigint=1, @dt DATETIME2(2)='01/01/1700'
                WHILE @I<=2000000
                BEGIN
            
                    IF @i%15 = 0
                        SELECT @DT = DATEADD(DAY,1,@dt)
            
                    INSERT INTO dbo.TestSelect( OrderDate )
                    SELECT @dt
            
                    SELECT @i=@i+1
                END
            

            选择窗口 50,000 到 50,010 用时不到 3 秒。

            选择最后一行 2,000,000 到 2,000,000 也需要 3 秒。

                SELECT q2.*
                FROM
                (
                    SELECT  ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum 
                            ,OrderDate
                    FROM TestSelect
                    WHERE OrderDate >= '1700-01-01'
                )q1
                INNER JOIN
                (
                    SELECT  ROW_NUMBER() OVER ( ORDER BY OrderDate ) AS RowNum 
                            ,*
                    FROM TestSelect
                    WHERE OrderDate >= '1700-01-01'
                )q2
                    ON q1.RowNum=q2.RowNum 
                    AND q1.OrderDate=q2.OrderDate 
                    AND q1.RowNum BETWEEN 50000 AND 50010
            

            【讨论】:

            • 感谢@Anup Shah 的详细解答。会检查一下+1
            • 嘿,我测试了这个解决方案,并根据我下面的建议将它与相应的查询进行了比较。在具有 100k 条记录的示例表上,它显示您的查询占用了总时间的 59%,而我的查询只占用了 41%。当我将测试样本增加到 1.6M 时,对于 50000 - 50010 的记录,它提高到 69% 和 31%,但是当我将范围移动到 250000 - 25010 时,优势下降到 60% 和 40%。最后,对于记录 750000 - 750010(相当位于堆的中间),差异是 56% 和 44%。进一步的测试可能会很有趣,但最重要的是,我的解决方案要简单得多。
            • @Peru:这里有一个很好的基准来确认您的不良偏移性能:4guysfromrolla.com/webtech/042606-1.shtml。我猜你们可能对seek method 感兴趣,它允许在恒定时间内分页
            【解决方案7】:

            即使您始终选择相同数量的行,当您想要在数据窗口末尾选择行时,性能也会下降。为了获取前 10 行,引擎只获取 10 行;要获得下一个 10,它必须获取 20,丢弃前 10,然后返回 10。要获得 30000 - 30010,它必须读取所有 30010,跳过前 30k,然后返回 10。

            一些提高性能的技巧(不是完整列表,完全跳过了构建 OLAP)。 您提到了联接;如果这可能加入不是内部查询,而是它的结果。您还可以尝试向ORDER BY OrderDate - ASCDESC 添加一些逻辑,具体取决于您要检索的存储桶。假设你想抓住“最后一个”10,ORDER BY ... DESC 会工作得更快。不用说,它必须是索引orderDate

            【讨论】:

            • 感谢@a1ex07。还有其他更好的方法可以在 sql server 2008 中编写分页查询吗?
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-09-28
            • 2017-03-24
            • 2017-11-15
            • 2019-04-13
            相关资源
            最近更新 更多