OFFSET ... FETCH 在高分页值时很慢
Posted
技术标签:
【中文标题】OFFSET ... FETCH 在高分页值时很慢【英文标题】:OFFSET ... FETCH is slow on high paging value 【发布时间】:2021-04-12 10:53:40 【问题描述】:这是我的场景:
CREATE TABLE [dbo].[tblSMSSendQueueMain](
[ID] [int] IDENTITY(1,1) NOT NULL,
[SendMethod] [int] NOT NULL
CONSTRAINT [PK_tblSMSSendQueueLog] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[tblSMSSendQueueMainSendStatus](
[ID] [bigint] IDENTITY(1,1) NOT NULL,
[QueueID] [int] NULL,
[SendStatus] [int] NULL,
[StatusDate] [datetime] NULL,
[UserID] [int] NULL,
CONSTRAINT [PK_tblSMSSendQueueMainSendStatus] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
还有一些索引:
CREATE NONCLUSTERED INDEX [IX_tblSMSSendQueueMainSendStatus_SendStatus_Single] ON [dbo].[tblSMSSendQueueMainSendStatus]
(
[SendStatus] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_tblSMSSendQueueMain_SendMethod] ON [dbo].[tblSMSSendQueueMain]
(
[SendMethod] ASC,
[ID] DESC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
每个表大约有 13m 行 tblSMSSendQueueMainSendStatus 的 QueueID 列是 tblSMSSendQueueMain 的 ID 列的外键。
服务器有一个 8 核 Xeon CPU 和 8GB RAM。
我在分页计划中使用了 offset 和 fetch,它对于 100k 以下的偏移量来说是完美的,但是当偏移量增加(超过 100k)时,查询响应很慢,大约需要 5 或 6 秒才能运行。
这是我的查询:
SELECT q.ID
FROM tblSMSSendQueueMain q
INNER JOIN tblSMSSendQueueMainSendStatus qs
ON q.ID = qs.QueueID
WHERE 1 = 1
AND qs.SendStatus = 5
AND [SendMethod] = 19
ORDER BY q.ID desc OFFSET 10 * (1000000 - 1) ROWS
FETCH NEXT 10 ROWS ONLY
有人知道我哪里出错了吗?
【问题讨论】:
检查执行计划。 docs.microsoft.com/en-us/sql/relational-databases/performance/… 使用OFFSET
和FETCH
进行分页几乎本质上是一种低效的操作,因为服务器实际上只能在到达您需要的位置之前计算100K 行——那里不是“大约正确”的概念,因此它必须尽最大努力为您提供您所要求的 10 行。由于没有人会在到达正确位置之前手动翻阅 10 万个结果,因此您最好使用实际值(无论是日期还是身份值)进行搜索,因为这些搜索可以通过索引提供服务。
@Larnu:我提到 100K 是因为 OP 提到从 100K 开始事情会变慢。我提到 this 是因为我不希望人们认为我在必要时无法数零。伙计们,在重要的时候,我完全可以。 :P 无论是从偏移量 100K 还是 1M 还是 10M 变慢都不会改变根本问题。
很公平,@JeroenMostert。我也不是要暗示你也看不懂零。 :)
【参考方案1】:
这很慢的原因是服务器获取正确起始行的唯一方法是读取它之前的每一行。
你最好使用Keyset Pagination。而不是通过起始行号进行分页,而是传入起始键的参数。
要使其工作,您必须返回一个或多个唯一列,并且要使其具有高性能,它们应该被很好地索引。
传入@startingRow
作为上一批最高的ID
,你可以随心所欲地得到这个。例如。我使用了ORDER BY
,所以它将是最后一行,或者您的客户端应用程序将能够从变量中检索它。
SELECT TOP (10)
q.ID
FROM tblSMSSendQueueMain q
INNER JOIN tblSMSSendQueueMainSendStatus qs
ON q.ID = qs.QueueID
WHERE 1 = 1
AND qs.SendStatus = 5
AND q.[SendMethod] = 19
AND qs.ID > @startingRow -- drop this line for the first query
ORDER BY qs.ID;
我必须说,你的查询有点奇怪。如果外键是q.ID = qs.QueueID
,那么如果你只是查询q.ID
,你会得到多个相同的结果。我怀疑你实际上只想要q.ID
,在这种情况下这是你的唯一密钥:
SELECT TOP (10) DISTINCT
q.ID
FROM tblSMSSendQueueMain q
INNER JOIN tblSMSSendQueueMainSendStatus qs
ON q.ID = qs.QueueID
WHERE 1 = 1
AND qs.SendStatus = 5
AND q.[SendMethod] = 19
AND q.ID > @startingRow -- drop this line for the first query
ORDER BY q.ID;
或者,我更喜欢EXISTS/IN
,因为它更清楚地说明了要求:
SELECT TOP (10)
q.ID
FROM tblSMSSendQueueMain q
WHERE 1 = 1
AND q.[SendMethod] = 19
AND q.ID IN (
SELECT qs.QueueID
FROM tblSMSSendQueueMainSendStatus qs
WHERE qs.SendStatus = 5
)
AND q.ID > @startingRow -- drop this line for the first query
ORDER BY q.ID;
【讨论】:
在您的场景中,我们如何通过一个操作进入第 n 页?例如从第 1 页到第 100000 页 你不能,你只需要做一个粗略的估计,但这确实是唯一的缺点。请参阅我链接的文章中的演示文稿,另请参阅blog.jooq.org/tag/keyset-pagination 我看到了链接的文章,但是该项目需要一个用于输入页码并按下GO按钮的文本框,我想知道为什么在这方面没有合适的解决方案,有点奇怪:) 很简单:因为索引键不是按行编号的。我想作为一次性跳转,您可以使用行分页来获取起始键,但这个想法通常是仅使用索引键来向后和向前分页以上是关于OFFSET ... FETCH 在高分页值时很慢的主要内容,如果未能解决你的问题,请参考以下文章
为啥 MySQL 在使用 JOIN 而不是 WHERE 时很慢?