计算事件表中的行,按时间范围分组,很多

时间:2018-01-09 14:52:00

标签: mysql group-by timestamp-with-timezone

想象一下,我有一张这样的表:

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;要检查)并以这种方式获得UNIONNumStarted次数:

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)。

1 个答案:

答案 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。但是,由于我们已经引入了其他语句(CREATEINSERTDROP),因此在这种情况下这似乎是一个没有实际意义的问题。

在这两种情况下,每一行代表我们请求的一个句点,第一列等于句点的“开始”部分(这样我们就可以在结果集中识别它)。

请务必根据需要在代码中使用异常处理,以确保在例程返回之前_rangesDROP ped;虽然临时表是MySQL会话的本地表,但如果你之后继续使用该会话,那么你可能想要一个干净的状态,特别是如果要再次使用这个函数的话。

如果这仍然太重,例如因为你有很多时间段,CREATE TEMPORARY TABLE本身因此会变得太大,或者因为多个语句不适合你的调用代码,或者因为你的用户没有没有权限创建和删除临时表,您必须依靠简单的GROUP BY而不是DAY(Date),并确保用户在系统的时运行mysql_tzinfo_to_sql tzdata 已更新。