有没有更好的选择来应用分页而不在 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;
注意使用
<
而不是<=
如果您想知道,在典型的 B-Tree+ 索引中,具有指示 ID 的行未被读取,它是被读取的行之后。
选择的键必须是唯一的,所以如果您按非唯一列分页,那么您必须向ORDER BY
和@987654333 添加第二列@。例如,您需要在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;
这比看起来更有效,因为 SQL Server 可以将这两个值转换为正确的<
。
NULL
s 的存在使事情变得更加复杂。您可能希望单独查询这些行。
【讨论】:
所以你不能直接跳转到第 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?的主要内容,如果未能解决你的问题,请参考以下文章