如何组合两行并计算MySQL中两个时间戳值之间的时差?

时间:2010-06-10 18:49:36

标签: sql mysql

我有一种情况,我确信这种情况非常普遍,我真的很困扰我无法弄清楚如何做或者搜索什么来找到相关的示例/解决方案。我对MySQL比较陌生(之前一直在使用MSSQL和PostgreSQL),我能想到的每一种方法都被MySQL缺乏的一些功能所阻挡。

我有一个“日志”表,它只是列出了许多不同的事件及其时间戳(存储为日期时间类型)。表中有很多数据和列与此问题无关,所以我们假设我们有一个这样的简单表:

CREATE TABLE log (  
  id INT NOT NULL AUTO_INCREMENT,  
  name VARCHAR(16),  
  ts DATETIME NOT NULL,  
  eventtype VARCHAR(25),  
  PRIMARY KEY  (id)  
)

假设某些行有一个eventtype ='start'而其他行有一个eventtype ='stop'。我想做的是以某种方式将每个“startrow”与每个“stoprow”结合起来并找到两者之间的时间差(然后将每个名称的持续时间相加,但这不是问题所在的位置)。每个“开始”事件应该在某个阶段发生相应的“停止”事件,然后发生“开始”事件,但由于数据收集器出现问题/错误/崩溃,可能会丢失一些事件。在这种情况下,我想在没有“伙伴”的情况下忽视这一事件。这意味着给定数据:

foo, 2010-06-10 19:45, start  
foo, 2010-06-10 19:47, start  
foo, 2010-06-10 20:13, stop

..我想忽略19:45开始事件而不是仅使用20:13停止事件作为停止时间获得两个结果行。

我试图以不同的方式加入表格,但对我来说关键问题似乎是找到一种方法来正确识别相应的“停止”事件到“start”事件给定的“名称” ”。这个问题与您在员工上下班的工作表并希望了解他们实际工作量有多少完全相同。我敢肯定必须有一个众所周知的解决方案,但我似乎无法找到它们......

6 个答案:

答案 0 :(得分:6)

我相信这可能是实现目标的更简单方法:

SELECT
    start_log.name,
    MAX(start_log.ts) AS start_time,
    end_log.ts AS end_time,
    TIMEDIFF(MAX(start_log.ts), end_log.ts)
FROM
    log AS start_log
INNER JOIN
    log AS end_log ON (
            start_log.name = end_log.name
        AND
            end_log.ts > start_log.ts)
WHERE start_log.eventtype = 'start'
AND end_log.eventtype = 'stop'
GROUP BY start_log.name

它应该运行得快得多,因为它消除了一个子查询。

答案 1 :(得分:1)

如果您不介意创建临时表*,那么我认为以下内容应该可以正常运行。我用120,000条记录测试了它,整个过程在6秒内完成。它有1,048,576条记录,在不到66秒的时间内就完成了 - 而这是在一台带有128MB RAM的旧Pentium III上:

*在MySQL 5.0(可能还有其他版本)中,临时表不能是真正的MySQL临时表,因为在同一查询中不能多次引用TEMPORARY表。见这里:

http://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html

相反,只需删除/创建一个普通表,如下所示:

DROP TABLE IF EXISTS `tmp_log`;
CREATE TABLE `tmp_log` (
    `id` INT NOT NULL,
    `row` INT NOT NULL,
    `name` VARCHAR(16),
    `ts` DATETIME NOT NULL,
    `eventtype` VARCHAR(25),
    INDEX `row` (`row` ASC),
    INDEX `eventtype` (`eventtype` ASC)
);

此表用于存储来自以下SELECT查询的已排序和编号的行列表:

INSERT INTO `tmp_log` (
    `id`,
    `row`,
    `name`,
    `ts`,
    `eventtype`
)
SELECT
    `id`,
    @row:=@row+1,
    `name`,
    `ts`,
    `eventtype`
FROM log,
(SELECT @row:=0) row_count
ORDER BY `name`, `id`;

上面的SELECT查询按名称对行进行排序,然后是id(只要启动事件出现在停止事件之前,就可以使用时间戳而不是id)。每行也都有编号。通过这样做,匹配的事件对总是彼此相邻,并且start事件的行号总是比stop事件的行id少一个。

现在从列表中选择匹配对:

SELECT
    start_log.row AS start_row,
    stop_log.row AS stop_row,
    start_log.name AS name,
    start_log.eventtype AS start_event,
    start_log.ts AS start_time,
    stop_log.eventtype AS stop_event,
    stop_log.ts AS end_time,
    TIMEDIFF(stop_log.ts, start_log.ts) AS duration
FROM
    tmp_log AS start_log
INNER JOIN tmp_log AS stop_log
    ON start_log.row+1 = stop_log.row
    AND start_log.name = stop_log.name
    AND start_log.eventtype = 'start'
    AND stop_log.eventtype = 'stop'
