【问题标题】:Index on a table not being used all the time未一直使用的表上的索引
【发布时间】:2017-10-20 10:57:48
【问题描述】:

我正在寻找一些关于 MySQL 表上的索引如何工作的见解,因为我遇到了一些我不理解的问题。

让我们从我正在使用的表开始:

mysql> SHOW CREATE TABLE channeldata\G
*************************** 1. row ***************************
       Table: channeldata
Create Table: CREATE TABLE `channeldata` (
  `channel_id` smallint(3) unsigned NOT NULL,
  `station_id` smallint(5) unsigned NOT NULL,
  `time` datetime NOT NULL,
  `reading` double NOT NULL DEFAULT '0',
  `average` double NOT NULL DEFAULT '0',
  `location_lat` double NOT NULL DEFAULT '0',
  `location_lon` double NOT NULL DEFAULT '0',
  `location_alt` double(8,3) DEFAULT '0.000',
  `quality` smallint(3) unsigned DEFAULT '0',
  PRIMARY KEY (`channel_id`,`station_id`,`time`),
  KEY `composite3` (`station_id`,`channel_id`,`quality`) USING BTREE,
  KEY `composite` (`channel_id`,`station_id`,`time`,`quality`) USING BTREE
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
/*!50100 PARTITION BY RANGE (YEAR(time))
(PARTITION p0 VALUES LESS THAN (2001) ENGINE = MyISAM,
 PARTITION p1 VALUES LESS THAN (2002) ENGINE = MyISAM,
 PARTITION p2 VALUES LESS THAN (2003) ENGINE = MyISAM,
 PARTITION p3 VALUES LESS THAN (2004) ENGINE = MyISAM,
 PARTITION p4 VALUES LESS THAN (2005) ENGINE = MyISAM,
 PARTITION p5 VALUES LESS THAN (2006) ENGINE = MyISAM,
 PARTITION p6 VALUES LESS THAN (2007) ENGINE = MyISAM,
 PARTITION p7 VALUES LESS THAN (2008) ENGINE = MyISAM,
 PARTITION p8 VALUES LESS THAN (2009) ENGINE = MyISAM,
 PARTITION p9 VALUES LESS THAN (2010) ENGINE = MyISAM,
 PARTITION p10 VALUES LESS THAN (2011) ENGINE = MyISAM,
 PARTITION p11 VALUES LESS THAN (2012) ENGINE = MyISAM,
 PARTITION p12 VALUES LESS THAN (2013) ENGINE = MyISAM,
 PARTITION p13 VALUES LESS THAN (2014) ENGINE = MyISAM,
 PARTITION p14 VALUES LESS THAN (2015) ENGINE = MyISAM,
 PARTITION p15 VALUES LESS THAN (2016) ENGINE = MyISAM,
 PARTITION p16 VALUES LESS THAN (2017) ENGINE = MyISAM,
 PARTITION p17 VALUES LESS THAN (2018) ENGINE = MyISAM) */
1 row in set (0.00 sec)

我正在运行查询以选择 2017 年 8 月/9 月/10 月的数据。“读数”在一天中均匀分布,并且始终以 10 分钟为边界(即 10:10:00、10:20: 00、10:30:00 等)从 2017 年 5 月起,每天的“阅读”数量相当一致,为 15.000。 P17 分区总共有超过 300 万个读数。

我需要帮助的查询如下所示:

SELECT 
        ROUND(`a`.`average`,2) `average`,
        UNIX_TIMESTAMP(`a`.`time`) * 1000 time,
        `a`.`station_id`
    FROM
        `argus`.`channeldata` PARTITION (p17) `a` 
    WHERE
        ((`a`.`station_id` = '3002' AND a.channel_id = '1') OR (`a`.`station_id` = '3004' AND a.channel_id = '1') OR [...] OR (`a`.`station_id` = '5052' AND a.channel_id = '1')) AND `a`.`time` BETWEEN "2017-08-17 00:00:00" AND "2017-10-13 23:59:59"  AND `a`.`quality` IN('1') ORDER BY `a`.`time` ASC;

这是格式化为清楚地显示WHERE 条件的查询。

SELECT 
        ROUND(`a`.`average`,2) `average`,
        UNIX_TIMESTAMP(`a`.`time`) * 1000 time,
        `a`.`station_id`
    FROM
        `argus`.`channeldata` PARTITION (p17) `a` 
    WHERE
        (     (`a`.`station_id` = '3002' AND a.channel_id = '1') 
           OR (`a`.`station_id` = '3004' AND a.channel_id = '1')
           OR [...]
           OR (`a`.`station_id` = '5052' AND a.channel_id = '1'))
     AND `a`.`time` BETWEEN "2017-08-17 00:00:00" AND "2017-10-13 23:59:59"  
     AND `a`.`quality` IN('1')
   ORDER BY `a`.`time` ASC;

为了获得一些指标,我开始从 4 周、5 周等间隔中选择读数。完成这些查询的执行时间大约为 4 到 5 秒,我在间隔中添加的天数越多,时间就会略有增加。但是,突然之间,执行时间出现了跳跃。在“BETWEEN”间隔中仅增加一天,执行时间几乎翻了两番,达到近 20 秒。

我在解释中运行了之前和之后的查询,结果是我不明白的。

间隔为BETWEEN "2017-08-18 00:00:00" AND "2017-10-13 23:59:59" EXPLAIN 看起来像这样:

+----+-------------+-------+-------+------------------------------+---------+---------+------+--------+-----------------------------+
| id | select_type | table | type  | possible_keys                | key     | key_len | ref  | rows   | Extra                       |
+----+-------------+-------+-------+------------------------------+---------+---------+------+--------+-----------------------------+
|  1 | SIMPLE      | a     | range | PRIMARY,composite3,composite | PRIMARY | 12      | NULL | 542026 | Using where; Using filesort |
+----+-------------+-------+-------+------------------------------+---------+---------+------+--------+-----------------------------+
1 row in set (0.00 sec)

将其增加一天到 BETWEEN "2017-08-17 00:00:00" AND "2017-10-13 23:59:59" 看起来像这样:

+----+-------------+-------+------+------------------------------+------+---------+------+---------+-----------------------------+
| id | select_type | table | type | possible_keys                | key  | key_len | ref  | rows    | Extra                       |
+----+-------------+-------+------+------------------------------+------+---------+------+---------+-----------------------------+
|  1 | SIMPLE      | a     | ALL  | PRIMARY,composite3,composite | NULL | NULL    | NULL | 3056618 | Using where; Using filesort |
+----+-------------+-------+------+------------------------------+------+---------+------+---------+-----------------------------+
1 row in set (0.00 sec)

那里发生了什么?为什么它突然不能使用主键/索引,而不是搜索行的子集,它必须搜索整个 300 万个分区。在旁注中,区间的确切位置并不重要。我也可以通过将间隔提前一个月来重现这个问题。

如果有帮助,在执行时间“跳转”之前返回的列是 525644,当我增加 1 天时的数字是 535004。

【问题讨论】:

  • 有多少百分比的数据有quality=1

标签: mysql indexing query-performance


【解决方案1】:

您的过滤条件是:

  1. 显式分区选择
  2. quality 上的相等匹配
  3. time 进行范围扫描
  4. station_idchannel_id 上的一组成对匹配项组合在一起。

您需要处理标准 2 和 3 的索引。将相等匹配列首先放在索引中,然后是范围扫描列,然后将索引与查询所需的其他列四舍五入以获得covering index

那个索引是(quality, time, station_id, channel_id, average)

为什么有效?查询计划器可以立即跳转到索引的第一个符合条件的行,因为它知道quality 和开始time 都需要。然后它可以按顺序扫描索引,进行成对匹配并检索average 列。 MySQL可以从索引满足整个查询,省去了很多跳回表取信息的时间,从而加快了查询速度。

您已经在(channel_id,station_id,time,quality) 上建立了索引。您可能希望在创建新索引时删除该索引,因为它看起来有类似的目的。

为什么查询计划器有时使用索引有时不使用?这取决于很多事情,主要是查询规划器对是否需要使用索引做更少的工作或只扫描表的估计。索引和列包含对基数的估计——数据项中不同值的数量。这些基数是估计值,有时非常不准确。您有分区:这可能会导致查询规划器以某种方式限制其选择。当查询计划器无法确定要做什么时的回退就是您得到的:全表扫描。

您的问题中提到的索引已经需要相当多的费力索引扫描才能满足查询;我猜当您更改日期戳范围时,查询计划程序会切换到全表扫描策略。这对于操作基于 DBMS 的软件的人来说是一个麻烦:随着应用程序的增长,有时查询计划器会突然转向一个新的、效率较低的计划。您需要掌握突然的性能变化并添加索引。

专业提示:与构建更好的索引相比,询问为什么查询计划器选择通常是徒劳的。 (除非你的开发工作是查询规划器。)

我建议使用五列索引。您的查询使用四列进行过滤,然后使用最后一列显示结果。在索引中包含所有五列意味着 MySQL 不必返回主表中索引找到的各个行。它可以单独满足从索引的查询,也就是说它可以从海量存储中顺序读取索引。在传统的旋转硬盘驱动器上,这意味着读取头不必在索引到表到索引之间来回滴答滴答来满足查询。它要快得多。它被称为covering index

专业提示:将BETWEEN 用于日期戳范围是错误的。而不是使用

  WHERE time BETWEEN '2017-08-17 00:00:00' AND '2017-10-13 23:59:59' 

使用这个。它在范围的末端更精确。它仍然会进行范围扫描。

  WHERE time >= '2017-08-17' 
    AND time <  '2017-10-13' + INTERVAL 1 DAY 

【讨论】:

  • 很有魅力,非常感谢。我很想了解为什么 MySQL 决定停止在我原来的问题中使用现有索引。我确实在某处读到,当它需要检查的行数约为总数的 30% 时,它突然停止使用索引,但我不知道这是否属实。另外,为什么要在 WHERE 子句中不使用平均值时将平均值添加到索引中?
【解决方案2】:

优化器有两种方法在一个范围内执行索引查询:

选项1,使用索引:

  1. 进入项目开头的索引。
  2. 向前扫描直到范围结束。过滤掉任何与其他 WHERE 条件不匹配的行。
  3. 对于每个项目,都可以访问数据以获取所需的其他列。这是对磁盘的随机读取——可能没有缓存,等等。

选项2,忽略索引,扫描数据。

  1. 扫描数据中的所有行,忽略任何不符合WHERE 条件的行。

执行一种方法和执行另一种方法之间的分界线取决于大量统计数据等。它通常在表格的 10% 到 30% 之间。您注意到边界处发生了很大的跳跃;这是因为统计数据并不“完美”。这种跳跃可能是好是坏。

附注。一旦你有了 Ollie 更好的索引,分区就不会给你带来任何性能。事实上,它可能会减慢查询速度。

DOUBLE(8 字节)用于 lat/lng/alt 是多余的。见my representation choices

DOUBLE(8,3)(还是8字节)更差;永远不要在FLOATDOUBLE 上使用(m,n)

平均值在数学上不正确。考虑保留一个总和和一个计数,然后计算SUM(sum)/SUM(count) 以获得正确的AVG

想以 10 倍的速度获得每周结果吗?每天在汇总表中建立和维护计数和总和。这会将数据缩小 1/144。然后通过汇总总和等来报告。汇总表上的discussion

【讨论】:

  • 感谢您的反馈,不幸的是表格和检索不是我的。使用的间隔是任意的,可以是 2 天,也可以是 3 个月。平均值实际上是在数据插入时计算的(它是包括当前读数在内的最后 X 个读数的平均值)并且是正确的——我猜他们只需要将它四舍五入到小数点后 2 位。您确定分区没有任何区别吗?引用的示例是在测试机器上,在实时服务器上,我们每年有数百万的读数可以追溯到 20 年前(如果间隔在分区边界上,我显然会考虑这一点)。
猜你喜欢
  • 2015-05-27
  • 2011-06-07
  • 2014-11-24
  • 2013-12-21
  • 2021-11-21
  • 1970-01-01
  • 2016-03-22
  • 2016-04-08
  • 2022-09-28
相关资源
最近更新 更多