带有分页和计数的 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 查询的主要内容,如果未能解决你的问题,请参考以下文章