在 SQL Server 中是否有更好的选项来应用分页而不应用 OFFSET?

Md.*_*kot 24 sql sql-server pagination keyset-pagination

我想对包含大量数据的表应用分页。我只想知道比在 SQL Server 中使用 OFFSET 更好的选择。

这是我的简单查询:

SELECT *
FROM TableName
ORDER BY Id DESC 
OFFSET 30000000 ROWS
FETCH NEXT 20 ROWS ONLY
Run Code Online (Sandbox Code Playgroud)

Cha*_*ace 44

您可以为此使用键集分页。它比使用 Rowset Pagination(按行号分页)要高效得多。

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

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

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

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


这是一个例子:

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

你从以下开始:

SELECT TOP (@numRows)
  *
FROM TableName
ORDER BY Id DESC;
Run Code Online (Sandbox Code Playgroud)

注意使用ORDER BY以确保顺序正确

在某些 RDBMS 中,您需要LIMIT而不是TOP

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

SELECT TOP (@numRows)
  *
FROM TableName
WHERE Id < @lastId
ORDER BY Id DESC;
Run Code Online (Sandbox Code Playgroud)

注意使用<not<=

如果您想知道,在典型的 B-Tree+ 索引中,不会读取具有指定 ID 的行,而是读取该行之后的行。


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

SQL Server 不支持行/元组比较器,因此您不能(OtherColumn, Id) < (@lastOther, @lastId)这样做(但是 PostgreSQL、MySQL、MariaDB 和 SQLite 均支持此操作)。

相反,您需要以下内容:

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

这比看起来更有效,因为 SQL Server 可以将其转换为<两个值的正确值。

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

  • 因此,除非您已读取所有前面的行来计算“@lastId”,否则您无法直接跳转到第 30,000,000 行。这有什么用? (2认同)
  • 确实如此,你不能,而且我确实提到过这一点。但用户实际上从一开始就“想要”这样做的情况非常罕见。正如您在评论中所说的 *“有人真的需要查看第 3000 万行吗?”* 通常他们可能会说“我已经在列表中完成了这一点,我想要接下来的几行”并按关键作品分页对此更好,因为丢失行问题不会影响它(如果您从较早的页面中删除行并按行号分页,那么您将丢失行)。它非常适合无限滚动和批量操作 (2认同)

小智 8

在非常大的商家网站上,我们使用存储在伪临时表中的 ids 技术组合,并将该表与产品表的行连接起来。

我用一个明显的例子来谈谈。

我们有这样的表格设计:

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);
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

然后,每当我们需要所需的页面、@N 产品的复合时,我们都会向该表添加一个联接,如下所示:

...
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
Run Code Online (Sandbox Code Playgroud)

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

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

DELETE FROM S_TEMP.T_PAGINATION_PGN
WHERE  PGN_SESSION_DATE < DATEADD(hour, -4, SYSUTCDATETIME());
Run Code Online (Sandbox Code Playgroud)