【问题标题】:Get time difference between Log records获取日志记录之间的时间差
【发布时间】:2021-07-18 08:59:14
【问题描述】:

我有一个记录错误状态的日志表。我想提取日志从 OPEN (OldStatus) 更改为 FIXED 或 REQUEST CLOSE (NewStatus) 所花费的时间。现在,我的查询查看日志的最大值和最小值,它不会产生我想要的结果。例如,bug #1 在 2020-01-01 的 2 小时内修复,然后在 2020-12-12 的 3 小时内重新打开(OldStatus)并获得 REQUEST CLOSE(NewStatus)。我希望查询结果返回两行,其中包含自最近打开时间以来修复错误所花费的日期和小时数。

这是数据:

CREATE TABLE Log (
    BugID int,
    CurrentTime timestamp,
    Person varchar(20),
    OldStatus varchar(20),
    NewStatus varchar(20)
);

INSERT INTO Log (BugID, CurrentTime, Person, OldStatus, NewStatus)
VALUES (1, '2020-01-01 00:00:00', 'A', 'OPEN', 'In Progress'),
       (1, '2020-01-01 00:00:01', 'A', 'In Progress', 'REVIEW In Progress'),
       (1, '2020-01-01 02:00:00', 'A', 'In Progress', 'FIXED'),
       (1, '2020-01-01 06:00:00', 'B', 'OPEN', 'In Progress'),
       (1, '2020-01-01 00:00:00', 'B', 'In Progress', 'REQUEST CLOSE')

SELECT DATEDIFF(HOUR, start_time, finish_time) AS Time_Spent_Min
FROM (
    SELECT  BugId, 
            MAX(CurrentTime) as finish_time, 
            MIN(CurrentTime) as start_time
            FROM Log
            WHERE (OldStatus = 'OPEN' AND NewString = 'In Progress') OR NewString = 'FIXED' 
) AS TEMP

实际数据如下:

仅供参考@Charlieface

【问题讨论】:

  • 您的查询不适用于您的示例数据,您缺少几个引用的列。
  • CurrentTime timestamp 您不能将timestamp 数据类型用于日期和时间。那是你的实际桌子吗?
  • 在 Microsoft SQL Server 上,timestamprowversion 的同义词,因此您不能使用它来插入 datetime 值。 CurrentTime 列的实际数据类型是什么?
  • 这就是所谓的“差距和孤岛”问题
  • 下次截屏时,不要包含敏感列而不是编辑它们。如果您将其发布为格式化表格而不是图像,或者发布指向重新创建此数据集的小提琴的链接(您应该第一次这样做),那么我们可以明确地对其进行排序。

标签: sql sql-server tsql


【解决方案1】:

这是一种孤岛问题

有很多解决方案,这里是一个:

  • 我们需要为OPEN -> In Progress的每个岛分配一个分组ID。我们可以使用加窗条件COUNT 来获取每个起点的分组编号。
  • 要对终点进行分组,我们需要使用LAG 分配上一行的NewStatus,然后在此基础上再执行一个条件COUNT
  • 然后我们只需按 BugId 和我们计算的分组进行分组并返回开始和结束时间
WITH IslandStart AS (
    SELECT *,
        COUNT(CASE WHEN OldStatus = 'OPEN' AND NewStatus = 'In Progress' THEN 1 END)
          OVER (PARTITION BY BugID ORDER BY CurrentTime ROWS UNBOUNDED PRECEDING) AS GroupStart,
        LAG(NewStatus) OVER (PARTITION BY BugID ORDER BY CurrentTime) AS Prev_NewStatus
    FROM Log l
),
IslandEnd AS (
    SELECT *,
        COUNT(CASE WHEN Prev_NewStatus IN ('CLAIM FIXED', 'REQUEST CLOSE') THEN 1 END)
          OVER (PARTITION BY BugID ORDER BY CurrentTime ROWS UNBOUNDED PRECEDING) AS GroupEnd
    FROM IslandStart l
)
SELECT
    BugId, 
    MAX(CurrentTime) as finish_time, 
    MIN(CurrentTime) as start_time,
    DATEDIFF(minute, MIN(CurrentTime), MAX(CurrentTime)) AS Time_Spent_Min
FROM IslandEnd l
WHERE GroupStart = GroupEnd + 1
GROUP BY
  BugId,
  GroupStart;

注意事项:

  • timestamp 不代表实际日期和时间,而是使用 datetimedatetime2
  • 如果OPEN -> In Progress 并不总是孤岛的第一行,您可能需要调整COUNT 条件

