7

我正在尝试解决 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 |
4

2 回答 2

10

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

因此,根据解释语句中的证据,您可以看到 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 可能导致创建临时表。再一次很难说没有看到语句之间的差异,即解释的输出。

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

于 2012-12-14T06:55:44.727 回答
1

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

于 2016-05-08T13:00:17.190 回答