SQL中的分页 - 性能问题

时间:2013-10-01 14:14:04

标签: sql performance sql-server-2008 pagination

我试图使用分页,我在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行但时差很大。有什么想法或建议吗?

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

有没有办法可以提高查询效果?

由于

7 个答案:

答案 0 :(得分:4)

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

因为SQL 2008版本中没有显式的SKIPP或OFFSET函数,我们需要创建一个,我们可以通过INNER JOIN创建。 在一个查询中,我们将首先使用OrderDate生成ID,并且该查询中不会包含任何其他内容。 我们在第二个查询中也这样做,但是如果你需要ALL列,我们还可以从表ORDER或ALL中选择一些其他感兴趣的列。 然后我们通过ID和OrderDate以及ADD 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   行。 test select正在查询从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

enter image description here

答案 1 :(得分:2)

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#,特别是因为结果集应该相对较小。

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

希望有所帮助。

答案 2 :(得分:1)

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

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

答案 3 :(得分:0)

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

答案 4 :(得分:0)

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

如果表具有唯一标识符,例如数字(order-id)或(order-date,order-index),可以在其上执行比较(大于,小于)操作,则将其用作偏移而不是行号。

例如,如果表格订单的'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 :(得分:0)

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

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

根据数据集的易变性和排序和过滤的列数,您可以考虑:

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

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

  3. 这将允许读取rownumber并且性能几乎不依赖于音量。虽然保留theese会,但不会为每个查询排序整个表。

    请注意,这是一次性查询并且与所有其他查询相比不常运行,最好只坚持使用查询优化:创建额外列/视图的努力可能无法获得回报。

答案 6 :(得分:0)

令人难以置信的是,没有其他答案提到最快的方式在所有SQL Server版本中进行分页,特别是关于OP的问题,其中偏移对于大页面数量来说可能非常慢{{{ 3}}。

在SQL中执行分页有一种完全不同的,快得多的方法。这通常被称为“寻找方法”,如benchmarked here中所述。

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,则只需使用<

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

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

注意,“搜索方法”也称为this blog post here