【问题标题】:ORDER BY ... ASC is slow and "Using index condition"ORDER BY ... ASC 很慢并且“使用索引条件”
【发布时间】:2014-08-02 07:32:42
【问题描述】:

我有 2 张桌子:userpost

使用 show create table 语句:

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 语句执行了 2 个查询,结果很奇怪,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)

我认为重点是Using index condition,但我不知道为什么。如何改进我的数据库以获得更好的性能?

更新:

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 |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+-------------+

【问题讨论】:

标签: mysql sql database indexing sql-order-by


【解决方案1】:

这些是理解这种行为的相关事实:

  • 您正在使用使用聚集索引概念的 InnoDB。 对于您的特定情况,聚集索引的一个有趣的副作用是每个非主键索引也将隐式包含主键作为索引中的最后一列。无需为 (owner_id, post_id) 上的索引创建索引 - 您已经拥有它。

  • MySQL 无法以正确的方式解析非前导索引列上的范围条件()。相反,它只会在索引查找期间忽略它们,然后将 where 子句的这一部分应用为过滤器。这只是 MySQL 的一个限制,不能直接在 post_id = 900000 的位置开始扫描——其他数据库可以做到这一点。

  • 当您使用DESC 命令时,MySQL 将开始读取它找到的最大post_id 值的索引。然后它将应用您的过滤器post_id &gt; 900000。如果匹配,则返回该行。然后它继续到下一行,依此类推,直到找到 10 个匹配的行。但是,所有匹配的行都保证是索引扫描的开始位置。

  • 当您使用ASC 命令时,MySQL 开始读取另一端的索引,根据post_id &gt; 900000 检查该值,并且可能需要丢弃该行,因为post_id 低于该阈值。现在猜猜它需要以这种方式处理多少行才能找到与post_id &gt; 900000 匹配的第一行?这就是占用你时间的原因。

  • “使用索引条件”指的是索引条件下推:http://dev.mysql.com/doc/refman/5.6/en/index-condition-pushdown-optimization.html 我想说它应该适用于这两种情况。但是,它在 DESC 情况下并不那么相关,因为过滤器无论如何都不会删除任何行。在 ASC 的情况下,它非常相关,如果没有它,性能将是最差的。

如果你不想验证我的陈述,你可以

  • 增加/减少数值(900000),看看性能如何变化。较低的值应该使ASC 更快,同时保持DESC 也更快。

  • 将范围条件&gt;更改为&lt;,看看是否会反转ASC/DESC的性能行为。请记住,您可能需要将数字更改为某个较低的值才能真正看到性能差异。

一个人怎么可能知道?

http://use-the-index-luke.com/ 是我的指南,它解释了索引的工作原理。

【讨论】:

  • 很好的解释。我试图反转范围条件,性能也反转了。但是我得到了另一个奇怪的东西,如果我把owner_id = 20替换成owner_id &gt; 19 and owner_id &lt; 21,速度总是很快的,不管我用desc还是asc。能给我解释一下吗?
  • @JackCood 需要猜测 ;) 你能展示一下执行计划吗?
  • 是的,当然 :) SELECT * FROM mydb.post where post_id &lt; 600000 and owner_id = 20 order by post_id desc limit 10; => (0.484s)SELECT * FROM mydb.post where post_id &lt; 600000 and owner_id &gt; 19 and owner_id &lt; 21 order by post_id desc limit 10; => (0.001s)。我试了很多次,结果都差不多。
  • @JackCood 通过执行计划我的意思是EXPLAIN 输出:) 可能更好地添加到您的原始问题。
  • 你说“不需要在 (owner_id, post_id) 上建立索引——你已经有了它”,但我相信指向主键的隐式最后指针不能用于排序/范围。在我的情况下,当我在(owner_id, post_id) 上至少在 MySQL 5.5 上建立索引时,SELECT * FROM post where owner_id = 20 order by post_id desc limit 10 的速度要快得多。也许是因为最后一段仅用于指针,而不用于排序/范围?
【解决方案2】:

这与“使用索引条件”无关,而是 MySQL 如何使用 INDEX 及其查询引擎的工作原理。 MySQL 使用简单的查询分析器和优化器。

对于post_id &gt; 900000 and owner_id = 20,您可能会注意到它尝试使用键my_fk,这是一个“更大的索引”,因为它的大小为(64+32)*行。它从索引中找到所有owner_id = 20(是的,没有使用post_id。愚蠢的mysql)

在 MySQL 使用 BIG 和 HEAVIER 索引来定位您需要的所有行之后,它会再次查找以通过主键读取实际行(因为您执行 SELECT *),(这里需要更多 HDD 查找),并过滤使用post_id &gt; 900000 (SLOW) 得到的结果

order by post_id desc 的情况下,它运行得更快可能有很多原因。一个可能的原因是 InnoDB 缓存,插入最少的行比其他行更温暖,更容易访问。

post_id &gt; 900000 and owner_id &gt; 19 and owner_id &lt; 20 的情况下,MySQL 放弃了my_fk,因为对二级索引的范围扫描并不比对主索引的范围扫描好。

它只是使用 PK 找到 post_id 900000 的正确页面,并从那里执行 SEQUENCE READ,如果您的 InnoDB 页面没有碎片。 (假设您使用的是 AUTO_INCREMENT)扫描一些页面,并筛选出符合您需要的页面。

要做一个“优化”,(现在就做):不要使用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 可以只使用 INDEX 的顺序(而不是索引顺序),它会更快。

【讨论】:

  • 你的回答对我来说很有意义,所以我投了赞成票。但是有些部分我不是很清楚。 USING INDEX 你的意思是选择查询中的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我不知道如何计算索引的大小,你能给我解释一下吗?
  • 啊,大小是根据类型来的。
  • @Dennis Cheung 你的观点很好,但是覆盖索引并不总是可能的(如果数据列大于 1000 字节等),所以“不要使用 SELECT *”并不总是可行的.如果 owner_id 的基数很低(例如每个所有者的平均帖子非常低),对 PK 的顺序读取也可能是致命的,所以我同意这取决于许多因素。
【解决方案3】:

我不是 MySQL 专家,但我认为这两个查询都没有使用索引 - 除非您创建的索引没有告诉我们。 “使用索引条件”可能是 MySQL 实现 LIMIT 关键字方式的产物。

如果你在你的 post 表上放置一个由 (owner_id, post_id) 组成的索引,它将有助于这两个查询。在 MySQL 中,它应该类似于:

create index ix_post_userpost on post (owner_id, post_id)

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

【讨论】:

    猜你喜欢
    • 2011-02-22
    • 2023-02-01
    • 2011-04-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-24
    • 2013-04-30
    • 1970-01-01
    相关资源
    最近更新 更多