ChatGPT解决这个技术问题 Extra ChatGPT

为什么 MYSQL 更高的 LIMIT 偏移量会减慢查询速度?

简而言之:一张包含超过 1600 万条记录的表 [2GB 大小]。当使用 ORDER BY *primary_key* 时,SELECT 的 LIMIT 偏移量越高,查询就越慢

所以

SELECT * FROM large ORDER BY `id`  LIMIT 0, 30 

远远少于

SELECT * FROM large ORDER BY `id` LIMIT 10000, 30 

那只订购 30 条记录,无论如何都一样。所以这不是 ORDER BY 的开销。现在,当获取最新的 30 行时,大约需要 180 秒。如何优化那个简单的查询?

注意:我是作者。在上述情况下,MySQL 不引用索引(PRIMARY)。有关说明,请参见用户“Quassnoi”的以下链接。
相关链接:We need tool support for keyset pagination。如果您想知道使用偏移量或键集分页时数据库内部发生了什么,请查看这些幻灯片。

E
Elzo Valugi

我自己也有同样的问题。鉴于您想要收集大量此类数据而不是一组特定的 30 个数据,您可能会运行一个循环并将偏移量增加 30。

所以你可以做的是:

持有一组数据的最后一个id(30) (eg lastId = 530) 添加条件WHERE id > lastId limit 0,30

所以你总是可以有一个零偏移。你会惊讶于性能的提升。


对所有人来说,这可能并不明显,这仅在您的结果集按该键以升序排序时才有效(对于降序,相同的想法有效,但将 > lastid 更改为 < lastid。)它是否无关紧要主键,或另一个字段(或一组字段)。
请注意,分页结果中经常使用限制/偏移量,并且持有 lastId 根本不可能,因为用户可以跳转到任何页面,而不总是下一页。换句话说,偏移量通常需要根据页面和限制动态计算,而不是遵循连续的模式。
我在 mysql.rjweb.org/doc.php/pagination 中更详细地讨论了“记住你离开的地方”
男人。你是一个活的救星。当我尝试您的答案时,我现在有 5 百万条数据需要大约 90 分钟来处理所有带有偏移量和限制的数据。该死的,它只需要 9 分钟来处理谢谢你的人。谢谢你!!
@Lanti让我们假设第563页从偏移量563 * 30 = 16890开始,因为在OP的示例中,30是页面大小并假设页码从0开始。进一步假设列id是唯一的并且被索引。然后执行 select id from large order by id limit 16889, 1 读取第 562 页最后一行的 id。这应该是相当有效的,因为只涉及索引。现在您有了“lastId”来继续选择下一页。
Q
Quassnoi

较高的偏移量会减慢查询速度是正常的,因为查询需要计算前 OFFSET + LIMIT 条记录(并且只占用其中 LIMIT 条记录)。该值越高,查询运行的时间越长。

查询不能直接到 OFFSET,因为首先,记录的长度可能不同,其次,删除的记录可能存在间隙。它需要在途中检查和计算每条记录。

假设 id 是 MyISAM 表的主键,或者是 InnoDB 表上的唯一非主键字段,您可以使用以下技巧加速它:

SELECT  t.* 
FROM    (
        SELECT  id
        FROM    mytable
        ORDER BY
                id
        LIMIT 10000, 30
        ) q
JOIN    mytable t
ON      t.id = q.id

见这篇文章:

MySQL ORDER BY / LIMIT 性能:延迟行查找


MySQL“早期行查找”行为是为什么它说这么久的答案。通过您提供的技巧,只有匹配的 id(直接通过索引)被绑定,从而节省了太多记录的不需要的行查找。那成功了,万岁!
@harald:“不工作”到底是什么意思?这是纯粹的性能改进。如果 ORDER BY 没有可用的索引,或者索引涵盖了您需要的所有字段,则不需要此解决方法。
@f055:答案是“加速”,而不是“即时”。你读过答案的第一句话吗?
是否可以为 InnoDB 运行类似的东西?
@Lanti:请将它作为一个单独的问题发布,并且不要忘记用 postgresql 标记它。这是一个特定于 MySQL 的答案。
R
Riedsio

MySQL 不能直接转到第 10000 条记录(或您建议的第 80000 个字节),因为它不能假设它是这样打包/排序的(或者它具有 1 到 10000 的连续值)。尽管实际上可能是这样,但 MySQL 不能假设没有漏洞/间隙/删除的 id。

因此,正如鲍勃所指出的,MySQL 必须先获取 10000 行(或遍历 id 上索引的第 10000 个条目),然后才能找到要返回的 30。

编辑:为了说明我的观点

请注意,虽然

