按... ASC排序很慢且出现"Using index condition"

3

我有两张表:userpost

通过显示创建表语句:

CREATE TABLE `user` (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(20) CHARACTER SET latin1 NOT NULL,
  `create_date` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=59 DEFAULT CHARSET=utf8;

CREATE TABLE `post` (
  `post_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `owner_id` bigint(20) NOT NULL,
  `data` varchar(300) CHARACTER SET latin1 DEFAULT NULL,
  PRIMARY KEY (`post_id`),
  KEY `my_fk` (`owner_id`),
  CONSTRAINT `my_fk` FOREIGN KEY (`owner_id`) REFERENCES `user` (`user_id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1012919 DEFAULT CHARSET=utf8;

在执行带有ORDER BY语句的两个查询时,一切正常,但结果非常奇怪,ASC很慢,而DESC非常快。

SELECT sql_no_cache * FROM mydb.post where post_id > 900000 and owner_id = 20 order by post_id desc limit 10;
10 rows in set (0.00 sec)

SELECT sql_no_cache * FROM mydb.post where post_id > 900000 and owner_id = 20 order by post_id asc limit 10;
10 rows in set (0.15 sec)

然后我使用解释语句:

explain SELECT sql_no_cache * FROM mydb.post where post_id > 900000 and owner_id = 20 order by post_id desc limit 10;
+----+-------------+-------+------+---------------+-------+---------+-------+--------+-------------+
| id | select_type | table | type | possible_keys | key   | key_len | ref   | rows   | Extra       |
+----+-------------+-------+------+---------------+-------+---------+-------+--------+-------------+
|  1 | SIMPLE      | post  | ref  | PRIMARY,my_fk | my_fk | 8       | const | 239434 | Using where |
+----+-------------+-------+------+---------------+-------+---------+-------+--------+-------------+
1 row in set (0.01 sec)


explain SELECT sql_no_cache * FROM mydb.post where post_id > 900000 and owner_id = 20 order by post_id asc limit 10;
+----+-------------+-------+------+---------------+-------+---------+-------+--------+------------------------------------+
| id | select_type | table | type | possible_keys | key   | key_len | ref   | rows   | Extra                              |
+----+-------------+-------+------+---------------+-------+---------+-------+--------+------------------------------------+
|  1 | SIMPLE      | post  | ref  | PRIMARY,my_fk | my_fk | 8       | const | 239434 | Using index condition; Using where |
+----+-------------+-------+------+---------------+-------+---------+-------+--------+------------------------------------+
1 row in set (0.00 sec)

我认为重点在于“使用索引条件”,但我不知道原因。如何改进我的数据库以获得更好的性能?
更新:
explain SELECT * FROM mydb.post where post_id < 600000 and owner_id = 20 order by post_id desc limit 10;
+----+-------------+-------+------+---------------+-------+---------+-------+--------+-------------+
| id | select_type | table | type | possible_keys | key   | key_len | ref   | rows   | Extra       |
+----+-------------+-------+------+---------------+-------+---------+-------+--------+-------------+
|  1 | SIMPLE      | post  | ref  | PRIMARY,my_fk | my_fk | 8       | const | 505440 | Using where |
+----+-------------+-------+------+---------------+-------+---------+-------+--------+-------------+


explain SELECT * FROM mydb.post where post_id < 600000 and owner_id > 19 and owner_id < 21 order by post_id desc limit 10;
+----+-------------+-------+-------+---------------+---------+---------+------+--------+-------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows   | Extra       |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+-------------+
|  1 | SIMPLE      | post  | range | PRIMARY,my_fk | PRIMARY | 4       | NULL | 505440 | Using where |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+-------------+

已提交至 http://bugs.mysql.com/bug.php?id=73046。 - Morgan Tocker
@MorganTocker:听起来不错,谢谢! - Jack Cood
这个问题在MySQL 5.7.6中已经得到解决。 - Morgan Tocker
3个回答

15
这些是理解此行为的相关事实:
您正在使用InnoDB,它使用聚集索引的概念。对于您的特定情况,聚集索引的一个有趣的副作用是,每个非主键索引都会隐式地包含主键作为索引的最后一列。不需要对(owner_id, post_id)创建索引 - 您已经拥有了它。
MySQL无法正确解析非前导索引列上的范围条件(<,>)。相反,在索引查找期间将忽略它们,并在稍后将where子句的这部分应用为过滤器。这只是MySQL的一个限制,不能直接从post_id = 900000的位置开始扫描 - 其他数据库可以很好地做到这一点。
当您使用DESC顺序时,MySQL将从找到的最大post_id值开始读取索引。然后它会应用您的过滤器post_id>900000。如果匹配,则返回该行。然后继续到下一行,以此类推,直到找到10个匹配行。但是,所有匹配的行都保证位于索引扫描开始的位置。
当您使用ASC顺序时,MySQL从另一端开始读取索引,检查此值是否符合post_id>900000,并且可能需要丢弃该行,因为post_id低于该阈值。现在猜猜在找到第一行与post_id>900000匹配之前需要处理多少行?这就是耗费您时间的原因。
"使用索引条件"是指索引条件下推:http://dev.mysql.com/doc/refman/5.6/en/index-condition-pushdown-optimization.html我认为它应该适用于两种情况。但是,在DESC情况下,它不是很相关,因为过滤器不会删除任何行。在ASC情况下,它非常重要,如果没有它,性能将更差。
如果您想验证我的陈述,您可以增加/减少数字值(900000),看看性能如何变化。较低的值应该使ASC更快,同时保持DESC也很快。
将范围条件">"更改为"<",看看它是否颠倒了ASC / DESC的性能行为。请记住,您可能需要将数字更改为较低的值才能真正看到性能差异。
怎么可能知道呢? http://use-the-index-luke.com/是我解释索引工作原理的指南。

这是非常好的解释。我尝试了反转范围条件,性能也反转了。但我遇到了另一件奇怪的事情,如果我将 owner_id = 20 替换为 owner_id > 19 and owner_id < 21,速度总是非常快,无论我使用 desc 还是 asc。你能给我解释一下吗? - Jack Cood
@JackCood 需要猜测 ;) 你能展示执行计划吗? - Markus Winand
当然可以 :) SELECT * FROM mydb.post where post_id < 600000 and owner_id = 20 order by post_id desc limit 10; => (0.484秒). SELECT * FROM mydb.post where post_id < 600000 and owner_id > 19 and owner_id < 21 order by post_id desc limit 10; => (0.001秒). 我尝试了很多次,结果都差不多。 - Jack Cood
@JackCood,你的第二个问题的答案在最后的执行计划中非常明显:它正在使用主键索引(post_id上的)。因此,它会转到存储post_id = 60000的位置,并向下检查每一行以获取owner_id过滤器。如果它快速找到10篇符合该过滤器的文章,查询将很快返回。 - Markus Winand
我想知道为什么在前一个查询中使用外键,而在后一个查询中使用主键。我真的不理解。后者更快,但很丑陋,如果这是常见情况,为什么MySQL不进行优化呢?老实说,我从未见过这样的优化。 - Jack Cood
显示剩余6条评论

