【问题标题】:SQL Server query with pagination and count带有分页和计数的 SQL Server 查询
【发布时间】:2014-02-27 12:55:14
【问题描述】:

我想使用分页进行数据库查询。因此,我使用了一个公用表表达式和一个排名函数来实现这一点。看下面的例子。

declare @table table (name varchar(30));
insert into @table values ('Jeanna Hackman');
insert into @table values ('Han Fackler');
insert into @table values ('Tiera Wetherbee');
insert into @table values ('Hilario Mccray');
insert into @table values ('Mariela Edinger');
insert into @table values ('Darla Tremble');
insert into @table values ('Mammie Cicero');
insert into @table values ('Raisa Harbour');
insert into @table values ('Nicholas Blass');
insert into @table values ('Heather Hayashi');

declare @pagenumber int = 2;
declare @pagesize int = 3;
declare @total int;

with query as
(
    select name, ROW_NUMBER() OVER(ORDER BY name ASC) as line from @table
)
select top (@pagesize) name from query
    where line > (@pagenumber - 1) * @pagesize

在这里,我可以指定@pagesize 和@pagenumber 变量来提供我想要的记录。但是,此示例(来自存储过程)用于在 Web 应用程序中进行网格分页。此 Web 应用程序需要显示页码。例如,如果数据库中有 12 条记录并且页面大小为 3,那么我将不得不显示 4 个链接,每个链接代表一个页面。

但我不能在不知道有多少记录的情况下执行此操作,而这个示例只是给了我记录的子集。

然后我将存储过程更改为返回计数(*)。

declare @pagenumber int = 2;
declare @pagesize int = 3;
declare @total int;
with query as
(
    select name, ROW_NUMBER() OVER(ORDER BY name ASC) as line, total = count(*) over()from @table
)
select top (@pagesize) name, total from query
    where line > (@pagenumber - 1) * @pagesize

因此,与每一行一起,它将显示记录总数。但我不喜欢它。

我的问题是是否有更好的方法(性能)来做到这一点,也许设置 @total 变量而不在 SELECT 中返回此信息。还是这个总列不会对性能造成太大影响?

谢谢

