我想对数据量很大的表应用分页。我只想知道比在 SQL Server 中使用 OFFSET 更好的选择。
这是我的简单查询:
SELECT *
FROM TableName
ORDER BY Id DESC
OFFSET 30000000 ROWS
FETCH NEXT 20 ROWS ONLY
您可以为此使用Keyset Pagination。它比使用Rowset Pagination(按行号分页)远更高效。
在行集分页中,必须读取之前的所有行,然后才能读取下一页。而在键集分页中,服务器可以“立即”跳转到索引中的正确位置,因此不会读取不需要的额外行。 为了使其性能良好,您需要在该键上有一个唯一索引,其中包括您需要查询的任何其他列。
在这种类型的分页中,您无法跳转到特定页码。您跳转到特定的键并从那里开始阅读。因此,您需要保存当前页面的唯一 ID,然后跳到下一个页面。或者,您可以预先计算或估计每个页面的起点。
除了明显的效率提升之外,还有一个很大的好处,那就是避免分页时由于从先前读取的页面中删除行而导致的“缺失行”问题。按键分页时不会发生这种情况,因为键不会改变。
让我们假设您有一个名为
TableName
的表,其索引位于
Id
,并且您希望从最新的 Id
值开始并向后工作。你开始于:
SELECT TOP (@numRows)
*
FROM TableName
ORDER BY Id DESC;
ORDER BY
以确保顺序正确
在某些 RDBMS 中,您需要LIMIT
而不是
客户端将保留最后收到的TOP
Id
值(在本例中为最低值)。在下一个请求时,您跳转到该键并继续:
SELECT TOP (@numRows)
*
FROM TableName
WHERE Id < @lastId
ORDER BY Id DESC;
注意使用 未<
而不是
如果您想知道,在典型的 B-Tree+ 索引中,具有指示 ID 的行<=
被读取,而是其之后的行被读取。
必须是唯一的,因此,如果您按非唯一列进行分页,则必须向ORDER BY
和
WHERE
添加第二列。例如,您需要 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
的存在使事情变得更加复杂。您可能想单独查询这些行。
让我用一个明显的例子来谈谈。
我们的桌子是这样设计的:
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());
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 相互跟随,则有效。
CREATE proc [dbo].[GetTransDetails](
@PageNo int = 1,
@PageSize int)as BEGIN
declare @idfrom int=1
declare @idto int=30 //number of rows
SET NOCOUNT ON;
if @PageNo>1
begin
set @idfrom=(@PageNo*30)-29
set @idto=@PageNo*30
end
select top 30 * from
(select ROW_NUMBER() OVER (ORDER BY id desc) AS rownumber,
*
FROM transdetails
)transList
where transList.rownumber between @idfrom and @idto END