【讨论】:

  • 感谢您的帮助。但是,它并没有像我预期的那样工作。请参考我在帖子中添加的图片:)
  • 那么究竟哪些行将代表每个岛屿?
  • (OPEN -> In Progress) 应该是岛的第一行。 (??? -> REQUEST CLOSE) 或 (??? -> FIXED) 应该是岛的最后一行
  • 您的查询似乎将 NewStatus = 'OPEN' 作为岛屿的最后一行,而我想要它做的是将 NewStatus = 'REQUEST CLOSE' OR NewStatus = 'CLAIM FIXED' 作为岛屿的最后一排。我放了一张新图片,仅供参考。抱歉命名混乱;/
  • 那么Claim Fixed -> OPEN 不属于任何岛屿的行会发生什么情况?
【解决方案2】:

这里有一些竞争因素:

  1. 您应该使用SmallDateTimeDateTime2DateTimeOffset 类型的列来存储日志中的实际时间,这些类型允许使用DateDiff()DateAdd() 和其他日期/基于时间的比较逻辑,其中Timestamp 旨在用作货币令牌,您可以使用它来确定一条记录是否比另一条记录更新,您不应该尝试使用它来确定实际 时间

  2. 您没有解释预期的工作流程,我们只能假设流程是 [OPEN]=>[In Progress]=>[CLAIM FIXED]。也没有提到“进行中”,我们认为这是一个临时状态。这里实际发生的是,这种结构实际上只能告诉您在“进行中”状态所花费的时间,这可能符合您的需求,因为这是实际工作所花费的时间,但重要的是要认识到我们没有首先知道何时将错误更改为“打开”,除非也记录了该错误,但我们需要查看数据来解释这一点。

  3. 您的示例数据集没有涵盖足够的组合,因此您不会注意到,一旦您添加了 1 个以上的错误,现有逻辑就会失败。更重要的是,您要求计算小时数,但您的示例数据仅显示变化分钟数,并且根本没有错误完成的示例。

    • 如果没有一组真实的数据进行测试,您会发现很难调试您的逻辑,并且在针对更大的数据集执行此操作之前很难接受它确实有效。编写脚本场景会有所帮助,就像您在此处的帖子一样,但您应该创建数据以反映该脚本。
    • 您在示例中使用'FIXED',但在查询中使用'CLAIM FIXED',那么它是哪一个?

第 1 步:结构

CurrentTime 的数据类型更改为基于日期时间 列。您的应用程序逻辑可能会推动这里的需求。如果您的系统是基于云的或国际化的,那么您可能会从使用DateTimeOffset 而不必转换为UTC 中看到好处,否则如果您不需要在日志中进行高精度计时,则使用SmallDateTime 是很常见的记录。

  • 许多 ORM 和应用程序框架将允许您将基于 DateTime 的列配置为并发令牌,您完全需要一个。如果您不喜欢使用较低精度的并发值,那么您可以将两列并排放置,以比较两条记录之间的时间差,我们需要使用基于 DateTime 的 类型。
  • 在日志的情况下,我们很少允许或期望编辑日志,如果您的日志是只读的,那么可能根本不需要并发令牌,特别是如果您只使用并发令牌来确定并发个人记录的编辑。

注意:您应该考虑为 Status 概念使用枚举或 FK。在您的示例数据集中,'In Progerss' 已经存在拼写错误,使用数字比较状态可能会带来一些性能优势,但它有助于防止拼写错误,尤其是在任何应用程序逻辑中使用 FK 或查找列表时。

第 2 步:示例数据

如果要求是计算记录之间花费的小时数,那么我们需要创建一些显示几个小时差异的简单示例,然后添加一些相同错误的示例被打开,修复,然后重新打开。

  • 错误 #1 在 2020 年 1 月 1 日 2 小时内修复,然后在 2020 年 12 月 12 日重新打开并在 3 小时内修复

下表显示了已知的数据状态和预期的小时数,我们需要再添加一些数据故事来验证最终查询是否处理了明显的边界条件,例如多个错误和重叠日期

BUG # Time Previous State New State Hrs In Progress
1 2020-01-01 08:00:00 OPEN In Progress
1 2020-01-01 10:00:00 In Progress FIXED (2 hrs)
1 2020-12-10 09:00:00 FIXED OPEN
1 2020-12-12 9:30:00 OPEN In Progress
1 2020-12-12 12:30:00 In Progress FIXED (3 hrs)
2 2020-03-17 11:15:00 OPEN In Progress
2 2020-03-17 14:30:00 In Progress FIXED (3.25 hrs)
3 2020-08-22 10:00:00 OPEN In Progress
3 2020-08-22 16:30:00 In Progress FIXED (6.5 hrs)

