【问题标题】:Adding limit clause to MySQL query slows it down dramatically向 MySQL 查询添加限制子句会显着降低它的速度
【发布时间】:2012-11-30 13:35:32
【问题描述】:

我正在尝试解决 MySQL 的性能问题,因此我想创建一个较小版本的表来使用。当我在查询中添加 LIMIT 子句时,它会从大约 2 秒(对于完整插入)变为天文数字(42 分钟)。

mysql> select pr.player_id, max(pr.insert_date) as insert_date from player_record pr
inner join date_curr dc on pr.player_id = dc.player_id where pr.insert_date < '2012-05-15'
group by pr.player_id;
+------------+-------------+
| 1002395119 | 2012-05-14  |
...
| 1002395157 | 2012-05-14  |
| 1002395187 | 2012-05-14  |
| 1002395475 | 2012-05-14  |
+------------+-------------+
105776 rows in set (2.19 sec)

mysql> select pr.player_id, max(pr.insert_date) as insert_date from player_record pr
inner join date_curr dc on pr.player_id = dc.player_id where pr.insert_date < '2012-05-15' 
group by pr.player_id limit 1;
+------------+-------------+
| player_id  | insert_date |
+------------+-------------+
| 1000000080 | 2012-05-14  |
+------------+-------------+
1 row in set (42 min 23.26 sec)

mysql> describe player_record;
+------------------------+------------------------+------+-----+---------+-------+
| Field                  | Type                   | Null | Key | Default | Extra |
+------------------------+------------------------+------+-----+---------+-------+
| player_id              | int(10) unsigned       | NO   | PRI | NULL    |       |
| insert_date            | date                   | NO   | PRI | NULL    |       |
| xp                     | int(10) unsigned       | YES  |     | NULL    |       |
+------------------------+------------------------+------+-----+---------+-------+
17 rows in set (0.01 sec) (most columns removed)

player_record 表中有 2000 万行,因此我在内存中为我要比较的特定日期创建了两个表。

CREATE temporary TABLE date_curr 
(
      player_id INT UNSIGNED NOT NULL, 
      insert_date DATE,     
      PRIMARY KEY player_id (player_id, insert_date)
 ) ENGINE=MEMORY;
INSERT into date_curr 
SELECT  player_id, 
        MAX(insert_date) AS insert_date 
FROM player_record 
WHERE insert_date BETWEEN '2012-05-15' AND '2012-05-15' + INTERVAL 6 DAY
GROUP BY player_id;

CREATE TEMPORARY TABLE date_prev LIKE date_curr;
INSERT into date_prev 
SELECT pr.player_id,
       MAX(pr.insert_date) AS insert_date 
FROM  player_record pr 
INNER join date_curr dc 
      ON pr.player_id = dc.player_id 
WHERE pr.insert_date < '2012-05-15' 
GROUP BY pr.player_id limit 0,20000;

date_curr 有 216k 条目,如果我不使用限制,date_prev 有 105k 条目。

这些表只是流程的一部分,用于将另一个表(5 亿行)缩减为可管理的内容。 date_curr 包含当前周的 player_id 和 insert_date,date_prev 包含当前周之前的 player_id 和最近的 insert_date,用于 date_curr 中存在的任何 player_id。

这里是解释输出:

mysql> explain SELECT pr.player_id, 
                      MAX(pr.insert_date) AS insert_date 
               FROM   player_record pr 
               INNER  JOIN date_curr dc 
                      ON pr.player_id = dc.player_id
               WHERE  pr.insert_date < '2012-05-15' 
               GROUP  BY pr.player_id 
               LIMIT  0,20000;                    
+----+-------------+-------+-------+---------------------+-------------+---------+------+--------+----------------------------------------------+
| id | select_type | table | type  | possible_keys       | key         | key_len | ref  | rows   | Extra                                        |
+----+-------------+-------+-------+---------------------+-------------+---------+------+--------+----------------------------------------------+
|  1 | SIMPLE      | pr    | range | PRIMARY,insert_date | insert_date | 3       | NULL     | 396828 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | dc    | ALL   | PRIMARY             | NULL        | NULL    | NULL | 216825 | Using where; Using join buffer               |
+----+-------------+-------+-------+---------------------+-------------+---------+------+--------+----------------------------------------------+
2 rows in set (0.03 sec)

