如何加快 LIMIT 子句中偏移量较大的 MySQL 查询?

Posted

技术标签:

【中文标题】如何加快 LIMIT 子句中偏移量较大的 MySQL 查询?【英文标题】:How can I speed up a MySQL query with a large offset in the LIMIT clause? 【发布时间】:2010-11-17 15:42:43 【问题描述】:

LIMIT 使用大偏移量的 mysql SELECT 时,我遇到了性能问题:

SELECT * FROM table LIMIT m, n;

如果偏移量m 大于 1,000,000,则操作非常缓慢。

我必须使用limit m, n;我不能使用像id > 1,000,000 limit n 这样的东西。

如何优化此语句以获得更好的性能?

【问题讨论】:

另见***.com/questions/4481388/… 【参考方案1】:

也许您可以创建一个索引表,它提供与目标表中的键相关的顺序键。然后,您可以将此索引表连接到您的目标表,并使用 where 子句更有效地获取您想要的行。

#create table to store sequences
CREATE TABLE seq (
   seq_no int not null auto_increment,
   id int not null,
   primary key(seq_no),
   unique(id)
);

#create the sequence
TRUNCATE seq;
INSERT INTO seq (id) SELECT id FROM mytable ORDER BY id;

#now get 1000 rows from offset 1000000
SELECT mytable.* 
FROM mytable 
INNER JOIN seq USING(id)
WHERE seq.seq_no BETWEEN 1000000 AND 1000999;

【讨论】:

这种方法只适用于不包含 where 条件的 select 语句。在我看来,这不是一个好的解决方案。 如何保持这个索引表的更新?就我而言,我必须按日期时间列排序并使用较大的偏移量,从而导致查询缓慢。如果我创建此支持表,则每次有新日期时都需要重新插入,因为它不按顺序排列。我已经看到了这个解决方案,但是使用了临时表。 如果我没看错,您只是将 id 列从 mytable 复制到另一个表中(并且您必须保持两个表的更新)。你能不能只加入你只选择id的地方?这是我见过的大多数解决方法。【参考方案2】:

互联网上某处有一篇博客文章介绍了如何最好地选择要显示的行尽可能紧凑,因此:只有 id;并产生完整的结果应该反过来获取您想要的所有数据仅针对您选择的行

因此,SQL 可能类似于(未经测试,我不确定它是否真的会有用):

select A.* from table A 
  inner join (select id from table order by whatever limit m, n) B
  on A.id = B.id
order by A.whatever

如果您的 SQL 引擎过于原始而无法允许此类 SQL 语句,或者它没有改进任何东西,那么可能值得将这个单一语句分解为多个语句并将 ID 捕获到一个数据结构中。

更新:我找到了我正在谈论的博客文章:它是 Jeff Atwood 在 Coding Horror 上的 "All Abstractions Are Failed Abstractions"。

【讨论】:

我测试了你的 SQL 建议。但它没有任何改进。 如果你有一个基于表 A 的 where 子句怎么办?它不起作用,因为它首先限制,然后应用 where 子句。如果您在子查询内部使用 join,您会降低性能,对吗? 它对我有用,与SELECT bunch,of,fields FROM ... 相比,SELECT id FROM ... 查询在近一百万行的集合上的执行速度快了大约 50 倍。 感谢 Atwood 文章的指点;这是有趣的阅读。但它不建议总是按照你所说的去做。相反,它使用这种技术作为在here工作的示例。我认为本文的整个前提是数据库是复杂的野兽,没有一种解决方案可以适用于所有情况(因此抽象不可避免地会“泄漏”)。【参考方案3】:

Paul Dixon 的回答确实是解决问题的办法,但是你要维护好序列表,保证没有行间断。

如果可行,更好的解决方案是简单地确保原始表没有行间隙,并从 id 1 开始。然后使用 id 抓取行进行分页。

SELECT * FROM table A WHERE id >= 1 AND id SELECT * FROM table A WHERE id >= 1001 AND id

等等……

【讨论】:

SELECT * FROM table WHERE id>1000 LIMIT 1000【参考方案4】:

如果您的表已经有一个索引,我认为没有必要创建一个单独的索引。如果是这样,那么您可以按此主键排序,然后使用键的值来单步执行:

