岛屿和差距问题
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 |
+----------+-----------+---------------------+---------------------+--------------+
我将尝试解释这里发生的事情:
- row_num 用于对受日期和驱动程序状态限制的行进行编号。我们需要铸造,因为我们需要没有时间的日期部分。
- row_num_all 这是关键属性,因为它最终允许我们按出现次数对行进行排序。 window 不受状态限制,因为我们需要对整个驾驶员数据进行编号。
- first_date
FIRST_VALUE
对我们来说是一个方便的函数。它只是检索第一次出现的日期时间。
- 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.
的最后一行
接下来是WHERE row_num = 1
。这很简单:因为所有行都有相同的关于第一个日期和最后一个日期的信息,我们只需要第一行。
最后的部分是ORDER BY row_num_all
。这是您获得正确顺序的地方。
P.S.
您想要的相关输出不正确。
对于日期 8/10/18 8:57 AM
和状态 1
,最后一个日期必须是 10/8/2018 9:07 AM
- 而不是 10/8/2018 9:04 AM
,如您所述。
还缺少日期 12/6/2018
和状态 2
的输出。
更新:
以下是 FIRST_VALUE
和 LAST_VALUE
工作原理的图示。
所有三个数字都有以下部分:
- 查询数据这是查询结果
- 原始查询原始源数据。
- Windows这些是计算的中间步骤。
- Frame 提及使用的框架。
- 绿格Window规格
这是幕后发生的事情:
- 首先,SQL 服务器为所有提到的字段创建分区。在图上是
partition
列。
- 每个分区可以有一个框架:默认或自定义。默认帧是
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
。这意味着该行在分区开始和当前行之间得到 window。如果您不提及框架,则使用默认框架。
- 每一帧为每一行创建 window。在图上,这些 windows 位于
row 1
至 row 2
列中,并用颜色标记。行号对应row_num_all
字段。
- 行仅在其 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 所说),此查询会生成问题的准确输出。
背景故事:我有一个数据库,其中包含卡车中 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 | +----------+-----------+---------------------+---------------------+--------------+
我将尝试解释这里发生的事情:
- row_num 用于对受日期和驱动程序状态限制的行进行编号。我们需要铸造,因为我们需要没有时间的日期部分。
- row_num_all 这是关键属性,因为它最终允许我们按出现次数对行进行排序。 window 不受状态限制,因为我们需要对整个驾驶员数据进行编号。
- first_date
FIRST_VALUE
对我们来说是一个方便的函数。它只是检索第一次出现的日期时间。 - 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.
接下来是
WHERE row_num = 1
。这很简单:因为所有行都有相同的关于第一个日期和最后一个日期的信息,我们只需要第一行。最后的部分是
ORDER BY row_num_all
。这是您获得正确顺序的地方。
P.S.
您想要的相关输出不正确。 对于日期
8/10/18 8:57 AM
和状态1
,最后一个日期必须是10/8/2018 9:07 AM
- 而不是10/8/2018 9:04 AM
,如您所述。还缺少日期
12/6/2018
和状态2
的输出。
更新:
以下是 FIRST_VALUE
和 LAST_VALUE
工作原理的图示。
所有三个数字都有以下部分:
- 查询数据这是查询结果
- 原始查询原始源数据。
- Windows这些是计算的中间步骤。
- Frame 提及使用的框架。
- 绿格Window规格
这是幕后发生的事情:
- 首先,SQL 服务器为所有提到的字段创建分区。在图上是
partition
列。 - 每个分区可以有一个框架:默认或自定义。默认帧是
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
。这意味着该行在分区开始和当前行之间得到 window。如果您不提及框架,则使用默认框架。 - 每一帧为每一行创建 window。在图上,这些 windows 位于
row 1
至row 2
列中,并用颜色标记。行号对应row_num_all
字段。 - 行仅在其 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 所说),此查询会生成问题的准确输出。