这是在具有 24G RAM 专用于数据库的系统上,目前几乎处于空闲状态。这个特定的数据库是测试,所以它是完全静态的。我重新启动了 mysql,它仍然具有相同的行为。

这里是“show profile all”的输出,大部分时间都花在复制到 tmp 表上。

| Status               | Duration   | CPU_user   | CPU_system | Context_voluntary | Context_involuntary | Block_ops_in | Block_ops_out | Messages_sent | Messages_received | Page_faults_major | Page_faults_minor | Swaps | Source_function       | Source_file   | Source_line |
| Copying to tmp table | 999.999999 | 999.999999 |   0.383941 |            110240 |               18983 |        16160 |           448 |             0 |                 0 |                 0 |                43 |     0 | exec                  | sql_select.cc |        1976 |

【问题讨论】:

  • 我怀疑你的问题是文件排序,但我更担心查询在做什么。例如,您将行添加到 date_curr,然后将行添加到 date_prev。然后在您的查询中,您从 date_curr 中选择并且从不在查询中使用 date_prev?请稍微澄清一下这个例子。另外,该程序的哪一部分花费时间最长?是选择吗?是插页吗?请澄清。
  • show create table player_record 怎么样,这样我们就可以看到索引等所有其他内容。我敢打赌,如果您在要查询的日期列上添加索引会加快速度。
  • 我在 insert_date 上有一个索引,它是解释语句中使用的键。无限制的查询2秒内返回(需要组装完整的响应);我不知道限制输出比提供整个输出要慢多少。 date_curr 采用当前周的 player_record 条目,date_prev 采用当前周之前的最新 player_record。 date_prev 查询(有限制)是一个慢的查询,它和不做插入的独立查询一样慢。
  • 您似乎严重过早地进行了优化。如果您只有 2000 万条记录,如果索引正确,您应该能够在表中进行最合理的工作。 (当人们开始使用临时表时,您也会遇到这种窘境。)您的表足够简单,即使 500MM 行也应该可以管理。使用 SQL 的“分而治之”可能会使读写变得更容易,但总是需要更长的时间。只要您尊重它,您就永远不会像 SQL 查询优化器那样擅长它。
  • @Josh 你的表在 playerid 和 insert_date 上有一个主键,对吗?然后你又索引了 insert_date ?如果我是正确的,那可能就是问题所在。我赞成删除 insert_date 上的索引并再次运行查询 MySQL 可能在这里使用了错误的索引。这可能是因为这是一个静态数据库,而 MySQL 可能有错误的统计数据。

标签: mysql performance limit


【解决方案1】:

答案有点长,但我希望你能从中学到一些东西。

因此,根据说明语句中的证据,您可以看到 MySQL 查询优化器可以使用的两个可能的索引如下:

possible_keys
PRIMARY,insert_date 

然而 MySQL 查询优化器决定使用以下索引:

key
insert_date

这是 MySQL 查询优化器使用错误索引的罕见情况。现在有一个可能的原因。您正在处理静态开发数据库。您可能会将其从生产中恢复以进行开发。

当 MySQL 优化器需要决定在查询中使用哪个索引时,它会查看所有可能索引的统计信息。您可以在这里阅读更多关于统计信息的信息http://dev.mysql.com/doc/innodb-plugin/1.0/en/innodb-other-changes-statistics-estimation.html

因此,当您从表中更新、插入和删除时,您会更改索引统计信息。可能是 MySQL 服务器因为静态数据统计错误,选择了错误的索引。然而,这只是一个可能的根本原因的猜测。

现在让我们深入研究索引。有两个可能的索引可以使用主键索引和 insert_date 上的索引。 MySQL 使用了 insert_date 之一。请记住,在查询执行期间,MySQL 始终只能使用一个索引。我们来看看主键索引和insert_date索引的区别。

关于主键索引(又名聚集)的简单事实:

  1. 主键索引通常是包含数据行的 btree 结构,即它是包含日期的表。

