我应该如何为具有两个范围条件的查询建立索引?

Posted

技术标签:

【中文标题】我应该如何为具有两个范围条件的查询建立索引?【英文标题】:How should I go about indexing for a query with two range conditions? 【发布时间】:2018-07-31 05:17:30 【问题描述】:

系列的下一篇……

CREATE TABLE `Alarms` (
  `AlarmId` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `DeviceId` BINARY(16) NOT NULL,
  `Code` BIGINT(20) UNSIGNED NOT NULL,
  `Ended` TINYINT(1) NOT NULL DEFAULT '0',
  `NaturalEnd` TINYINT(1) NOT NULL DEFAULT '0',
  `Pinned` TINYINT(1) NOT NULL DEFAULT '0',
  `Acknowledged` TINYINT(1) NOT NULL DEFAULT '0',
  `StartedAt` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
  `EndedAt` TIMESTAMP NULL DEFAULT NULL,
  `MarkedForDeletion` TINYINT(1) NOT NULL DEFAULT '0',
  PRIMARY KEY (`AlarmId`),
  KEY `Key1` (`Ended`,`Acknowledged`),
  KEY `Key2` (`Pinned`),
  KEY `Key3` (`DeviceId`,`Pinned`),
  KEY `Key4` (`DeviceId`,`StartedAt`,`EndedAt`),
  KEY `Key5` (`DeviceId`,`Ended`,`EndedAt`),
  KEY `Key6` (`MarkedForDeletion`,`DeviceId`,`Acknowledged`,`Ended`,`StartedAt`),
  KEY `Key7` (`MarkedForDeletion`,`DeviceId`,`Ended`,`Pinned`,`EndedAt`)
) ENGINE=INNODB;

This query is quite slow when the timestamps are chosen such that many rows are matched:

SELECT (((UNIX_TIMESTAMP(`StartedAt`)) DIV 900) * 900) AS `Period_TS`,
COUNT(`AlarmId`) AS `n`
FROM `Alarms`
WHERE `StartedAt` >= FROM_UNIXTIME(1518990000)  
AND `StartedAt` <  FROM_UNIXTIME(1518998400) 
AND `DeviceId` IN (
    UNHEX('00030000000000000000000000000000'),
    UNHEX('000300000000000000000000000181cd'),
    UNHEX('000300000000000000000000000e7cf6'),
    UNHEX('000300000000000000000000000e7cf7'),
    UNHEX('000300000000000000000000000f423f')
) AND `MarkedForDeletion` = FALSE
GROUP BY `Period_TS` ASC;

我相信这是因为我在两个字段(DeviceIdStartedAt)上混合了范围条件。

如果是这样,我能做些什么来解决这个问题?也许是触发使用索引合并的东西?

【问题讨论】:

我从来没有接到过太多使用它们的电话,但你试过index hints吗? (强制使用key4) 【参考方案1】:

mysql 不支持“跳过扫描”索引操作,因此您的诊断可能是正确的。您可以尝试使用union all 拆分逻辑:

SELECT . . .
FROM ((SELECT a.*
       FROM alarms a
       WHERE MarkedForDeletion = FALSE AND
             DeviceId = UNHEX('00030000000000000000000000000000') AND
             StartedAt >= FROM_UNIXTIME(1518990000) AND
             StartedAt <  FROM_UNIXTIME(1518998400)
      ) UNION ALL
      (SELECT a.*
       FROM alarms a
       WHERE MarkedForDeletion = FALSE AND
             DeviceId = UNHEX('000300000000000000000000000181cd') AND
             StartedAt >= FROM_UNIXTIME(1518990000) AND
             StartedAt <  FROM_UNIXTIME(1518998400)
      ) UNION ALL
      . . .
     ) a
GROUP BY `Period_TS` ASC;

对于此查询,您需要一个前三列为(MarkedForDeletion, DeviceId, StartedAt) 的索引。

【讨论】:

有希望 - “使用索引条件;使用 where” 为每个联合(并且记录它正在使用 Key3Key4USE INDEX,尽管我确实添加了新的您建议的索引,它也接受USE INDEX)-尽管匹配了150万行时,PRIMARY最终仍会花费20多秒,即使我删除了GROUP BY的东西并在上面拍了LIMIT 100-也许我想做的只是不可行。我还担心当在...中添加越来越多的设备时,那串 UNION 会如何扩展…… 奇怪的是,一个基本的SELECT COUNT(AlarmId) AS n FROM Alarms WHERE MarkedForDeletion = FALSE AND DeviceId = UNHEX(...) AND StartedAt &gt;= ... AND StartedAt &lt; ... 是一个缓慢的“使用位置;使用索引”超过 150 万行(运行大约 1-2 秒) - 我原以为新索引会立即实现 @LightnessRacesinOrbit 。 . .这取决于有多少场比赛。您也可以使用count(*) 而不是计算特定列。 我想我希望它只是“知道”它有 N 行匹配那些满足索引的标准:) COUNT(*)COUNT(AlarmId) 似乎在给我相同的表现 基于我这周遇到的麻烦(请参阅我的最后两个问题和这个问题!)我想我可能需要考虑说服管理层雇用真正了解他们的人'正在做来工作并帮助我几天。 :( 太糟糕了,我不能传真给你 :)【参考方案2】:

IN 介于= 和“范围”之间。所以,我对问题的标题提出了质疑。两个范围几乎是不可能优化的; IN 加上一个范围有一些优化的机会。

基于

WHERE `StartedAt` >= FROM_UNIXTIME(1518990000)  
AND   `StartedAt` <  FROM_UNIXTIME(1518998400) 
AND `DeviceId` IN (
    UNHEX('00030000000000000000000000000000'),
    UNHEX('000300000000000000000000000181cd'),
    UNHEX('000300000000000000000000000e7cf6'),
    UNHEX('000300000000000000000000000e7cf7'),
    UNHEX('000300000000000000000000000f423f')
) AND `MarkedForDeletion` = FALSE

我会提供 2 个索引,让优化器决定使用哪个:

INDEX(MarkedForDeletion, StartedAt, DeviceId)
INDEX(MarkedForDeletion, DeviceId, StartedAt)

一些较新版本的 MySQL/MariaDB 可以跨越并利用 second 索引中的所有 3 列。在所有版本中,任一索引的前 2 列都使其成为候选。选择可能是由统计数据驱动的,可能(也可能不是)是“正确”的选择。

由于AlarmId 不能是NULL,请使用模式:COUNT(*)

进行该更改后,我的每个索引都“覆盖”,从而额外提升了性能。

【讨论】:

只需使用您在***.com/a/49000336/560648 上提供的第一个索引重新运行我的查询,它的速度要快得多。再说一次,我想这部分归功于运气,因为我并没有只用那个指数来涵盖所有的可能性。同样讽刺的是,现在需要相互交叉引用 Stack Overflow 答案的“覆盖索引”...... 评论其他答案

以上是关于我应该如何为具有两个范围条件的查询建立索引?的主要内容,如果未能解决你的问题,请参考以下文章

如何为两个查询条件执行单个 Prepared Statement

MySQL如何为表字段添加索引

在Postgres中加入两个表后如何为具有相同名称的列提供别名

如何为单个If语句执行多个条件

具有许多包含列的 SQL Server 范围索引

Laravel 4,如何为具有关系的表创建种子数据?