【问题标题】:MySQL LIMIT X, Y slows down as I increase XMySQL LIMIT X, Y 当我增加 X 时变慢
【发布时间】:2020-02-25 04:42:59
【问题描述】:

我有一个包含大约 600 000 个列表的数据库,在带有分页的页面上浏览这些列表时,我使用此查询来限制记录:

SELECT file_id, file_category FROM files ORDER BY file_edit_date DESC LIMIT 290580, 30

在第一页 LIMIT 0, 30 上,它会在几毫秒内加载,LIMIT 30,30LIMIT 60,30LIMIT 90,30 等也是如此。但是当我前进到页面末尾时,查询大约需要 1 秒执行。

索引可能不相关,如果我运行它也会发生:

SELECT * FROM `files` LIMIT 400000,30

不知道为什么。 有什么办法可以改善吗?

除非有更好的解决方案,否则只加载所有记录并在 PHP 页面中循环它们以查看记录是否在分页范围内并打印它是一种不好的做法吗?

服务器是具有 16GB 内存的 i7; MySQL 社区服务器 5.7.28; 文件表大约 200 MB

如果重要的话,这里是 my.cnf

query_cache_type = 1

query_cache_size = 1G

sort_buffer_size = 1G

thread_cache_size = 256

table_open_cache = 2500

query_cache_limit = 256M

innodb_buffer_pool_size = 2G

innodb_log_buffer_size = 8M

tmp_table_size=2G

max_heap_table_size=2G

【问题讨论】:

  • 你在追pagination alternative吗?
  • 看起来像我。它需要遍历所有记录才能达到给定的限制,我没想到会这么痛苦。
  • 如果您的行有id UNSIGNED AUTO_INCREMENT, PRIMARY KEY(id)。然后你可以使用WHERE id>? && id<?
  • 我认为当我只需要从某个类别中选择文件时,这是行不通的,如果我理解正确,这仅在没有已删除项目时才有效。我找到了一种更简单的方法,在 urls 中而不是 ?page=1page=2 我将简单地使用 ?date=TIMESTAMP 这将允许我直接跳转到右侧 file_edit_date 限制。
  • 我明白你在说DELETE 问题匹配你将从哪里开始。为什么不将您的计数存储在变量中,而不是 PRIMARY KEY

标签: mysql database performance pagination mariadb


【解决方案1】:

您可能会发现添加以下索引将有助于提高性能:

CREATE INDEX idx ON files (file_edit_date DESC, file_id, file_category);

如果使用,MySQL 将只需要一次索引扫描来检索某个偏移量的记录数。请注意,我们在 select 子句中包含列,以便索引可以覆盖整个查询。

【讨论】:

  • 我添加了一些细节。如果它也运行这个查询慢,索引是否仍然相关? SELECT * FROM files LIMIT 400000,30 ?如果从较小的数字开始,则运行速度很快。
  • 您需要运行 EXPLAIN 以更好地了解 MySQL 实际在做什么,但我的建议在大多数情况下应该会执行良好。
  • file_idedit_date 已经在索引中,我应该提到这一点,但我找到了更多信息,看起来有一个已知限制,它需要通过所有行以便在我想要的地方停下来,这需要时间。我会尝试添加一个无间隙的列file_pagination_index,这样我就可以告诉它从 60 加载到 90。
  • 警告:在 8.0 之前,INDEX 声明中的 DESC 被忽略。在这种特殊情况下,这个警告无关紧要,因为其他列只是为了“覆盖”而优化器愿意向后扫描表。
  • 等一下,我的注意力不集中,我只是注意到上面索引上的DESC,所以这是否意味着这个新索引有助于向后处理项目?帮助解决页尾当前的呆滞问题?
【解决方案2】:

LIMIT 是为了减少结果集的大小而发明的,如果您使用索引对结果集进行排序,优化器可以使用它。

当使用LIMIT x,n 时,服务器需要处理 x+n 行来传递结果。 x 的值越高,需要处理的行数就越多。

这是一个简单表的解释输出,在列 a 上有一个唯一索引:

MariaDB [test]> explain select a,b from t1 order by a limit 0, 2;
+------+-------------+-------+-------+---------------+---------+---------+------+------+-------+
| id   | select_type | table | type  | possible_keys | key     | key_len | ref  | rows | Extra |
+------+-------------+-------+-------+---------------+---------+---------+------+------+-------+
|    1 | SIMPLE      | t1    | index | NULL          | PRIMARY | 4       | NULL | 2    |       |
+------+-------------+-------+-------+---------------+---------+---------+------+------+-------+
1 row in set (0.00 sec)

