我正在尝试解决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 |
答案 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个操作。
当您读取二级索引时,搜索索引查找指针然后读取主键索引以根据指针查找数据。这基本上是两个操作,使得读取二级索引的操作比读取主键索引的成本高两倍。
在你的情况下,因为它选择了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)
时,它返回到查询的几毫秒,没有限制,同时产生相同的结果。