有没有更好的选择来应用分页而不在 SQL 服务器中应用 OFFSET?

Is there any better option to apply pagination without applying OFFSET in SQL Server?

我想在具有大量数据的 table 上应用分页。我只想知道一个比在 SQL 服务器中使用 OFFSET 更好的选择。

这是我的简单查询:

SELECT *
FROM TableName
ORDER BY Id DESC 
OFFSET 30000000 ROWS
FETCH NEXT 20 ROWS ONLY

您可以使用 Keyset Pagination for this. It's far more efficient 而不是使用行集分页(按行号分页)。

在行集分页中,必须先读取所有前面的行,然后才能读取下一页。而在键集分页中,服务器可以立即跳转到索引中的正确位置,因此不会读取不需要的额外行。

为了使其表现良好,您需要在该键上有一个唯一索引,其中包括您需要查询的任何其他列。

在这种类型的分页中,您无法跳转到特定的页码。你跳到一个特定的键并从那里读取。所以你需要保存你所在页面的唯一 ID 并跳到下一个。或者,您可以预先计算或估计每个页面的起点。

一大好处,除了明显的效率提升外,还避免了分页时由于从先前读取的页面中删除行而导致的“缺失行”问题。按键分页时不会发生这种情况,因为按键不会改变。


举个例子:

让我们假设您有一个名为 TableName 的 table,其索引在 Id 上,并且您想从最新的 Id 值开始并向后工作。

您开始于:

SELECT TOP (@numRows)
  *
FROM TableName
ORDER BY Id DESC;

Note the use of ORDER BY to ensure the order is correct

In some RDBMSs you need LIMIT instead of TOP

客户端将保留最后收到的 Id 值(在本例中为最低值)。在下一个请求中,您跳转到该键并继续:

SELECT TOP (@numRows)
  *
FROM TableName
WHERE Id < @lastId
ORDER BY Id DESC;

Note the use of < not <=

如果您想知道,在典型的 B-Tree+ 索引中,具有指定 ID 的行是 not 读取的,它是 after[= 的行78=] 已读。


选择的键必须是唯一的,因此如果您按非唯一列进行分页,则必须添加第二列ORDER BYWHERE。例如,您需要 OtherColumn, Id 上的索引来支持此类查询。不要忘记索引中的 INCLUDE 列。

SQL 服务器不支持 row/tuple comparators,所以你不能做 (OtherColumn, Id) < (@lastOther, @lastId)(但是在 Postgre[=85] 中支持=]、MySQL、MariaDB 和 SQLite)。

相反,您需要以下内容:

SELECT TOP (@numRows)
  *
FROM TableName
WHERE (
    (OtherColumn = @lastOther AND Id < @lastId)
    OR OtherColumn < @lastOther
)
ORDER BY
  OtherColumn DESC,
  Id DESC;

这比看起来更有效,因为 SQL 服务器可以将其转换为适当的 < 两个值。

NULL 的存在使事情变得更加复杂。您可能想单独查询这些行。

在非常大的商家网站上,我们使用存储在伪临时 table 中的 id 的技术组合,并将此 table 加入产品行 table。

举个例子说清楚

我们有这样的 table 设计:

CREATE TABLE S_TEMP.T_PAGINATION_PGN
(PGN_ID              BIGINT IDENTITY(-9 223 372 036 854 775 808, 1) PRIMARY KEY,
 PGN_SESSION_GUID    UNIQUEIDENTIFIER NOT NULL,
 PGN_SESSION_DATE    DATETIME2(0) NOT NULL,
 PGN_PRODUCT_ID      INT NOT NULL,
 PGN_SESSION_ORDER   INT NOT NULL);
CREATE INDEX X_PGN_SESSION_GUID_ORDER 
   ON S_TEMP.T_PAGINATION_PGN (PGN_SESSION_GUID, PGN_SESSION_ORDER)
   INCLUDE (PGN_SESSION_ORDER);
CREATE INDEX X_PGN_SESSION_DATE 
   ON S_TEMP.T_PAGINATION_PGN (PGN_SESSION_DATE);

我们有一个非常大的产品 table 调用 T_PRODUIT_PRD 并且客户使用许多谓词对其进行了过滤。我们将过滤后的 SELECT 中的行以这种方式插入到此 table 中:

DECLARE @SESSION_ID UNIQUEIDENTIFIER = NEWID();
INSERT INTO S_TEMP.T_PAGINATION_PGN
SELECT @SESSION_ID , SYSUTCDATETIME(), PRD_ID,
       ROW_NUMBER() OVER(ORDER BY --> custom order by
FROM   dbo.T_PRODUIT_PRD 
WHERE  ... --> custom filter

然后每次我们需要一个所需的页面时,@N 产品的组合我们添加一个连接到此 table 为:

...
JOIN S_TEMP.T_PAGINATION_PGN
   ON PGN_SESSION_GUID = @SESSION_ID
      AND 1 + (PGN_SESSION_ORDER / @N) = @DESIRED_PAGE_NUMBER
      AND PGN_PRODUCT_ID = dbo.T_PRODUIT_PRD.PRD_ID

所有索引都可以完成工作!

当然,我们必须定期清除此 table,这就是为什么我们有一个计划作业删除 4 个多小时前生成会话的行:

DELETE FROM S_TEMP.T_PAGINATION_PGN
WHERE  PGN_SESSION_DATE < DATEADD(hour, -4, SYSUTCDATETIME());

本着与SQLPro解决方案相同的精神,我建议:

WITH CTE AS
(SELECT 30000000  AS N
UNION ALL SELECT N-1 FROM CTE
WHERE N > 30000000 +1 - 20)
SELECT T.* FROM CTE JOIN TableName T ON CTE.N=T.ID
ORDER BY CTE.N DESC

尝试了 20 亿行,它是即时的! 很容易使它成为一个存储过程...... 当然,如果 id 相互跟随则有效。

Recently I have learnt that the way OFFSET works are by counting how many rows it should skip. After that it is giving your result. In other words, to get the results from rows 30000000 to 30000020 it needs to scan through the first 30000000 and then throw them away.

这不是由于 OFFSET.This 您纯粹的性能问题,因为查询编写不当或缺少索引或未使用索引。

甚至 KEYSET 分页技术也与 OFFSET 类似。

每当涉及百万条记录时,您需要优化查询或了解问题出在哪里,然后优化查询和调整索引。

在您的示例中,您需要抛出 Table 架构及其现有索引。 您需要显示真实查询,然后才能相应地进行计划。

In other words, to get the results from rows 30000000 to 30000020 it needs to scan through the first 30000000 and then throw them away.

只要查询没有以正确的方式编写或索引没有被利用,那么同样的事情就会发生 OFFSET 或任何其他 predicate

假设id是table的PK Table那么名字

;with CTE as
(
SELECT id
FROM TableName
ORDER BY Id DESC 
OFFSET 30000000 ROWS
FETCH NEXT 20 ROWS ONLY
)

Select tb.col,tb.col2,tb.col3
from TableName tb inner join CTE C on tb.id=c.id

您注意到这种情况下的查询计划。OFFSET 不会执行任何操作 SCAN。 以上所有查询都很好,并且已修改为仅使用 INDEX。

您检查过查询计划了吗?您的查询发生了什么?

您是如何实现 KEYSET 分页的?