MySQL 只有在使用 ORDER BY 字段 DESC 和 LIMIT 时查询才慢
MySQL query is slow only when using ORDER BY field DESC and LIMIT
概述
我是 运行 MySQL 5.7.30-33,我遇到了一个问题,似乎 MySQL 在 运行 时使用了错误的索引一个问题。我使用现有查询获得了 3 秒的查询时间。但是,仅通过更改 ORDER BY、删除 LIMIT 或强制使用 USE INDEX,我可以获得 0.01 秒的查询时间。不幸的是,我需要坚持使用我的原始查询(它已融入应用程序),所以如果这种差异可以在 schema/indexing.
中得到解决,那就太好了
设置/问题
我的table结构如下:
CREATE TABLE `referrals` (
`__id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`systemcreated` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`referrerid` mediumtext COLLATE utf8mb4_unicode_ci,
`referrersiteid` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
... lots more mediumtext fields ...
PRIMARY KEY (`__id`),
KEY `systemcreated` (`systemcreated`,`referrersiteid`,`__id`)
) ENGINE=InnoDB AUTO_INCREMENT=53368 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED
table 只有 ~55k 行,但非常宽,因为一些字段包含巨大的 BLOB:
mysql> show table status like 'referrals'\G;
*************************** 1. row ***************************
Name: referrals
Engine: InnoDB
Version: 10
Row_format: Compressed
Rows: 45641
Avg_row_length: 767640
Data_length: 35035897856
Max_data_length: 0
Index_length: 3653632
Data_free: 3670016
Auto_increment: 54008
Create_time: 2020-12-12 12:46:14
Update_time: 2020-12-12 17:50:28
Check_time: NULL
Collation: utf8mb4_unicode_ci
Checksum: NULL
Create_options: row_format=COMPRESSED
Comment:
1 row in set (0.00 sec)
我客户的应用程序使用此查询 table,不幸的是,这不能轻易更改:
SELECT *
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc
limit 16;
这导致查询时间约为 3 秒。
解释看起来像这样:
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | referrals | NULL | index | systemcreated | PRIMARY | 4 | NULL | 32 | 5.56 | Using where |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
请注意,它使用主键进行查询而不是 systemcreated
索引。
实验 1
如果我将查询更改为使用 ASC 而不是 DESC:
SELECT *
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id asc
limit 16;
然后用了0.01秒,EXPLAIN看起来是一样的:
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | referrals | NULL | index | systemcreated | PRIMARY | 4 | NULL | 32 | 5.56 | Using where |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
实验 2
如果我更改查询以坚持使用 ORDER BY __id DESC,但删除 LIMIT:
SELECT *
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc;
然后它也需要 0.01 秒,解释如下:
+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
| 1 | SIMPLE | referrals | NULL | range | systemcreated | systemcreated | 406 | NULL | 2086 | 11.11 | Using index condition; Using filesort |
+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
实验 3
或者,如果我强制原始查询使用 systemcreated
索引,那么它也会给出 0.01 秒的查询时间。这是解释:
mysql> explain SELECT *
FROM referrals USE INDEX (systemcreated)
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc
limit 16;
+----+-------------+--------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
| 1 | SIMPLE | referrals | NULL | range | systemcreated | systemcreated | 406 | NULL | 2086 | 11.11 | Using index condition; Using filesort |
+----+-------------+--------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
实验 4
最后,如果我使用原来的 ORDER BY __id DESC LIMIT 16 但 select 更少的字段,那么它也 returns 在 0.01 秒内!解释如下:
mysql> explain SELECT field1, field2, field3, field4, field5
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc
limit 16;
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | referrals | NULL | index | systemcreated | PRIMARY | 4 | NULL | 32 | 5.56 | Using where |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
总结
所以唯一表现不佳的组合是 ORDER BY __id DESC LIMIT 16
。
我 认为 我的索引设置正确。我通过 systemcreated
和 referrersiteid
字段查询,并按 __id 排序,所以我有一个定义为 (systemcreated, referrersiteid, __id) 的索引,但是 MySQL 似乎仍在使用 PRIMARY 键。
有什么建议吗?
"Avg_row_length: 767640";很多 MEDIUMTEXT
。一行限制在8KB左右;溢出进入“非记录”块。读取这些块需要额外的磁盘命中。
SELECT *
将达到所有那些胖柱。总数将约为 50 次读取(每次 16KB)。这需要时间。
(Exp 4) SELECT a,b,c,d
运行 更快,因为它不需要每行获取所有 ~50 个块。
你的二级索引,(systemcreated
,referrersiteid
,__id
), --只有第一列有用。这是因为 systemcreated LIKE 'xxx%'
。这是一个“运行ge”。一旦命中了一个运行ge,剩下的索引就失效了。除了...
“索引提示”(USE INDEX(...)
) 今天可能有所帮助,但明天当数据分布发生变化时可能会使情况变得更糟。
如果不能去掉LIKE
中的通配符,推荐这两个索引:
INDEX(systemcreated)
INDEX(referrersiteid)
真正的加速可以通过将查询翻转过来来实现。也就是说,首先找到 16 个 id,然后再去寻找所有那些笨重的列:
SELECT r2... -- whatever you want
FROM
(
SELECT __id
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc
limit 16
) AS r1
JOIN referrals r2 USING(__id)
ORDER BY __id DESC -- yes, this needs repeating
并保留您拥有的 3 列二级索引。即使它必须扫描超过 16 行才能找到所需的 16 行,但它的体积要小得多。这意味着子查询(“derived table”)将适度快速。那么外部查询仍然会有 16 次查找——可能需要读取 16*50 个块。读取的总块数还是会少很多。
ASC
和 DESC
在 ORDER BY
上几乎没有明显的区别。
为什么优化器选择PK而不是看似更好的二级索引? PK 可能 是最好的,特别是当 16 行位于 table 的 'end'(DESC)时。但如果它必须扫描整个 table 而找不到 16 行,那将是一个糟糕的选择。
同时,通配符测试使二级索引仅部分有用。优化器根据不充分的统计数据做出决定。有时感觉就像抛硬币。
如果你使用我的由内而外的重构,那么我推荐以下两个复合索引——优化器可以在它们之间做出半智能、半正确的选择,用于派生 table:
INDEX(systemcreated, referrersiteid, __id),
INDEX(referrersiteid, systemcreated, __id)
它会继续说“filesort”,但别担心;它只对 16 行进行排序。
而且,请记住,SELECT *
会影响性能。 (虽然你可能无法解决这个问题。)
概述
我是 运行 MySQL 5.7.30-33,我遇到了一个问题,似乎 MySQL 在 运行 时使用了错误的索引一个问题。我使用现有查询获得了 3 秒的查询时间。但是,仅通过更改 ORDER BY、删除 LIMIT 或强制使用 USE INDEX,我可以获得 0.01 秒的查询时间。不幸的是,我需要坚持使用我的原始查询(它已融入应用程序),所以如果这种差异可以在 schema/indexing.
中得到解决,那就太好了设置/问题
我的table结构如下:
CREATE TABLE `referrals` (
`__id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`systemcreated` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`referrerid` mediumtext COLLATE utf8mb4_unicode_ci,
`referrersiteid` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
... lots more mediumtext fields ...
PRIMARY KEY (`__id`),
KEY `systemcreated` (`systemcreated`,`referrersiteid`,`__id`)
) ENGINE=InnoDB AUTO_INCREMENT=53368 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED
table 只有 ~55k 行,但非常宽,因为一些字段包含巨大的 BLOB:
mysql> show table status like 'referrals'\G;
*************************** 1. row ***************************
Name: referrals
Engine: InnoDB
Version: 10
Row_format: Compressed
Rows: 45641
Avg_row_length: 767640
Data_length: 35035897856
Max_data_length: 0
Index_length: 3653632
Data_free: 3670016
Auto_increment: 54008
Create_time: 2020-12-12 12:46:14
Update_time: 2020-12-12 17:50:28
Check_time: NULL
Collation: utf8mb4_unicode_ci
Checksum: NULL
Create_options: row_format=COMPRESSED
Comment:
1 row in set (0.00 sec)
我客户的应用程序使用此查询 table,不幸的是,这不能轻易更改:
SELECT *
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc
limit 16;
这导致查询时间约为 3 秒。
解释看起来像这样:
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | referrals | NULL | index | systemcreated | PRIMARY | 4 | NULL | 32 | 5.56 | Using where |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
请注意,它使用主键进行查询而不是 systemcreated
索引。
实验 1
如果我将查询更改为使用 ASC 而不是 DESC:
SELECT *
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id asc
limit 16;
然后用了0.01秒,EXPLAIN看起来是一样的:
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | referrals | NULL | index | systemcreated | PRIMARY | 4 | NULL | 32 | 5.56 | Using where |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
实验 2
如果我更改查询以坚持使用 ORDER BY __id DESC,但删除 LIMIT:
SELECT *
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc;
然后它也需要 0.01 秒,解释如下:
+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
| 1 | SIMPLE | referrals | NULL | range | systemcreated | systemcreated | 406 | NULL | 2086 | 11.11 | Using index condition; Using filesort |
+----+-------------+-------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
实验 3
或者,如果我强制原始查询使用 systemcreated
索引,那么它也会给出 0.01 秒的查询时间。这是解释:
mysql> explain SELECT *
FROM referrals USE INDEX (systemcreated)
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc
limit 16;
+----+-------------+--------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
| 1 | SIMPLE | referrals | NULL | range | systemcreated | systemcreated | 406 | NULL | 2086 | 11.11 | Using index condition; Using filesort |
+----+-------------+--------------+------------+-------+---------------+---------------+---------+------+------+----------+---------------------------------------+
实验 4
最后,如果我使用原来的 ORDER BY __id DESC LIMIT 16 但 select 更少的字段,那么它也 returns 在 0.01 秒内!解释如下:
mysql> explain SELECT field1, field2, field3, field4, field5
FROM referrals
WHERE `systemcreated` LIKE 'XXXXXX%'
AND `referrersiteid` LIKE 'XXXXXXXXXXXX%'
order by __id desc
limit 16;
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | referrals | NULL | index | systemcreated | PRIMARY | 4 | NULL | 32 | 5.56 | Using where |
+----+-------------+-------------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
总结
所以唯一表现不佳的组合是 ORDER BY __id DESC LIMIT 16
。
我 认为 我的索引设置正确。我通过 systemcreated
和 referrersiteid
字段查询,并按 __id 排序,所以我有一个定义为 (systemcreated, referrersiteid, __id) 的索引,但是 MySQL 似乎仍在使用 PRIMARY 键。
有什么建议吗?
"Avg_row_length: 767640";很多
MEDIUMTEXT
。一行限制在8KB左右;溢出进入“非记录”块。读取这些块需要额外的磁盘命中。SELECT *
将达到所有那些胖柱。总数将约为 50 次读取(每次 16KB)。这需要时间。(Exp 4)
SELECT a,b,c,d
运行 更快,因为它不需要每行获取所有 ~50 个块。你的二级索引,(
systemcreated
,referrersiteid
,__id
), --只有第一列有用。这是因为systemcreated LIKE 'xxx%'
。这是一个“运行ge”。一旦命中了一个运行ge,剩下的索引就失效了。除了...“索引提示”(
USE INDEX(...)
) 今天可能有所帮助,但明天当数据分布发生变化时可能会使情况变得更糟。如果不能去掉
LIKE
中的通配符,推荐这两个索引:INDEX(systemcreated) INDEX(referrersiteid)
真正的加速可以通过将查询翻转过来来实现。也就是说,首先找到 16 个 id,然后再去寻找所有那些笨重的列:
SELECT r2... -- whatever you want FROM ( SELECT __id FROM referrals WHERE `systemcreated` LIKE 'XXXXXX%' AND `referrersiteid` LIKE 'XXXXXXXXXXXX%' order by __id desc limit 16 ) AS r1 JOIN referrals r2 USING(__id) ORDER BY __id DESC -- yes, this needs repeating
并保留您拥有的 3 列二级索引。即使它必须扫描超过 16 行才能找到所需的 16 行,但它的体积要小得多。这意味着子查询(“derived table”)将适度快速。那么外部查询仍然会有 16 次查找——可能需要读取 16*50 个块。读取的总块数还是会少很多。
ASC
和 DESC
在 ORDER BY
上几乎没有明显的区别。
为什么优化器选择PK而不是看似更好的二级索引? PK 可能 是最好的,特别是当 16 行位于 table 的 'end'(DESC)时。但如果它必须扫描整个 table 而找不到 16 行,那将是一个糟糕的选择。
同时,通配符测试使二级索引仅部分有用。优化器根据不充分的统计数据做出决定。有时感觉就像抛硬币。
如果你使用我的由内而外的重构,那么我推荐以下两个复合索引——优化器可以在它们之间做出半智能、半正确的选择,用于派生 table:
INDEX(systemcreated, referrersiteid, __id),
INDEX(referrersiteid, systemcreated, __id)
它会继续说“filesort”,但别担心;它只对 16 行进行排序。
而且,请记住,SELECT *
会影响性能。 (虽然你可能无法解决这个问题。)