岛屿和差距问题

Islands and Gaps Issue

背景故事:我有一个数据库,其中包含卡车中 driver 的数据点,其中还包含。在卡车上时,driver 可以有一个 'driverstatus'。我想做的是按 driver、卡车

对这些状态进行分组

截至目前,我已尝试使用 LAG/LEAD 来提供帮助。这样做的原因是,我可以判断 driver 状态更改何时发生,然后我可以将该行标记为具有该状态的最后日期时间。

这本身是不够的,因为我需要按状态和日期对状态进行分组。为此,我有 DENSE_RANK 之类的东西,但我无法正确处理 ORDER BY 子句。

这是我的测试数据,这是我许多人在排名中挣扎的一次尝试。

/****** Script for SelectTopNRows command from SSMS  ******/
DECLARE @SomeTable TABLE
(
    loginId VARCHAR(255),
    tractorId VARCHAR(255),
    messageTime DATETIME,
    driverStatus VARCHAR(2)
);

INSERT INTO @SomeTable (loginId, tractorId, messageTime, driverStatus)
VALUES('driver35','23533','2018-08-10 8:33 AM','2'),
('driver35','23533','2018-08-10 8:37 AM','2'),
('driver35','23533','2018-08-10 8:56 AM','2'),
('driver35','23533','2018-08-10 8:57 AM','1'),
('driver35','23533','2018-08-10 8:57 AM','1'),
('driver35','23533','2018-08-10 8:57 AM','1'),
('driver35','23533','2018-08-10 9:07 AM','1'),
('driver35','23533','2018-08-10 9:04 AM','1'),
('driver35','23533','2018-08-12 8:07 AM','3'),
('driver35','23533','2018-08-12 8:37 AM','3'),
('driver35','23533','2018-08-12 9:07 AM','3'),
('driver35','23533','2018-06-12 8:07 AM','2'),
('driver35','23533','2018-06-12 8:37 AM','2'),
('driver35','23533','2018-06-12 9:07 AM','2')
;
SELECT *, DENSE_RANK() OVER(PARTITION BY 
  loginId, tractorId, driverStatus 
ORDER BY messageTime ) FROM @SomeTable
;

我的最终结果理想情况下是这样的:

loginId tractorId   startTime           endTime            driverStatus
driver35    23533   2018-08-10 8:33 AM  2018-08-10 8:56 AM      2
driver35    23533   2018-08-10 8:57 AM  2018-08-10 9:07 AM      1
driver35    23533   2018-08-12 8:07 AM  2018-08-12 9:07 AM      3

非常感谢任何帮助。

SELECT 
  t.loginId, 
  t.tractorId, 
  startTime = MIN(messageTime), 
  endTime   = MAX(messageTime),
  driverStatus 
FROM @someTable t
GROUP BY loginId, tractorId, driverStatus
ORDER BY MIN(messageTime);

结果:

loginId        tractorId  startTime               endTime                 driverStatus
-------------- ---------- ----------------------- ----------------------- ------------
driver35       23533      2018-10-08 08:33:00.000 2018-10-08 08:56:00.000 2
driver35       23533      2018-10-08 08:57:00.000 2018-10-08 09:07:00.000 1
driver35       23533      2018-12-08 08:07:00.000 2018-12-08 09:07:00.000 3
WITH drivers_data AS
(
    SELECT *,
           row_num =     ROW_NUMBER()
                         OVER (PARTITION BY loginId,
                                            tractorId,
                                            CAST(messageTime AS date),
                                            driverStatus
                               ORDER BY messageTime),

           row_num_all = ROW_NUMBER()
                         OVER (PARTITION BY loginId,
                                            tractorId
                               ORDER BY messageTime),

           first_date =  FIRST_VALUE (messageTime)
                         OVER (PARTITION BY loginId,
                                            tractorId,
                                            CAST(messageTime AS date),
                                            driverStatus
                               ORDER BY messageTime),

           last_date =   LAST_VALUE (messageTime)
                         OVER (PARTITION BY loginId,
                                            tractorId,
                                            CAST(messageTime AS date),
                                            driverStatus
                               ORDER BY messageTime
                               ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
    FROM @t
)
SELECT loginId, tractorId, first_date, last_date, driverStatus
FROM drivers_data
WHERE row_num = 1
ORDER BY row_num_all;

输出:

+==========+===========+=====================+=====================+==============+
| loginId  | tractorId | first_date          | last_date           | driverStatus |
|==========|===========|=====================|=====================|==============|
| driver35 | 23533     | 2018-10-08 08:33:00 | 2018-10-08 08:56:00 | 2            |
|----------|-----------|---------------------|---------------------|--------------|
| driver35 | 23533     | 2018-10-08 08:57:00 | 2018-10-08 09:07:00 | 1            |
|----------|-----------|---------------------|---------------------|--------------|
| driver35 | 23533     | 2018-12-06 08:07:00 | 2018-12-06 09:07:00 | 2            |
|----------|-----------|---------------------|---------------------|--------------|
| driver35 | 23533     | 2018-12-08 08:07:00 | 2018-12-08 09:07:00 | 3            |
+----------+-----------+---------------------+---------------------+--------------+

我将尝试解释这里发生的事情:

  1. row_num 用于对受日期和驱动程序状态限制的行进行编号。我们需要铸造,因为我们需要没有时间的日期部分。
  2. row_num_all 这是关键属性,因为它最终允许我们按出现次数对行进行排序。 window 不受状态限制,因为我们需要对整个驾驶员数据进行编号。
  3. first_date FIRST_VALUE 对我们来说是一个方便的函数。它只是检索第一次出现的日期时间。
  4. last_date 假设最后一个日期我们需要 LAST_VALUE window 函数是正确的。但是使用它很棘手,需要更多解释。如您所见,我明确使用了特殊的框架 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING。但为什么?让我解释。让我们使用默认框架 获取日期 10/8/2018 和状态 2 的一部分输出。我们得到以下结果:
+==========+===========+=====================+=====================+==============+
| loginId  | tractorId | first_date          | last_date           | driverStatus |
|==========|===========|=====================|=====================|==============|
| driver35 | 23533     | 2018-10-08 08:33:00 | 2018-10-08 08:33:00 | 2            |
|----------|-----------|---------------------|---------------------|--------------|
| driver35 | 23533     | 2018-10-08 08:33:00 | 2018-10-08 08:37:00 | 2            |
|----------|-----------|---------------------|---------------------|--------------|
| driver35 | 23533     | 2018-10-08 08:33:00 | 2018-10-08 08:56:00 | 2            | 
+----------+-----------+---------------------+---------------------+--------------+

如您所见,最后一个日期不正确!发生这种情况是因为 LAST_VALUE 使用默认帧 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW- 这意味着最后一行始终是 window 中的 当前行 。这是引擎盖下发生的事情。创建了三个 windows。每行都有自己的 window。然后它从 window:

中检索最后一行

Window 第一行

+==========+===========+=====================+=====================+==============+
| loginId  | tractorId | first_date          | last_date           | driverStatus |
|==========|===========|=====================|=====================|==============|
| driver35 | 23533     | 2018-10-08 08:33:00 | 2018-10-08 08:33:00 | 2            |
+----------+-----------+---------------------+---------------------+--------------+

Window 第二行

+==========+===========+=====================+=====================+==============+
| loginId  | tractorId | first_date          | last_date           | driverStatus |
|==========|===========|=====================|=====================|==============|
| driver35 | 23533     | 2018-10-08 08:33:00 | 2018-10-08 08:33:00 | 2            |
|----------|-----------|---------------------|---------------------|--------------|
| driver35 | 23533     | 2018-10-08 08:33:00 | 2018-10-08 08:37:00 | 2            |
+----------+-----------+---------------------+---------------------+--------------+

Window 第三行

+==========+===========+=====================+=====================+==============+
| loginId  | tractorId | first_date          | last_date           | driverStatus |
|==========|===========|=====================|=====================|==============|
| driver35 | 23533     | 2018-10-08 08:33:00 | 2018-10-08 08:33:00 | 2            |
|----------|-----------|---------------------|---------------------|--------------|
| driver35 | 23533     | 2018-10-08 08:33:00 | 2018-10-08 08:37:00 | 2            |
|----------|-----------|---------------------|---------------------|--------------|
| driver35 | 23533     | 2018-10-08 08:33:00 | 2018-10-08 08:56:00 | 2            | 
+----------+-----------+---------------------+---------------------+--------------+

因此,解决方案是更改框架:我们需要的不是从开头移动到当前行,而是从当前行移动到结尾。所以,UNBOUNDED FOLLOWING 就意味着这个 - 当前 window.

的最后一行
  1. 接下来是WHERE row_num = 1。这很简单:因为所有行都有相同的关于第一个日期和最后一个日期的信息,我们只需要第一行。

  2. 最后的部分是ORDER BY row_num_all。这是您获得正确顺序的地方。

P.S.

  1. 您想要的相关输出不正确。 对于日期 8/10/18 8:57 AM 和状态 1,最后一个日期必须是 10/8/2018 9:07 AM - 而不是 10/8/2018 9:04 AM,如您所述。

  2. 还缺少日期 12/6/2018 和状态 2 的输出。

更新:

以下是 FIRST_VALUELAST_VALUE 工作原理的图示。

所有三个数字都有以下部分:

  1. 查询数据这是查询结果
  2. 原始查询原始源数据。
  3. Windows这些是计算的中间步骤。
  4. Frame 提及使用的框架。
  5. 绿格Window规格

这是幕后发生的事情:

  1. 首先,SQL 服务器为所有提到的字段创建分区。在图上是 partition 列。
  2. 每个分区可以有一个框架:默认或自定义。默认帧是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。这意味着该行在分区开始和当前行之间得到 window。如果您不提及框架,则使用默认框架。
  3. 每一帧为每一行创建 window。在图上,这些 windows 位于 row 1row 2 列中,并用颜色标记。行号对应row_num_all字段。
  4. 行仅在其 window 的范围内运行。

1。 FIRST_VALUE

要获得第一次约会,我们可以使用方便的 FIRST_VALUE window 功能。 如您所见,我们在这里使用默认框架。这意味着对于每一行,window 将位于 window 的开头和当前行之间。为了获得第一次约会,这正是我们所需要的。每行将从第一行获取值。第一个日期在 "first_date" 字段中。

2。 LAST_VALUE - 错误的框架

现在我们需要计算最后的日期。最后一个日期在分区的最后一行,所以我们可以使用 LAST_VALUE window 函数。 正如我之前提到的,如果我们不提及框架,则使用默认框架。如图所示,框架总是在当前行结束 - 这是 不正确的 ,因为我们需要最后 window 行的日期。 last_date 字段向我们显示了错误的结果 - 它反映了当前行的日期。

3。 LAST_VALUE - 正确的框架

要解决获取最后日期的问题,我们需要更改 LAST_VALUE 将在其上运行的框架:ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING。如您所见,现在每行的 window 位于当前行和分区末尾之间。在这种情况下 LAST_VALUE 将正确地从 window 的最后一行获取日期。现在 last_date 字段中的结果是正确的。

下面的解决方案在每个 loginID / tractorID 组合中识别每次岛屿开始(当 driverStatus 变化时),然后分配一个 "id" 数字给那个岛。

之后,只需 min/max 即可找到该岛的开始和结束时间。

答案:

select b.loginId
, b.tractorId
, min(b.messageTime) as startTime
, max(b.messageTime) as endTime
, b.driverStatus
from (
    select a.loginId
    , a.tractorId
    , a.messageTime
    , a.driverStatus
    , a.is_island_start_flg
    , sum(a.is_island_start_flg) over (partition by a.loginID, a.tractorID order by a.messageTime asc) as island_nbr --assigning the "id" number to the island
    from (
        select st.loginId
        , st.tractorId
        , st.messageTime
        , st.driverStatus
        , iif(lag(st.driverStatus, 1, st.driverStatus) over (partition by st.loginID, st.tractorId order by st.messageTime asc) = st.driverStatus, 0, 1) as is_island_start_flg --identifying start of island
        from @SomeTable as st
        ) as a
    ) as b
group by b.loginId
, b.tractorId
, b.driverStatus
, b.island_nbr --purposefully in the group by, to make sure each occurrence of a status is in final results
order by b.loginId asc
, b.tractorId asc
, min(b.messageTime) asc

当您遗漏样本数据的最后三个记录时(因为这不在问题的预期输出中,就像 JohnyL 所说),此查询会生成问题的准确输出。