【问题标题】:Why does using this index key for my simple mysql query increase query time so much?为什么对我的简单 mysql 查询使用这个索引键会增加查询时间这么多?
【发布时间】:2019-12-12 14:31:48
【问题描述】:

我试图了解我的 InnoDB 表上以下两个查询之间查询时间的巨大差异:

SELECT * 
FROM   db_telemetry.monitor_data 
WHERE  monitor_id = 6 
       AND created_at > '2019/11/14' 
       AND created_at < '2019/11/29'; 

4317 行在 37.672 秒内返回

SELECT * 
FROM   db_telemetry.monitor_data USE INDEX(ix_monitor_data_created_at) 
WHERE  monitor_id = 6 
       AND created_at > '2019/11/14' 
       AND created_at < '2019/11/29'; 

4317 行在 0.110 秒内返回

根据EXPLAIN,第一个(慢速)查询中的优化器选择monitor_id 作为其索引键。根据我的阅读,这很奇怪,因为 monitor_id 的基数相对较低(见下文)

我的桌子:

SHOW CREATE TABLE monitor_data

CREATE TABLE `monitor_data` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  `monitor_id` int(11) NOT NULL DEFAULT '0',
  `vbattery` float DEFAULT NULL,
  `rssi` float DEFAULT NULL,
  `ecio` float DEFAULT NULL,
  `tboard` float DEFAULT NULL,
  `txbytes` float DEFAULT NULL,
  `rxbytes` float DEFAULT NULL,
  `satelite_count` float DEFAULT NULL,
  `gps_fix` float DEFAULT NULL,
  `drive_space_remaining` float DEFAULT NULL,
  `other` text,
  `daq_reachable` tinyint(1) DEFAULT NULL,
  `monitor_reachable` tinyint(1) DEFAULT NULL,
  `clock_reset_flag` tinyint(1) DEFAULT NULL,
  `site_key` varchar(50) DEFAULT NULL,
  `internal_temp` float DEFAULT NULL,
  `vin` float DEFAULT NULL,
  `webrelay_reachable` tinyint(1) DEFAULT NULL,
  `daq_current_time` datetime DEFAULT NULL,
  `webrelay_current_time` datetime DEFAULT NULL,
  `latitude` float DEFAULT NULL,
  `longitude` float DEFAULT NULL,
  `speed` float DEFAULT NULL,
  PRIMARY KEY (`id`,`monitor_id`),
  KEY `monitor_id` (`monitor_id`),
  KEY `ix_monitor_data_site_key` (`site_key`),
  KEY `ix_monitor_data_created_at` (`created_at`),
  CONSTRAINT `monitor_data_ibfk_1` FOREIGN KEY (`monitor_id`) REFERENCES `monitors` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10839466 DEFAULT CHARSET=latin1

它的索引:

SHOW INDEX FROM monitor_data

Table           Non_unique  Key_name                    Seq_in_index    Column_name  Cardinality
------------------------------------------------------------------------------------------------
monitor_data    0           PRIMARY                     1               id           11311240               
monitor_data    0           PRIMARY                     2               monitor_id   11311240       
monitor_data    1           monitor_id                  1               monitor_id   110                
monitor_data    1           ix_monitor_data_site_key    1               site_key     28137          
monitor_data    1           ix_monitor_data_created_at  1               created_at   11311240           

Sub_part and Packed all NULL
Index_type all BTREE
Collation all 'A'

这是在具有 20GB 通用 SSD 的 AWS RDS t2.small 实例上运行的 MySQL 版本 5.6.40。

如果我只使用monitor_id 条件:

SELECT * 
FROM   db_telemetry.monitor_data 
WHERE  monitor_id = 6; 

0.078 秒内返回 274324 行

如果我只使用created_at 条件:

SELECT * 
FROM   db_telemetry.monitor_data 
WHERE  created_at > '2019/11/14' 
       AND created_at < '2019/11/29'; 

0.109 秒内返回 202976 行

所以,问题:

  1. 为什么优化器默认选择monitor_id 作为索引,我的架构是否可能存在问题,需要USE INDEX()
  2. 由于两个索引都是孤立的,因此将数据集减少到相似的 # 行为什么使用monitor_id 的多条件查询要慢得多 索引?

注意:我观察到某些较小的日期范围,优化器会翻转以选择 ix_monitor_data_created_at

【问题讨论】:

  • 除了格式化查询,我更喜欢 SHOW CREATE TABLE;我只是觉得两者都更容易理解。 - 这是 MyISAM 吗?为什么?
  • @Strawberry,这是 InnoDB,我更新了问题
  • 什么版本的mysql?
  • @PeterHe 版本 5.6.40
  • 在 mysql 8.0 之前,mysql innodb 不会持久化统计信息。自服务器启动以来,它仅在运行时内置内存统计信息,因此如果您的数据出现偏差,它可能会非常不准确。不准确的统计数据会导致次优的执行计划。在您的情况下,使用索引提示不是问题。但是如果这是您的重要查询并且经常使用,您可以尝试将索引monitor_id 转换为复合索引以包含列create_at。 mysql 应该自动为您的查询使用这个索引

标签: mysql


【解决方案1】:
PRIMARY KEY (`id`,`monitor_id`),

idAUTO_INCREMENT 时没有意义。也许与PRIMARY KEY(id)唯一 区别在于您允许id 的重复值。 (但您必须明确设置 id 才能获得 dup。)无论哪种方式,PK 都与数据“聚集”在一起,并且数据按 id 排序。

对于查询,你需要这个复合索引:

INDEX(monitor_id, created_at)

为什么优化器选择了“错误”的索引?很多可能的原因,但主要是因为它没有足够的统计数据。时间差异很大的另一个可能原因是...

表格中插入的行的顺序是什么?大概是“按时间顺序”?也就是说,该日期范围的行彼此“接近”,从而“快速”地使用该索引。同时,通过monitor_id 向上看意味着在桌子上跳来跳去。

我的复合索引解决了所有问题,方法是在 (6, '2019/11/14') 处插入索引的 BTree,然后向前扫描,直到所有 4317 个 index 行都完全正确成立。同时,它通过数据(通过id)获取SELECT *

另一个问题...您可能按所示顺序运行了 4 个查询,并从“冷”缓存(buffer_pool)开始。也就是说,第一个查询有 4317 次磁盘读取的开销。 (注意:在 HDD 上大约需要 43.17 秒。)然后另一个 SELECTs 找到了所有缓存。

所以...在运行计时测试时,请运行两次查询。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-17
    • 2016-04-05
    • 1970-01-01
    相关资源
    最近更新 更多