我需要查询分页数据。要求之一是不仅要显示给定页面上的项目,还要显示用户可以查看的项目总数(支持页面导航,以及向用户发出通知,例如“显示 159 个项目中的第 50-75 个项目”) ”)。我在我正在处理的代码库中看到了一种我不太喜欢的模式;它基本上相当于将所有数据插入到临时表中,然后选择页面的特定数据,然后选择计数。看起来像这样。
CREATE PROCEDURE [dbo].[GetAllFoos] ( --Some parameters to filter the foos
@PageIndex INT = 1,
@PageSize INT = 25,
@TotalCount INT OUTPUT )
AS
BEGIN
SELECT --Some large number of fields
INTO #Foos
FROM [dbo].Foo
WHERE --Some condition is true
SELECT @TotalCount = Count(1)
FROM #Foos;
SELECT *
FROM #Foos
ORDER BY FooID
OFFSET ((@PageIndex - 1) * @PageSize) ROWS
FETCH NEXT @PageSize ROWS ONLY
END
上面查询了整个 foo 集合,并将其中的所有数据获取到临时表中。这可能是数百或数千条记录,然后(默认情况下)它会丢弃除其中 25 条之外的所有记录。这是计算的巨大浪费。就我而言,我通过查看 SSMS 中的客户端统计信息找到了一种稍微更好的方法
CREATE PROCEDURE [dbo].[GetAllFoos] ( --Some other parameters to filter the foos
@PageIndex INT = 1,
@PageSize INT = 25,
@TotalCount INT OUTPUT )
AS
BEGIN
SELECT --Some large number of fields
FROM [dbo].Foo
WHERE --Some condition is true
ORDER BY FooID
OFFSET ((@PageIndex - 1) * @PageSize) ROWS
FETCH NEXT @PageSize ROWS ONLY
SELECT @TotalCount = Count(1)
FROM [dbo].Foo
WHERE --Some condition is true
END
修改后的查询的第一部分,仅选择我需要的所有数据行本身花费的时间较少,但我遇到了一个问题,添加辅助查询会立即重新添加该时间。不过,我能够缓解这个问题通过向
[dbo].Foo
添加一个围绕我想要查询的条件定制的索引,它的执行速度大约提高了 4 倍(同样,根据 ssms 中的客户端统计数据)。
也就是说,我觉得所有这些都相当笨重——我知道 sql 的级别非常低,但是有没有更好的方法来实现这个目标?我可以进行 where 子句过滤,获取计数,然后在下一步中在单个操作中执行
offset ... rows / fetch next ... rows
子句?无论我尝试什么,查询计划似乎总是显示两个查询路径,有时针对不同的索引,但总是两个单独的读取操作。下面是运行第二个查询的示例(真实的表/索引名称已模糊)。
如果不实际读取所有行,就无法“获取总数”。
你能得到的最好的结果就是使用窗口函数:
SELECT
f.*
FROM (
SELECT *,
TotalCount = Count(*),
RowNumber = ROW_NUMBER() OVER (ORDER BY FooID)
FROM [dbo].Foo
WHERE --Some condition is true
) f
WHERE f.RowNumber >= (PageIndex - 1) * PageSize + 1
AND f.RowNumber < PageIndex * PageSize;
根据选择,我可能会放弃获取总数。在绝大多数情况下实际上没有必要。
说实话,你最好使用 Keyset Pagination,它远更高效。您只需跳转到一个 ID(或从头开始)并从那里开始读取。实际上不会读取未返回的行。 请参阅在 SQL Server 中是否有更好的选项来应用分页而不应用 OFFSET?
但这需要对整个堆栈一直到 UI 进行彻底的重新思考。您不会获得总行数,也不会按行号进行分页。因此,您无法向用户显示页数,甚至无法向用户显示它们所在的页码。它就像一个无限滚动的页面,只有上一个和下一个按钮。