1
这段文字讲述了MySQL如何使用索引和查询引擎的工作原理,而“Using index condition”并不重要。MySQL使用简单的查询分析器和优化器。在“post_id>900000 and owner_id=20”的情况下,你会注意到它试图使用一个“BIGGER INDEX”——即大小为(64+32)*rows的键“my_fk”,从索引中查找所有“owner_id=20”的行(是的,“post_id”没有被使用,MySQL太愚蠢了)。在MySQL使用一个更大、更重的索引来定位所需的所有行之后,它通过主键进行另一个查找以读取实际行(因为你使用了“SELECT *”),然后通过使用“post_id>900000”来过滤结果(速度慢)。在“order by post_id desc”的情况下,它运行得更快可能有很多原因。其中一个可能的原因是InnoDB缓存,最近插入的行更容易访问。
post_id > 900000 and owner_id > 19 and owner_id < 20 的情况下,MySQL 放弃使用 my_fk,因为基于辅助索引的范围扫描比基于主键索引的范围扫描更差。

它只使用 PK 来定位 post_id 为 900000 的正确页面,然后从那里执行 顺序读取(如果你的 InnoDB 页面没有碎片),扫描一些页面,并过滤符合需要的内容。

要进行“优化”(现在就做):不要使用 SELECT *

要进行“过早优化”(不要这样做;现在不要这样做):通过 USE INDEX 提示 MySQL;创建一个包含所有所需列的精确索引。

很难说哪个更快,my_fkPK。因为性能因数据模式而异。如果 owner_id = 20 在你的表中占主导地位或常见,则直接使用 PK 可能更快。

如果你的表中owner_id=20不常见,那么my_fk会有所提升,因为在读取(post_id>900000+XXX)之前需要读取太多行数据。
-- 编辑:顺便尝试使用ORDER BY owner_id ASC, post_id ASC或DESC。如果MySQL可以直接使用索引的顺序(而不是对索引进行排序),那么速度会更快。

你的回答对我来说很有道理,所以我已经点赞了。但是有些部分我不是很清楚。USING INDEX 你是指在select查询中使用 USE INDEX 吗?you may notice it try to use key my_fk which is a "BIGGER INDEX" as it is sized in (64+32)*rows 我不知道如何计算索引的大小,你能解释一下吗? - Jack Cood
啊,大小基于类型。 - Jack Cood
@Dennis Cheung,你提出了很好的观点,但是覆盖索引并不总是可行的(如果数据列大于1000字节等),因此“不要使用SELECT *”并不总是切实可行的。此外,如果owner_id的基数很低(例如,每个所有者的平均帖子非常少),则PK上的顺序读取可能会导致灾难性后果,因此我同意这取决于许多因素。 - kenn

0
我不是MySQL专家,但我认为这两个查询都没有使用索引——除非你创建了我们不知道的索引。在MySQL中,“Using Index condition”可能是MySQL实现LIMIT关键字的一种产物。
如果你在post表上建立一个由(owner_id, post_id)组成的索引,它将有助于这两个查询。在MySQL中,它应该长这样:
create index ix_post_userpost on post (owner_id, post_id)

(我不能保证语法,因为我没有MySQL。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接