我应该如何为具有两个范围条件的查询建立索引?
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;
我相信这是因为我在两个字段(DeviceId
和 StartedAt
)上混合了范围条件。
如果是这样,我能做些什么来解决这个问题?也许是触发使用索引合并的东西?
【问题讨论】:
我从来没有接到过太多使用它们的电话,但你试过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” 为每个联合(并且记录它正在使用Key3
或 Key4
和 USE 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 >= ... AND StartedAt < ...
是一个缓慢的“使用位置;使用索引”超过 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