ORDER BY start_log.id;

完成后,删除临时表可能是个好主意:

DROP TABLE IF EXISTS `tmp_log`;row

<强>更新

您可以尝试以下想法,通过使用变量存储上一行的值,可以完全消除临时表和连接。它按名称对行进行排序,然后按时间戳排序,将所有值组合在一起,并按时间顺序排列每个组。我认为这应该确保所有相应的开始/停止事件彼此相邻。

SELECT id, name, start, stop, TIMEDIFF(stop, start) AS duration FROM (
    SELECT
        id, ts, eventtype,
        (@name <> name) AS new_name,
        @start AS start,
        @start := IF(eventtype = 'start', ts, NULL) AS prev_start,
        @stop  := IF(eventtype = 'stop',  ts, NULL) AS stop,
        @name  := name AS name
    FROM table1 ORDER BY name, ts
) AS tmp, (SELECT @start:=NULL, @stop:=NULL, @name:=NULL) AS vars
WHERE new_name = 0 AND start IS NOT NULL AND stop IS NOT NULL;

我不知道它与Ivar Bonsaksen的方法相比如何,但它在我的盒子上运行得相当快。

以下是我创建测试数据的方法:

CREATE TABLE  `table1` (
    `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(5),
    `ts` DATETIME,
    `eventtype` VARCHAR(5),
    PRIMARY KEY (`id`),
    INDEX `name` (`name`),
    INDEX `ts` (`ts`)
) ENGINE=InnoDB;

DELIMITER //
DROP PROCEDURE IF EXISTS autofill//
CREATE PROCEDURE autofill()
BEGIN
    DECLARE i INT DEFAULT 0;
    WHILE i < 1000000 DO
        INSERT INTO table1 (name, ts, eventtype) VALUES (
            CHAR(FLOOR(65 + RAND() * 26)),
            DATE_ADD(NOW(),
            INTERVAL FLOOR(RAND() * 365) DAY),
            IF(RAND() >= 0.5, 'start', 'stop')
        );
        SET i = i + 1;
    END WHILE;
END;
//
DELIMITER ;

CALL autofill();

答案 2 :(得分:1)

您可以更改数据收集器吗?如果是,则将group_id字段(带索引)添加到日志表中,并将start事件的id写入其中(在group_id中,start和end的id相同)。 然后就可以了

SELECT S.id, S.name, TIMEDIFF(E.ts, S.ts) `diff`
FROM `log` S
    JOIN `log` E ON S.id = E.group_id AND E.eventtype = 'end'
WHERE S.eventtype = 'start'

答案 3 :(得分:1)

试试这个。

select start.name, start.ts start, end.ts end, timediff(end.ts, start.ts) duration from (
    select *, (
        select id from log L2 where L2.ts>L1.ts and L2.name=L1.name order by ts limit 1
    ) stop_id from log L1
) start join log end on end.id=start.stop_id
where start.eventtype='start' and end.eventtype='stop';

答案 4 :(得分:0)

这个怎么样:

SELECT start_log.ts AS start_time, end_log.ts AS end_time
FROM log AS start_log
INNER JOIN log AS end_log ON (start_log.name = end_log.name AND end_log.ts > start_log.ts)
WHERE NOT EXISTS (SELECT 1 FROM log WHERE log.ts > start_log.ts AND log.ts < end_log.ts)
 AND start_log.eventtype = 'start'
 AND end_log.eventtype = 'stop'

这将找到每对行(别名为start_logend_log),其间没有任何事件,其中第一行始终是开始,而最后一行始终是停止。由于我们不允许中间事件,因此自然会排除不会立即停止的开始。

答案 5 :(得分:0)

我通过结合你的两个解决方案来实现它,但是查询效率不高,而且我认为可以更智能地省略那些不需要的行。

我现在得到的是:

SELECT y.name, 
       y.start, 
       y.stop, 
       TIMEDIFF(y.stop, y.start) 
  FROM (SELECT l.name, 
               MAX(x.ts) AS start, 
               l.ts AS stop 
          FROM log l 
          JOIN (SELECT t.name, 
                       t.ts 
                  FROM log t 
                 WHERE t.eventtype = 'start') x ON x.name = l.name 
                       AND x.ts < l.ts 
         WHERE l.eventtype = 'stop' 
      GROUP BY l.name, l.ts) y 
WHERE NOT EXISTS (SELECT 1 
                    FROM log AS d 
                   WHERE d.ts > y.start AND d.ts < y.stop AND d.name = y.name 
                         AND d.eventtype = 'stop')

当我包含WHERE NOT EXISTS条款时,限制为给定的“名称”,查询从大约0.5秒到大约14秒......表格会变得非常大,我担心这个小时多少小时将最终采用所有名称。我目前只有表中的2010年6月的数据(10天),现在是109888行。