SELECT * FROM large ORDER BY id LIMIT 10000, 30 

会很慢(呃),

SELECT * FROM large WHERE id >  10000 ORDER BY id LIMIT 30 

将是 fast(er),并且会返回相同的结果,前提是没有丢失 id(即间隙)。


这是对的。但是由于它受“id”的限制,为什么当该id在索引(主键)内时需要这么长时间?优化器应直接引用该索引,然后获取具有匹配 id 的行(来自该索引)
如果您在 id 上使用 WHERE 子句,它可以直接指向该标记。但是,如果你对它进行限制,按 id 排序,它只是与开头的相对计数器,所以它必须贯穿整个过程。
很好的文章eversql.com/…
为我工作@Riedsio 谢谢。
s
sym

我发现了一个有趣的例子来优化 SELECT 查询 ORDER BY id LIMIT X,Y。我有 3500 万行,所以需要 2 分钟才能找到一系列行。

这是诀窍:

select id, name, address, phone
FROM customers
WHERE id > 990
ORDER BY id LIMIT 1000;

只需将 WHERE 与您获得的最后一个 id 放在一起,就可以大大提高性能。对我来说,从 2 分钟到 1 秒 :)

其他有趣的技巧:http://www.iheavy.com/2013/06/19/3-ways-to-optimize-for-paging-in-mysql/

它也适用于字符串


这仅适用于没有数据被删除的表
@miro 仅当您假设您的查询可以在随机页面上进行查找时才如此,我不相信这张海报是假设的。虽然对于大多数现实世界的情况我不喜欢这种方法,但只要您始终基于获得的最后一个 id,这将适用于差距。
b
bobs

这两个查询的耗时部分是从表中检索行。从逻辑上讲,在 LIMIT 0, 30 版本中,只需要检索 30 行。在 LIMIT 10000, 30 版本中,评估 10000 行并返回 30 行。可以对我的数据读取过程进行一些优化,但请考虑以下几点:

如果查询中有 WHERE 子句怎么办?引擎必须返回所有符合条件的行,然后对数据进行排序,最后得到 30 行。

还要考虑在 ORDER BY 序列中未处理行的情况。必须对所有符合条件的行进行排序以确定要返回的行。


只是想知道为什么要花费时间来获取那 10000 行。该字段上使用的索引(id,它是一个主键)应该使检索这些行的速度与寻找记录号的 PK 索引一样快。 10000,这反过来应该是快速寻找文件到该偏移量乘以索引记录长度,(即,寻找 10000*8 = 字节号 80000 - 假设 8 是索引记录长度)
@Rahman - 计算超过 10000 行的唯一方法是一一跨过它们。这可能只涉及一个索引,但索引行仍然需要时间来逐步完成。没有 MyISAM 或 InnoDB 结构可以正确(在所有情况下)“寻找”记录 10000。10000*8 建议假设(1)MyISAM,(2)固定长度记录,以及(3)从不从表中删除.无论如何,MyISAM 索引是 BTree,所以它不起作用。
正如这个答案所说,我相信,真正慢的部分是行查找,而不是遍历索引(当然也会加起来,但远不及磁盘上的行查找)。根据为此问题提供的解决方法查询,我相信如果您选择索引之外的列,则往往会发生行查找——即使它们不是 order by 或 where 子句的一部分。我还没有找到为什么这是必要的原因,但这似乎是为什么某些变通办法会有所帮助。
我相信延迟是由计算索引树中的条目引起的,而不是查找起始索引(针对该索引树优化了 SQL 索引树,它被指向靠近目标行,而不经过特定行)。下一部分,读取行数,在使用 WHERE ID > x 时同样“慢”。但无论如何,后者在大多数现实世界的应用程序中都是无用的。
c
ch271828n

对于那些对比较和数字感兴趣的人:)

实验 1:数据集包含大约 1 亿行。每行包含几个 BIGINT、TINYINT 以及两个包含大约 1k 个字符的 TEXT 字段(故意)。

蓝色 := SELECT * FROM post ORDER BY id LIMIT {offset}, 5

橙色 := @Quassnoi 的方法。 SELECT t.* FROM (SELECT id FROM post ORDER BY id LIMIT {offset}, 5) AS q JOIN post t ON t.id = q.id

当然,第三种方法,... WHERE id>xxx LIMIT 0,5 在这里没有出现,因为它应该是常数时间。

实验2:类似的事情,只是一行只有3个BIGINT。

绿色 := 之前的蓝色

red := 之前的橙色

https://i.stack.imgur.com/bjxjA.png


您的 id 是主键字段还是非主键字段?
@ospider 主要恕我直言