向MySQL查询添加限制子句会大大减慢它的速度

时间:2012-12-12 18:57:38

标签: mysql performance limit

我正在尝试解决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包含来自当前星期之前的event_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 |

2 个答案:

答案 0 :(得分:8)

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

因此,基于explain语句中的证据,您可以看到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结构,即它包含日期的表。
  2. 关于二级索引(又名非群集)的简单事实:

    1. 辅助索引通常是一个btree结构,它包含被索引的数据(索引中的列)和指向主键索引上数据行位置的指针。
    2. 这是一个微妙但很大的区别。

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

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

      在你的情况下,因为它选择了insert_date作为使用它的索引,它只是为了进行连接而做了两倍的工作。那是问题一个。

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

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

      此步骤导致filesort问题,该问题基本上是从读取二级索引和主索引(请记住2个操作)返回的数据,然后必须对它们进行排序。排序通常在磁盘上完成,因为它在内存中是非常昂贵的操作。因此,整个查询结果被写入磁盘并且分类很慢,以便获得结果。

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

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

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

答案 1 :(得分:1)

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