SELECT * FROM myBigTable WHERE id > :OFFSET ORDER BY id ASC;

另一个优化是不使用 SELECT * 而只使用 ID,这样它就可以简单地读取索引而不必定位所有数据(减少 IO 开销)。如果您需要其他一些列,那么也许您可以将它们添加到索引中,以便使用主键读取它们(这很可能保存在内存中,因此不需要磁盘查找) - 尽管这不合适在所有情况下,您都必须玩一玩。

我写了一篇更详细的文章:

http://www.4pmp.com/2010/02/scalable-mysql-avoid-offset-for-large-tables/

【讨论】:

只有 mysql 或 mosts dbs 以这种奇怪的方式行事吗?到目前为止,最好的解决方案是子查询(当您没有有序索引时)。先查询排序,再放偏移量。 只使用ID的想法可能确实是一个很好的解决方案,这取决于我想的存储引擎!【参考方案5】:

如果记录很大,则速度缓慢可能来自加载数据。如果 id 列被索引,那么只选择它会快得多。然后,您可以使用 IN 子句对适当的 id 进行第二次查询(或者可以使用第一个查询中的 min 和 max id 制定 WHERE 子句。)

慢:

SELECT * FROM table ORDER BY id DESC LIMIT 10 OFFSET 50000

快速:

SELECT id FROM table ORDER BY id DESC LIMIT 10 OFFSET 50000

SELECT * FROM table WHERE id IN (1,2,3...10)

【讨论】:

这实际上是这里的最佳答案,并且与 Jeff Atwood 在the other answer 中链接的博客文章所描述的一样。【参考方案6】:

我最近遇到了这个问题。问题是要解决两个部分。首先我必须在我的 FROM 子句中使用一个内部选择,它只在主键上为我做限制和偏移:

$subQuery = DB::raw("( SELECT id FROM titles WHERE id BETWEEN $startId AND $endId  ORDER BY title ) as t");  

然后我可以将其用作查询的 from 部分:

'titles.id',
                            'title_eisbns_concat.eisbns_concat', 
                            'titles.pub_symbol', 
                            'titles.title', 
                            'titles.subtitle', 
                            'titles.contributor1', 
                            'titles.publisher', 
                            'titles.epub_date', 
                            'titles.ebook_price', 
                            'publisher_licenses.id as pub_license_id', 
                            'license_types.shortname',
                            $coversQuery
                        )
                        ->from($subQuery)
                        ->leftJoin('titles',  't.id',  '=', 'titles.id')
                        ->leftJoin('organizations', 'organizations.symbol', '=', 'titles.pub_symbol') 
                        ->leftJoin('title_eisbns_concat', 'titles.id', '=', 'title_eisbns_concat.title_id') 
                        ->leftJoin('publisher_licenses', 'publisher_licenses.org_id', '=', 'organizations.id') 
                        ->leftJoin('license_types', 'license_types.id', '=', 'publisher_licenses.license_type_id')

我第一次创建这个查询时,我在 MySql 中使用了 OFFSET 和 LIMIT。这一直很好,直到我超过第 100 页,然后偏移量开始变得难以忍受。在我的内部查询中将其更改为 BETWEEN 可以加快任何页面的速度。我不知道为什么 MySql 没有加快 OFFSET 的速度,但似乎又把它卷了回来。

【讨论】:

这与许多其他解决方案非常相似,您事先知道要从哪个 ID 开始限制(可能有更优雅的方法来做到这一点)。主要问题是当您需要在中间显示页面时,您不知道从哪个 ID 开始(由 where 子句确定)。我的猜测是您的子查询中的按标题排序未编入索引。您可以尝试使用 explain 来弄清楚发生了什么,并创建新的索引。尝试索引文本可能会出现问题。 dba.stackexchange.com/questions/35821/…

以上是关于如何加快 LIMIT 子句中偏移量较大的 MySQL 查询?的主要内容,如果未能解决你的问题,请参考以下文章

mysql 中的LIMIT用法

大数据量时 Mysql LIMIT如何正确对其进行优化(转载)

优化LIMIT分页

MySQL的limit子句

Sql Server 按偏移量分页行 - 没有'ORDER BY'

如何使用子查询来定义 Mysql SELECT LIMIT 偏移量?