想象一下,我有一张这样的表:
CREATE TABLE `Alarms` (
`AlarmId` INT UNSIGNED NOT NULL AUTO_INCREMENT
COMMENT "32-bit ID",
`Ended` BOOLEAN NOT NULL DEFAULT FALSE
COMMENT "Whether the alarm has ended",
`StartedAt` TIMESTAMP NOT NULL DEFAULT 0
COMMENT "Time at which the alarm was raised",
`EndedAt` TIMESTAMP NULL
COMMENT "Time at which the alarm ended (NULL iff Ended=false)",
PRIMARY KEY (`AlarmId`),
KEY `Key4` (`StartedAt`),
KEY `Key5` (`Ended`, `EndedAt`)
) ENGINE=InnoDB;
现在,对于GUI,我想生成:
目的是向用户提供一个下拉框,他们可以从中选择日期以查看当天活动的警报(在之前或期间,期间或之后结束)。所以像这样:
+-----------------------------------+
| Choose day ▼ |
+-----------------------------------+
| 2017-12-03 (3 started) |
| 2017-12-04 (1 started, 2 ended) |
| 2017-12-05 (2 ended) |
| 2017-12-16 (1 started, 1 ended) |
| 2017-12-17 (1 started) |
| 2017-12-18 |
| 2017-12-19 |
| 2017-12-20 |
| 2017-12-21 (1 ended) |
+-----------------------------------+
我可能会对警报施加年龄限制,以便在一年之后存档/删除警报。这就是我们正在使用的规模。
我预计每天会有零至数万个警报。
我的第一个想法是相当简单的:
(
SELECT
COUNT(`AlarmId`) AS `NumStarted`,
NULL AS `NumEnded`,
DATE(`StartedAt`) AS `Date`
FROM `Alarms`
GROUP BY `Date`
)
UNION
(
SELECT
NULL AS `NumStarted`,
COUNT(`AlarmId`) AS `NumEnded`,
DATE(`EndedAt`) AS `Date`
FROM `Alarms`
WHERE `Ended` = TRUE
GROUP BY `Date`
);
这使用我的两个索引,加入类型为ref
,ref类型为const
,我很满意。我可以迭代结果集,将发现的非NULL
值转储到C ++ std::map<boost::gregorian::date, std::pair<size_t, size_t>>
(然后&#34;填补空白&#34;在没有警报开始或结束的日子里,但是从前几天开始活动。)
我投入工作的扳手是列表应该考虑基于位置的时区,但只有我的应用程序知道时区。出于逻辑原因,MySQL会话是故意SET time_zone = '+00:00'
,因此时间戳都以UTC格式启动。 (然后使用各种其他工具对历史时区执行任何必要的位置特定更正,同时考虑DST和诸如此类的东西。)对于应用程序的其余部分,这很好,但对于此特定查询,它会中断日期{{1} } ING。
也许我可以预先计算(在我的应用程序中)时间范围列表,并生成 2n GROUP
ed查询的大量查询(其中 n =&#34;天数&#34;要检查)并以这种方式获得UNION
和NumStarted
次数:
NumEnded
但是,当然,即使我将数据库限制为一年的历史警报,也要像730 -- Example assuming desired timezone is -05:00
--
-- 3rd December
(
SELECT
COUNT(`AlarmId`) AS `NumStarted`,
NULL AS `NumEnded`,
'2017-12-03' AS `Date`
FROM `Alarms`
-- Alarm started during 3rd December UTC-5
WHERE `StartedAt` >= '2017-12-02 19:00:00'
AND `StartedAt` < '2017-12-03 19:00:00'
GROUP BY `Date`
)
UNION
(
SELECT
NULL AS `NumStarted`,
COUNT(`AlarmId`) AS `NumEnded`,
'2017-12-03' AS `Date`
FROM `Alarms`
-- Alarm ended during 3rd December UTC-5
WHERE `EndedAt` >= '2017-12-02 19:00:00'
AND `EndedAt` < '2017-12-03 19:00:00'
GROUP BY `Date`
)
UNION
-- 4th December
(
SELECT
COUNT(`AlarmId`) AS `NumStarted`,
NULL AS `NumEnded`,
'2017-12-04' AS `Date`
FROM `Alarms`
-- Alarm started during 4th December UTC-5
WHERE `StartedAt` >= '2017-12-03 19:00:00'
AND `StartedAt` < '2017-12-04 19:00:00'
GROUP BY `Date`
)
UNION
(
SELECT
NULL AS `NumStarted`,
COUNT(`AlarmId`) AS `NumEnded`,
'2017-12-04' AS `Date`
FROM `Alarms`
-- Alarm ended during 4th December UTC-5
WHERE `EndedAt` >= '2017-12-03 19:00:00'
AND `EndedAt` < '2017-12-04 19:00:00'
GROUP BY `Date`
)
UNION
-- 5th December
-- [..]
d UNION
一样秒。我的蜘蛛般的感觉告诉我这是一个非常糟糕的主意。
我还能如何生成这些按时间分组的统计数据?或者这真的很愚蠢,我应该考虑解决阻止我在MySQL中使用 tzinfo 的问题?
必须适用于MySQL 5.1.73(CentOS 6)和MariaDB 5.5.50(CentOS 7)。
答案 0 :(得分:0)
UNION
方法实际上并不是一个可行的解决方案;你可以通过招聘一个临时表来实现同样的目标,而不需要灾难性的大型查询:
CREATE TEMPORARY TABLE `_ranges` (
`Start` TIMESTAMP NOT NULL DEFAULT 0,
`End` TIMESTAMP NOT NULL DEFAULT 0,
PRIMARY KEY (`Start`, `End`)
);
INSERT INTO `_ranges` VALUES
-- 3rd December UTC-5
('2017-12-02 19:00:00', '2017-12-03 19:00:00'),
-- 4th December UTC-5
('2017-12-03 19:00:00', '2017-12-04 19:00:00'),
-- 5th December UTC-5
('2017-12-04 19:00:00', '2017-12-05 19:00:00'),
-- etc.
;
-- Now the queries needed are simple and also quick:
SELECT
`_ranges`.`Start`,
COUNT(`AlarmId`) AS `NumStarted`
FROM `_ranges` LEFT JOIN `Alarms`
ON `Alarms`.`StartedAt` >= `_ranges`.`Start`
ON `Alarms`.`StartedAt` < `_ranges`.`End`
GROUP BY `_ranges`.`Start`;
SELECT
`_ranges`.`Start`,
COUNT(`AlarmId`) AS `NumEnded`
FROM `_ranges` LEFT JOIN `Alarms`
ON `Alarms`.`EndedAt` >= `_ranges`.`Start`
ON `Alarms`.`EndedAt` < `_ranges`.`End`
GROUP BY `_ranges`.`Start`;
DROP TABLE `_ranges`;
(此方法的灵感来自a DBA.SE post。)
请注意,有两个SELECT
- 原来的UNION
不再可能,因为temporary tables cannot be accessed twice in the same query。但是,由于我们已经引入了其他语句(CREATE
,INSERT
和DROP
),因此在这种情况下这似乎是一个没有实际意义的问题。
在这两种情况下,每一行代表我们请求的一个句点,第一列等于句点的“开始”部分(这样我们就可以在结果集中识别它)。
请务必根据需要在代码中使用异常处理,以确保在例程返回之前_ranges
为DROP
ped;虽然临时表是MySQL会话的本地表,但如果你之后继续使用该会话,那么你可能想要一个干净的状态,特别是如果要再次使用这个函数的话。
如果这仍然太重,例如因为你有很多时间段,CREATE TEMPORARY TABLE
本身因此会变得太大,或者因为多个语句不适合你的调用代码,或者因为你的用户没有没有权限创建和删除临时表,您必须依靠简单的GROUP BY
而不是DAY(Date)
,并确保用户在系统的时运行mysql_tzinfo_to_sql
tzdata 已更新。