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

Posted

技术标签:

【中文标题】有没有更好的选择来应用分页而不在 SQL Server 中应用 OFFSET?【英文标题】:Is there any better option to apply pagination without applying OFFSET in SQL Server? 【发布时间】:2021-12-29 12:37:02 【问题描述】:

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

这是我的简单查询:

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

【问题讨论】:

是的。 use-the-index-luke.com/sql/partial-results/fetch-next-page 为什么你需要一个比OFFSET / FETCH更好的选择OFFSET / FETCH 怎么了?? 您真的需要跳转到第 1,500,000 页吗?也许您需要更好的标准? @marc_s 最近我了解到 OFFSET 的工作方式是计算它应该跳过多少行。之后,它会给出你的结果。换句话说,要从第 30000000 行到第 30000020 行获取结果,它需要扫描前 30000000 行,然后将它们丢弃。好像很浪费。因此,如果存在更好的方法,我想知道一个更好的选择来应用服务器端分页。 另见use-the-index-luke.com/no-offset,它被称为键集分页。 【参考方案1】:

您可以为此使用Keyset Pagination这是far more efficient,而不是使用行集分页(按行号分页)。

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

在这种类型的分页中,您不能跳转到特定的页码。你跳到一个特定的键并从那里读取。为了使其表现良好,您需要在该键上有一个唯一索引,其中包括您需要查询的任何其他列。

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


这是一个例子:

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

你开始:

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

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

客户端将保存最后收到的Id 值(在这种情况下是最低的)。在下一个请求中,您跳转到该键并继续:

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

注意使用&lt; 而不是&lt;=

如果您想知道,在典型的 B-Tree+ 索引中,具有指示 ID 的行被读取,它是被读取的行之后


选择的键必须是唯一的,所以如果您按非唯一列分页,那么您必须向ORDER BY 和@987654333 添加第二列@。例如,您需要在OtherColumn, Id 上建立一个索引来支持这种类型的查询。不要忘记索引中的INCLUDE 列。

SQL Server 不支持行/元组比较器,因此您不能这样做(OtherColumn, Id) &lt; (@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;

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

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

【讨论】:

所以你不能直接跳转到第 30,000,000 行,除非你已经阅读了所有前面的行来计算 @lastId。这有什么用? 这是真的,你不能,我确实提到了这一点。但很少有用户从一开始就想要这样做。正如您在评论中所说的那样“有人真的需要查看第 30 百万行吗?” 通常他们可能会说“我已经到了列表中的这一点,我想要接下来的几行”并且按键分页对此效果更好,因为缺少行的问题不会影响它(如果您从早期页面中删除行并按行号分页,那么您将错过行)。它非常适合无限滚动和批处理操作【参考方案2】:

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

让我用一个清晰​​的例子来谈谈。

我们有这样的桌子设计:

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);

我们有一个非常大的产品表调用 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

然后每次我们需要一个想要的页面,@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

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

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

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

【讨论】:

【参考方案3】:

本着与 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 相互跟随,则有效。

【讨论】:

以上是关于有没有更好的选择来应用分页而不在 SQL Server 中应用 OFFSET?的主要内容,如果未能解决你的问题,请参考以下文章

ssm中逆向工程与分页的应用

MySQL 嵌套连接排序

php form表单怎么把数据提交到本页而不跳转?

谷歌应用引擎和分页

更新由于分页/排序而隐藏的行

Servlet 分页保存查询条件