【问题讨论】:

    标签: sql-server pagination common-table-expression


    【解决方案1】:

    假设您使用的是 MSSQL 2012,您可以使用Offset and Fetch,它可以极大地清理服务器端分页。我们发现性能很好,而且在大多数情况下更好。至于获得总列数,只需使用内联下面的窗口函数......它不会包括“偏移”和“获取”施加的限制。

    对于 Row_Number,您可以像以前一样使用窗口函数,但我建议您将客户端计算为 (pagenumber*pagesize + resultsetRowNumber),因此如果您位于 10 个结果的第 5 页,并且位于第三行你会输出第 53 行。

    当应用于包含大约 200 万个订单的 Orders 表时,我发现以下内容:

    快速版本

    不到一秒就跑完了。它的好处是您可以在公用表表达式中进行一次过滤,它适用于分页过程和计数。当 where 子句中有许多谓词时,这会使事情变得简单。

    declare @skipRows int = 25,
            @takeRows int = 100,
            @count int = 0
    
    ;WITH Orders_cte AS (
        SELECT OrderID
        FROM dbo.Orders
    )
    
    SELECT 
        OrderID,
        tCountOrders.CountOrders AS TotalRows
    FROM Orders_cte
        CROSS JOIN (SELECT Count(*) AS CountOrders FROM Orders_cte) AS tCountOrders
    ORDER BY OrderID
    OFFSET @skipRows ROWS
    FETCH NEXT @takeRows ROWS ONLY;
    

    慢速版

    这大约需要 10 秒,是 Count(*) 导致缓慢。我很惊讶这这么慢,但我怀疑它只是在计算每一行的总数。虽然很干净。

    declare @skipRows int = 25,
    @takeRows int = 100,
    @count int = 0
    
    
    SELECT 
        OrderID,
        Count(*) Over() AS TotalRows
    FROM Location.Orders
    ORDER BY OrderID
    OFFSET @skipRows ROWS
    FETCH NEXT @takeRows ROWS ONLY;
    

    结论

    我们之前已经完成了这个性能调整过程,实际上发现它取决于查询、使用的谓词和所涉及的索引。例如,第二次我们引入了一个视图,因此我们实际上查询了基表,然后连接了视图(包括基表),它实际上执行得非常好。

    我建议制定一些简单明了的策略,并将它们应用到高价值的查询中。

    【讨论】:

    • 谢谢,我将使用 OFFSET 和 FETCH 运算符改进我的分页查询。但是,我关心的是结果集中 count(*) 列的返回(如果它对性能有很大的影响)。
    • CROSS JOIN 确实比 Count(*) Over() 快!
    • 哎哟..我是个愚蠢的人..我必须将WHERE clausule 放在WITH 语句中的第一个SELECT 中......现在它正在工作!谢谢你,你让我很开心
    • @FabioGouw,FWIW,在 SQL Server 2014 中,我们看到使用交叉连接的 CTE 实际上比使用 OVER() 慢。在处理 80k 记录集时,CTE 的运行速度会慢约 25%。
    • (更正 -- SQL Server 2012,而不是 2014。)就 OP 而言,我认为 YMMV 取决于多种因素。
    【解决方案2】:
    DECLARE @pageNumber INT = 1  , 
            @RowsPerPage INT = 20
    
    SELECT  *
    FROM    TableName
    ORDER BY Id
            OFFSET ( ( @pageNumber - 1 ) * @RowsPerPage ) ROWS
                 FETCH NEXT @RowsPerPage ROWS ONLY;
    

    【讨论】:

    • 计数在哪里?
    【解决方案3】:

    如果你事先计算计数呢?

    declare @pagenumber int = 2;
    declare @pagesize int = 3;
    declare @total int;
    
    SELECT @total = count(*)
    FROM @table
    
    with query as
    (
       select name, ROW_NUMBER() OVER(ORDER BY name ASC) as line from @table
    )
    select top (@pagesize) name, @total total from query
    where line > (@pagenumber - 1) * @pagesize
    

    另一种方法是计算max(line)。检查链接

    Return total records from SQL Server when using ROW_NUMBER

    UPD:

    对于单个查询,请在上面的链接中查看 marc_s 的答案。

        with query as
        (
           select name, ROW_NUMBER() OVER(ORDER BY name ASC) as line from @table
        )
        select top (@pagesize) name, 
           (SELECT MAX(line) FROM query) AS total 
        from query
        where line > (@pagenumber - 1) * @pagesize
    

    【讨论】:

    • 谢谢,但在这里我必须查询数据库两次:第一次用于计数,第二次用于实际分页结果集。对于我的示例,它可以工作,但是真正的 sp 有一个复杂的查询,带有连接和可选参数,我不想重写它并执行两次。
    • 在另一个问题中,有一条评论指出“用 Count(*) 替换 Max(RowNum) 字段”,这导致了我开始这个问题的同样问题。但是@BlackjacketMack 将这种方法与交叉连接进行了比较,最后一种性能更好。谢谢
    【解决方案4】:
    @pagenumber=5
    @pagesize=5
    

    创建一个公用表表达式,这样写逻辑

    Between ((@pagenumber-1)*(@pagesize))+1 and (@pagenumber *@pagesize)
    

    【讨论】:

      【解决方案5】:

      我们可以通过多种方式实现分页:希望这些信息对您和其他人有用。

      示例 1:使用 offset-fetch 下一个子句。 2005年推出

      declare @table table (name varchar(30));
      insert into @table values ('Jeanna Hackman');
      insert into @table values ('Han Fackler');
      insert into @table values ('Tiera Wetherbee');
      insert into @table values ('Hilario Mccray');
      insert into @table values ('Mariela Edinger');
      insert into @table values ('Darla Tremble');
      insert into @table values ('Mammie Cicero');
      insert into @table values ('Raisa Harbour');
      insert into @table values ('Nicholas Blass');
      insert into @table values ('Heather Hayashi');
      
      declare @pagenumber int = 1
      declare @pagesize int = 3
      
      --this is a CTE( common table expression and this is introduce in 2005)
      with query as
      (
        select ROW_NUMBER() OVER(ORDER BY name ASC) as line, name from @table
      ) 
      
      --order by clause is required to use offset-fetch
      select * from query
      order by name 
      offset ((@pagenumber - 1) * @pagesize) rows
      fetch next @pagesize rows only
      

      示例 2:使用 row_number() 函数和之间

      declare @table table (name varchar(30));
      insert into @table values ('Jeanna Hackman');
      insert into @table values ('Han Fackler');
      insert into @table values ('Tiera Wetherbee');
      insert into @table values ('Hilario Mccray');
      insert into @table values ('Mariela Edinger');
      insert into @table values ('Darla Tremble');
      insert into @table values ('Mammie Cicero');
      insert into @table values ('Raisa Harbour');
      insert into @table values ('Nicholas Blass');
      insert into @table values ('Heather Hayashi');
      
      declare @pagenumber int = 2
      declare @pagesize int = 3
      
      SELECT *
      FROM 
      (select ROW_NUMBER() OVER (ORDER BY PRODUCTNAME) AS RowNum, * from Products)
      as Prodcut
      where RowNum between (((@pagenumber - 1) * @pageSize )+ 1) 
      and (@pagenumber * @pageSize )
      

      希望对大家有帮助

      【讨论】:

        【解决方案6】:

        我不喜欢其他过于复杂的解决方案,所以这是我的版本。

        一次执行三个选择查询,并使用输出参数获取计数值。此查询返回总计数、过滤器计数和页面行。它支持对源数据进行排序、搜索和过滤。它易于阅读和修改。

        假设您有两个具有一对多关系的表,项目及其价格随时间而变化,因此示例查询不太简单。

        create table shop.Items
        (
            Id uniqueidentifier not null primary key,
            Name nvarchar(100) not null,
        );
        
        create table shop.Prices
        (
            ItemId uniqueidentifier not null,
            Updated datetime not null,
            Price money not null,
            constraint PK_Prices primary key (ItemId, Updated),
            constraint FK_Prices_Items foreign key (ItemId) references shop.Items(Id)
        );
        

        这里是查询:

        select @TotalCount = count(*) over()
        from shop.Items i;
        
        select @FilterCount = count(*) over()
        from shop.Items i
        outer apply (select top 1 p.Price, p.Updated from shop.Prices p where p.ItemId = i.Id order by p.Updated desc) as p
        where (@Search is null or i.Name like '%' + @Search + '%')/**where**/;
        
        select i.Id as ItemId, i.Name, p.Price, p.Updated
        from shop.Items i
        outer apply (select top 1 p.Price, p.Updated from shop.Prices p where p.ItemId = i.Id order by p.Updated desc) as p
        where (@Search is null or i.Name like '%' + @Search + '%')/**where**/
        order by /**orderby**/i.Id
        offset @SkipCount rows fetch next @TakeCount rows only;
        

        您需要向查询提供以下参数:

        • @SkipCount - 要跳过的记录数,根据页码计算。
        • @TakeCount - 要返回的记录数,根据或等于页面大小计算得出。
        • @Search - 在某些列中搜索的文本,由网格搜索框提供。
        • @TotalCount - 数据源中的记录总数,输出参数。
        • @FilterCount - 搜索和过滤操作后的记录数,输出参数。

        如果网格必须支持按列对行进行排序,您可以将/**orderby**/ 注释替换为列列表及其排序方向。您从网格中获取此信息并将其转换为 SQL 表达式。我们最初仍然需要按某个列对记录进行排序,我通常使用 ID 列。

        如果网格必须支持按每列单独过滤数据,您可以用 SQL 表达式替换 /**where**/ 注释。

        如果用户没有搜索和过滤数据,而只是点击了网格页面,这个查询根本不会改变,数据库服务器会很快执行。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-03-03
          • 1970-01-01
          • 2015-02-22
          • 2021-12-28
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多