第三步:查询

这里要注意的是,'In Progress' 实际上是要查询的重要状态。我们真正想要的是查看OldStatus'In Progress' 的所有行,并且我们希望将该行链接到具有相同BugID 的最新记录之前这一行并且 NewStatus 等于 'In Progress'

上表中有趣的是,并非所有预期的小时数都是整数(整数),这使得使用DateDiff 有点棘手,因为它只计算边界变化,而不是总小时数。为了突出这一点,看看接下来的两个查询,第一个代表 59 分钟,另一个只有 2 分钟:

SELECT DateDiff(HOUR, '2020-01-01 08:00:00', '2020-01-01 08:59:00') -- 0 (59m)
SELECT DateDiff(HOUR, '2020-01-01 08:59:00', '2020-01-01 09:01:00') -- 1 (1m)

但是,SQL 结果显示第一个查询为0 小时,但第二个查询返回1 小时。那是因为它只比较HOUR 列,实际上根本没有做 time 值的减法。

要解决这个问题,我们可以使用 MINUTEMI 作为日期部分参数并将结果除以 60。

SELECT CAST(ROUND(DateDiff(MI, '2020-01-01 08:00:00', '2020-01-01 08:59:00')/60.0,2) as Numeric(10,2)) -- 0.98
SELECT CAST(ROUND(DateDiff(MI, '2020-01-01 08:59:00', '2020-01-01 09:01:00')/60.0,2) as Numeric(10,2)) -- 0.03

您可以通过计算模数来选择以其他方式对其进行格式化,以获得整数而不是分数的分钟,但这超出了本文的范围,了解DateDiff的局限性是重要的更进一步。

有多种方法可以关联同一个表中的先前记录,如果您需要记录中的其他值,那么您可以使用带有子查询的连接来从之前的所有记录中返回 TOP 1当前一个,您可以使用窗口查询或CROSS APPLY 来执行嵌套查找。以下使用CROSS APPLY,这是所有 RDBMS 的标准,但我觉得它使 MS SQL 查询非常干净:

SELECT [Fixed].BugID, [start_time], [Fixed].[CurrentTime] as [finish_time]
     , DATEDIFF(MI, [start_time], [Fixed].[CurrentTime]) / 60 AS Time_Spent_Hr 
     , DATEDIFF(MI, [start_time], [Fixed].[CurrentTime]) % 60 AS Time_Spent_Min 
FROM Log as Fixed
CROSS APPLY (SELECT MAX(CurrentTime) AS start_time 
             FROM Log as Started
             WHERE Fixed.BugID = Started.BugID 
               AND Started.NewStatus = 'In Progress'
               AND CurrentTime < Fixed.CurrentTime) as Started
WHERE Fixed.OldStatus = 'In Progress'

你可以玩这个小提琴:http://sqlfiddle.com/#!18/c408d4/3 然而结果表明:

BugID start_time finish_time Time_Spent_Hr Time_Spent_Min
1 2020-01-01T08:00:00Z 2020-01-01T10:00:00Z 2 0
1 2020-12-12T09:30:00Z 2020-12-12T12:30:00Z 3 0
2 2020-03-17T11:15:00Z 2020-03-17T14:30:00Z 3 15
3 2020-08-22T10:00:00Z 2020-08-22T16:30:00Z 6 30

【讨论】:

    【解决方案3】:

    如果我假设每一个“打开”后跟一个“固定”在下一次打开之前,那么你基本上可以使用lead()来解决这个问题。

    此版本取消透视数据,因此您可以在同一行中“打开”和“固定”:

    select l.*, datediff(hour, currenttime, fixed_time)
    from (select v.*,
                 lead(v.currenttime) over (partition by v.bugid order by v.currenttime) as fixed_time
          from log l cross apply
               (values (bugid, currentTime, oldStatus),
                       (bugid, currentTime, newStatus)
               ) v(bugid, currentTime, status)
          where v.status in ('OPEN', 'FIXED')
         ) l
    where status = 'OPEN';
    

    Here 是一个 dbfiddle,它使用与您的解释兼容的数据。 (您的样本数据不正确。)

    【讨论】:

      猜你喜欢
      • 2011-05-17
      • 2013-09-08
      • 1970-01-01
      • 1970-01-01
      • 2014-02-17
      • 1970-01-01
      • 2012-01-12
      • 1970-01-01
      • 2011-08-12
      相关资源
      最近更新 更多