关于二级索引(又称非聚集)的简单事实:

  1. 二级索引通常是一个 btree 结构,其中包含被索引的数据(索引中的列)和指向主键索引上数据行位置的指针。

这是一个微妙但很大的区别。

让我解释一下,当您阅读主键索引时,您正在阅读该表。该表也按主索引的顺序排列。因此,要找到一个值,我会搜索索引读取数据,即 1 操作。

当您读取二级索引时,您搜索索引找到指针,然后读取主键索引以根据指针查找数据。这实际上是 2 次操作,使得读取二级索引的操作成本是读取主键索引的两倍。

在您的情况下,因为它选择 insert_date 作为使用它的索引,所以只是为了进行联接而做的工作加倍。那是问题一。

现在,当您限制记录集时,它是查询执行的最后一部分。 MySQL 必须根据 ORDER BY 和 GROUP BY 条件对整个记录集进行排序(如果尚未排序),然后根据 LIMIT BY 部分获取所需的记录数并发送回。 MySQL 必须做很多工作来跟踪要发送的记录以及它在记录集中的位置等。 LIMIT BY 确实会影响性能,但我怀疑可能是导致读取的因素。

查看您的 GROUP BY,它是按 player_id。使用的索引是 insert_date。 GROUP BY 本质上是对您的记录集进行排序,但是因为它没有用于排序的索引(请记住,索引是按照其中包含的列的顺序排序的)。本质上,您是在询问 player_id 的排序/顺序,并且使用的索引是在 insert_date 上排序的。

这一步导致了文件排序问题,它本质上是读取从读取二级索引和主索引返回的数据(记住这两个操作),然后必须对它们进行排序。排序通常在磁盘上完成,因为它在内存中是一项非常昂贵的操作。因此,整个查询结果被写入磁盘并以非常慢的速度进行排序以获得您的结果。

通过删除 insert_date 索引,MySQL 现在将使用主键索引,这意味着数据是有序的(ORDER BY/GROUP BY)player_id 和 insert_date。这将消除读取二级索引然后使用指针读取主键索引(即表)的需要,并且由于数据已经排序,因此 MySQL 在应用查询的 GROUP BY 片段时几乎没有工作。

现在,如果您可以在删除索引后发布说明语句的结果,那么以下是一个有根据的猜测,我可能能够证实我的想法。因此,通过使用错误的索引,结果在磁盘上被排序以正确应用 LIMIT BY。删除 LIMIT BY 允许 MySQL 可能在内存中排序,因为它不必应用 LIMIT BY 并跟踪返回的内容。 LIMIT BY 可能导致创建临时表。再一次很难说没有看到语句之间的差异,即解释的输出。

希望这能让您更好地理解索引以及为什么它们是一把双刃剑。

【讨论】:

  • 使用 LIMIT 切换索引和连接顺序 - 这是我的大困惑,不应该发生在 IMO。查询使用 PRIMARY 加入 date_curr 然后 player_record,但添加 'LIMIT 1' 并使用 insert_date 加入 player_record 然后 date_curr。我提交了一个错误报告。使用 FORCE INDEX 效果很好;无需删除。我还发现,在 2012 年 5 月 30 日之后它起作用了。我的统计数据收集开始于 5 月 14 日,我认为少量的 insert_date 愚弄了它,但再次没有理由限制应该改变查询设置。感谢您让我再次查看索引。
  • 很好的答案,但是强制索引是要走的路:)
  • @KonstantinKrass 直到大索引无法有效地回答查询,然后您必须强制使用另一个索引。在某些时候索引会成为一个问题,因此调整索引比强制使用它更好。
  • @Namphibian 你是对的。总是强制索引表现不佳。然后需要进行一些重构才能使其正常工作。
【解决方案2】:

遇到了同样的问题。当我添加 FORCE INDEX (id) 时,它回到了几毫秒的查询,没有限制,同时产生相同的结果。

【讨论】:

    猜你喜欢
    • 2017-10-13
    • 1970-01-01
    • 1970-01-01
    • 2019-01-26
    • 2018-06-18
    • 1970-01-01
    • 2012-04-25
    • 1970-01-01
    • 2020-05-15
    相关资源
    最近更新 更多