MariaDB [test]> explain select a,b from t1 order by a limit 400000, 2;
+------+-------------+-------+-------+---------------+---------+---------+------+--------+-------+
| id   | select_type | table | type  | possible_keys | key     | key_len | ref  | rows   | Extra |
+------+-------------+-------+-------+---------------+---------+---------+------+--------+-------+
|    1 | SIMPLE      | t1    | index | NULL          | PRIMARY | 4       | NULL | 400002 |       |
+------+-------------+-------+-------+---------------+---------+---------+------+--------+-------+
1 row in set (0.00 sec)

运行上述语句时(不带 EXPLAIN),LIMIT 0 的执行时间为 0.01 秒,LIMIT 400000 为 0.6 秒。

由于 MariaDB 不支持子查询中的 LIMIT,您可以将 SQL 语句拆分为两个语句:

第一条语句获取id(并且只需要读取索引文件),第二条语句使用从第一条语句中获取的id:

MariaDB [test]> select a  from t1 order by a limit 400000, 2;
+--------+
| a      |
+--------+
| 595312 |
| 595313 |
+--------+
2 rows in set (0.08 sec)

MariaDB [test]> select a,b from t1 where a in (595312,595313);
+--------+------+
| a      | b    |
+--------+------+
| 595312 | foo  |
| 595313 | foo  |
+--------+------+
2 rows in set (0.00 sec)

【讨论】:

  • 有趣——我尝试了EXPLAIN的4个变种,只有一个显示400002;其余的显示(大约)表中的行数。此外,MariaDB 的最后一个显示实际限制的版本是 10.0。
【解决方案3】:

注意:我将使用一些强硬的语言。计算机又大又快,它们可以处理比十年前更大的东西。但是,正如您所发现的,存在限制。我将指出您威胁过的多个限制;我将尝试解释为什么限制可能是个问题。

设置

query_cache_size = 1G

太可怕了。每当写入表时,QC 会扫描 1GB 以查找对该表的任何引用,以清除 QC 中的条目。将其减少到 50M。仅此一项,就可以加速整个系统。

sort_buffer_size = 1G
tmp_table_size=2G
max_heap_table_size=2G

由于不同的原因而不好。如果您有多个执行复杂查询的连接,则可能会为每个连接分配大量 RAM,从而占用 RAM,导致交换,并可能崩溃。不要将它们设置为高于 RAM 的 1%。

一般情况下,不要盲目更改 my.cnf 中的值。最重要的设置是innodb_buffer_pool_size,它应该比您的数据集大,但不超过可用 RAM 的 70%。

加载所有记录

哎哟!将所有数据从 MySQL 转移到 PHP 的成本是不小的。一旦进入 PHP,它将被存储在不是为大量数据设计的结构中——400030(或 600000)行在 PHP 中可能需要 1GB;这可能会破坏它的“memory_limit”,导致 PHP 崩溃。 (好吧,只是因为一条错误消息而死。)可以提高该限制,但是 PHP 可能会将 MySQL 推出内存,导致交换,或者可能耗尽交换空间。真是一团糟!

偏移量

至于大号OFFSET,为什么呢?您是否有用户对数据进行分页?他快到10,000页了?他身上有蜘蛛网吗?

OFFSET 在您的示例中必须读取并跳过 290580 行。这很昂贵。

有关没有这种开销的分页方式,请参阅http://mysql.rjweb.org/doc.php/pagination

如果您有一个程序“抓取”所有 600K 行,一次 30 行,那么该链接中有关“记住您离开的地方”的提示将非常适合这种用途。它不会“减速”。

如果您正在做不同的事情;这是什么?

分页和空白

没问题。另请参阅:http://mysql.rjweb.org/doc.php/deletebig#deleting_in_chunks,它更旨在遍历整张桌子。它专注于找到前进的第 30 行的有效方法。 (这并不一定比记住最后一个id更好。)

该链接针对DELETEing, but can easily be revised toSELECT`。

一次扫描 600K 行表 30 行的一些数学运算:

我的链接:已触及 600K 行。或者两倍,如果您按照第二个链接中的建议使用LIMIT 30,1 向前看。

OFFSET ..., 30 必须触摸 (600K/30)*600K/2 行 -- 大约 6 十亿 行。

(推论:将 30 更改为 100 会加快您的查询速度,尽管它仍然会非常缓慢。它不会加快我的方法,但它已经相当快了。)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-26
    • 2011-04-24
    • 1970-01-01
    • 2013-11-23
    • 2019-07-26
    • 2022-11-13
    相关资源
    最近更新 更多