为啥 MySQL 不使用 EXPLAIN 中的索引?

Posted

技术标签:

【中文标题】为啥 MySQL 不使用 EXPLAIN 中的索引?【英文标题】:Why does MySQL not use the index from EXPLAIN?为什么 MySQL 不使用 EXPLAIN 中的索引? 【发布时间】:2013-12-17 11:31:42 【问题描述】:

我有一个简单的表格,目前有大约 1000 万行。 这是定义:

CREATE TABLE `train_run_messages` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `train_id` int(10) unsigned NOT NULL,
  `customer_id` int(10) unsigned NOT NULL,
  `station_id` int(10) unsigned NOT NULL,
  `train_run_id` int(10) unsigned NOT NULL,
  `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `type` tinyint(4) NOT NULL,
  `customer_station_track_id` int(10) unsigned DEFAULT NULL,
  `lateness_type` tinyint(3) unsigned NOT NULL,
  `lateness_amount` mediumint(9) NOT NULL,
  `lateness_code` tinyint(3) unsigned DEFAULT '0',
  `info_text` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `timestamp` (`timestamp`),
  KEY `lateness_amount` (`lateness_amount`),
  KEY `customer_timestamp` (`customer_id`,`timestamp`),
  KEY `trm_customer` (`customer_id`),
  KEY `trm_train` (`train_id`),
  KEY `trm_station` (`station_id`),
  KEY `trm_trainrun` (`train_run_id`),
  KEY `FI_trm_customer_station_tracks` (`customer_station_track_id`),
  CONSTRAINT `FK_trm_customer_station_tracks` FOREIGN KEY (`customer_station_track_id`) REFERENCES `customer_station_tracks` (`id`),
  CONSTRAINT `trm_customer` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
  CONSTRAINT `trm_station` FOREIGN KEY (`station_id`) REFERENCES `stations` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
  CONSTRAINT `trm_train` FOREIGN KEY (`train_id`) REFERENCES `trains` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
  CONSTRAINT `trm_trainrun` FOREIGN KEY (`train_run_id`) REFERENCES `train_runs` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=9928724 DEFAULT CHARSET=utf8;

我们有很多按 customer_id 和时间戳过滤的查询,因此我们为此创建了一个组合索引。

现在我有这个简单的查询:

SELECT * FROM `train_run_messages` WHERE `customer_id` = '5' AND `timestamp` >= '2013-12-01 00:00:57' AND `timestamp` <= '2013-12-31 23:59:59' LIMIT 0, 100 

在我们当前有大约 10M 条目的机器上,这个查询需要大约 16 秒,这在我看来有点长,因为这样的查询有一个索引。

让我们看看这个查询的解释输出:

+----+-------------+--------------------+------+-------------------------------------------    +--------------------+---------+-------+--------+-------------+
| id | select_type | table              | type | possible_keys                             | key                | key_len | ref   | rows       | Extra       |
+----+-------------+--------------------+------+-------------------------------------------+--------------------+---------+-------+--------+-------------+
|  1 | SIMPLE      | train_run_messages | ref  | timestamp,customer_timestmap,trm_customer | customer_timestamp | 4       | const | 551405     | Using where |
+----+-------------+--------------------+------+-------------------------------------------+--------------------+---------+-------+--------+-------------+

所以 mysql 告诉我它将使用 customer_timestamp 索引,很好!为什么查询仍然需要约 16 秒? 由于我并不总是信任 MySQL 查询分析器,让我们尝试使用强制索引:

SELECT * FROM `train_run_messages` USE INDEX (customer_timestamp) WHERE `customer_id` = '5' AND `timestamp` >= '2013-12-01 00:00:57' AND `timestamp` <= '2013-12-31 23:59:59' LIMIT 0, 100 

查询时间:0.079s!!

我:不解!

那么任何人都可以解释为什么 MySQL 显然没有使用它说它将从 EXPLAIN 输出中使用的索引吗?有什么方法可以证明它在执行真正的查询时真正使用了什么索引?

顺便说一句:这是慢日志的输出:

# Time: 131217 11:18:04
# User@Host: root[root] @ localhost [127.0.0.1]
# Query_time: 16.252878  Lock_time: 0.000168 Rows_sent: 100  Rows_examined: 9830711
SET timestamp=1387275484;
SELECT * FROM `train_run_messages` WHERE `customer_id` = '5' AND `timestamp` >= '2013-12-01 00:00:57' AND `timestamp` <= '2013-12-31 23:59:59' LIMIT 0, 100;

尽管它并没有具体说明它没有使用任何索引,但 Rows_examined 表明它会执行全表扫描。

那么这是否可以在不使用 USE INDEX 的情况下修复?我们使用 Propel 作为 ORM,目前无法在不手动编写查询的情况下使用 MySQL 特定的“USE INDEX”。

编辑: 这是 EXPLAIN 和 USE INDEX 的输出:

+----+-------------+--------------------+-------+--------------------+--------------------+---------+------+--------+-------------+
| id | select_type | table              | type  | possible_keys      | key                | key_len | ref  | rows   | Extra       |
+----+-------------+--------------------+-------+--------------------+--------------------+---------+------+--------+-------------+
|  1 | SIMPLE      | train_run_messages | range | customer_timestmap | customer_timestmap | 8       | NULL | 191264 | Using where |
+----+-------------+--------------------+-------+--------------------+--------------------+---------+------+--------+-------------+

【问题讨论】:

有多少个不同的客户 ID? 在train_run_messages表中只有customerId为5的条目。(系统是为多客户设计的,但是这个数据库中只有一个客户) 在这种情况下,它将忽略 customer_id 上的索引(根据经验,如果索引没有将记录缩小到大约 1/3 以下,那么它将被忽略)。但是,我希望时间戳可以缩小范围 是的,时间戳大大缩小了范围,从 2009 年到现在,这 1000 万条记录或多或少均匀分布。 【参考方案1】:

MySQL 有三个候选索引

(时间戳) (customer_id,时间戳) (customer_id)

你在问

`customer_id` = '5' AND `timestamp` BETWEEN ? AND ?

优化器已从统计信息中选择(customer_id, timestamp)

InnoDB 引擎的优化器依赖于在打开表时使用采样的统计信息。默认采样读取索引文件上的 8 页。

所以,我建议以下三点

    增加innodb_stats_sample_pages=64。 innodb_stats_sample_pages 的默认值为 8 页。 参考http://dev.mysql.com/doc/refman/5.5/en/innodb-parameters.html#sysvar_innodb_stats_sample_pages 删除冗余索引。以下索引就好了。目前只有customer_id = 5(你说) (时间戳) (customer_id) 运行OPTIMIZE TABLE train_run_messages 重新组织表格。 这减少了表和索引的大小,有时这使优化器更智能

【讨论】:

我对您的回答的理解是否正确,它没有解释为什么解释说它会使用 (customer_id,timestamp) 而实际上在执行查询时它不会使用它?这和表的统计和innodb_stats_sample_pages有关系吗? @Shyru 我想是的。与 Postgresql 的基于直方图的统计不同,InnoDB 依赖于抽样,innodb_stats_sample_pages 是准确统计的唯一方法。如果基于直方图的统计,Postgresql 将不会使用 (customer_id, timestamp) 他已经知道只有 5 个 customer_id。 但是为什么EXPLAIN说它会使用索引customer_timestamp但实际上它没有使用这个索引?? 根据我的经验,FORCE INDEX() 强制使用该索引。我猜USE INDEX 只是暗示不是力量。你为什么不用USE INDEX 来解释一下真的使用给定的索引。如果使用 FORCE INDEX 需要多长时间呢?顺便说一句,现在是上午 00:12。我无法跟踪。对不起,请告诉我测试结果。我也怀疑。 我尝试了您概述的步骤(但我没有删除时间戳索引,因为我们需要其他查询)并且不幸的是它没有任何区别。我还使用 USE INDEX (timestamp) 进行了尝试,这也显着减少了查询时间 (~0.06s),因为我们只有一个 customer_id。因此,如果 mysql 将使用它在 EXPLAIN 的可能键中输出的任何索引,它会运行得很快。我真的不明白这一点。 :-(【参考方案2】:

对我来说,将客户 ID 用引号括起来的最大问题是......例如 = '5'。通过这样做,它不能使用客户/时间戳索引,因为客户 ID 需要转换为字符串以匹配您的 '5' 而不是 = 5,您应该一切顺利。

【讨论】:

与数字列进行比较时不确定是否正确(它只会执行一次转换),但如果使用不带引号的数字与字符串列进行比较,这将是正确的。 @Kickstart,关键是......如果索引列是数字,并且它出于任何原因尝试将数据列转换为字符串以匹配 = '5',它不能利用索引也是如此,与使用 column = 5 的实际查询(具有相同的预期数据类型)相比,没有进行转换或错误解释来解决执行计划。 我相信它可以(并且在快速测试时当然可以),因为它只需将 '5' 转换为 5 一次,然后就可以使用索引。这是一个开销,但很小,不会随着行数的增加而增加。将未加引号的 5 与 char 列进行比较时会出现问题,其中许多不同的字符值可以计算为数字 5。在这种情况下,MySQL 必须将每一行转换为数字,因此无法使用任何索引。 @Kickstart,正确。当您提供 EQUALITY 时,我相信引擎正在执行“嘿……用户想要 '5'”。也许这作为一个字符串包括“5”、“50”、“500”、“528392”、“532901289”等。但会成为性能杀手并忽略索引能力。 @DRapp,我刚刚执行了 customer_id = 5 的查询,结果是一样的,约 16 秒的查询时间,所以我认为这里没有区别。

以上是关于为啥 MySQL 不使用 EXPLAIN 中的索引?的主要内容,如果未能解决你的问题,请参考以下文章

MySQL索引优化性能分析及explain的使用

mysql学习-explain中的extra

MySQL 专家:为啥 2 个查询给出不同的“解释”索引使用结果?

mysql explain的使用

Mysql索引explain执行计划

为啥 MySQL 中的这个查询不使用索引?