带有分页和计数的 SQL Server 查询

Posted

技术标签:

【中文标题】带有分页和计数的 SQL Server 查询【英文标题】:SQL Server query with pagination and count 【发布时间】: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 中返回此信息。还是这个总列不会对性能造成太大影响?

谢谢

【问题讨论】:

【参考方案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**/ 注释。

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

【讨论】:

以上是关于带有分页和计数的 SQL Server 查询的主要内容,如果未能解决你的问题,请参考以下文章

物理分页和逻辑分页

小5聊sql server 分页和分组-row_number()和over()rank()和over()的小区别

分页和文件上传

kendo ui grid 完成服务器端分页和设置总数

基于 mybatis 的分页和过滤查询

带有分页和分组的 Django ListView