我在 Postgres 中有一个关于水道上船只位置的数据集。这是该表的示例:
船号 | ts | waterway_id |
---|---|---|
船_A | 2019-01-01 16:29:11 | WW_01 |
船_A | 2019-01-01 17:03:04 | WW_02 |
船_B | 2019-01-01 16:11:34 | WW_01 |
船_B | 2019-01-01 16:13:45 | WW_01 |
船_B | 2019-01-01 17:05:13 | WW_01 |
船_C | 2019-01-01 16:03:00 | WW_01 |
船_C | 2019-01-01 16:09:50 | WW_02 |
船_C | 2019-01-01 16:16:22 | WW_01 |
船_C | 2019-01-01 16:45:44 | WW_01 |
boat_id 是船的唯一标识,ts 是时间戳,water_id 是航道的唯一标识。我想知道数据集中每小时有多少艘船通过每条水道。结果应如下所示:
waterway_id | 报告_ts | 通过次数 |
---|---|---|
WW_01 | 2019-01-01 00:00 | 3 |
WW_01 | 2019-01-01 01:00 | 1 |
... | ... | ... |
WW_01 | 2019-12-31 23:00 | 5 |
WW_02 | 2019-01-01 00:00 | 13 |
WW_02 | 2019-01-01 01:00 | 11 |
... | ... | ... |
原始数据包含船只的位置,而不是通道。因此:
- 同一航道上同一条船的多个数据点应计为一个通道。
- 如果一艘船已经在另一条航道上并且回来了,它应该被算作另一条航道。
- 如果在多个小时内在同一水道上检测到一艘船,而其间没有在另一个水道上,则应在首次检测到的小时内将其计为一次通过。在上面的示例数据中,boat_A 在 16 小时在水道 WW_01 上通过 1 次,在 17 小时在 WW_02 上通过 1 次,boat_b 在 16 小时在 WW_01 上进行 1 次通过(18 小时没有通过,因为它没有去中间的另一个水道),boat_C在 16 小时在水路 WW_01 上进行 2 次通过,在 16 小时在 WW_02 上进行 1 次通过。在表格中(结果中不必包含 0 次航道小时组合):
waterway_id | 报告_ts | 通过次数 |
---|---|---|
WW_01 | 2019-01-01 16:00 | 4 |
WW_02 | 2019-01-01 16:00 | 1 |
WW_02 | 2019-01-01 17:00 | 1 |
得到这个结果的查询应该是什么样的?在我看来,它包括两个步骤:
- 计算每条船每条航道的独特通道
- 将这些组织在一个表格中,如上面的示例
在这里提琴
编辑以解决这个问题(强调我的),这与原始请求不同:
主键很重要
但在我们开始之前,我们需要确保您在数据上定义了正确的主键,即
(Boat_Id,Timestamp)
. 创建它给了我们两件事:Boat
不能同时在两个地方)Boat
使用分析/窗口函数以外的方法有效地定位每个记录的先前记录获得段落
要确定是否发生了段落,我们需要知道每个 的最后位置
Boat
,我们通过相关子查询搜索最大Timestamp
小于当前的条目来获得该位置Timestamp
。由于我们只对Boats
已移动的 感兴趣,因此Waterways
我们可以将它们从结果集中排除。或者,您可以使用 Erwin 和 Vérace 所做的分析/窗口函数。我将其作为“第二种解决方案”提供,因为在大多数情况下,分析/窗口函数将强制进行排序1。对于大量数据(或不同的 RDBMS),这可能比仅使用正确的主键2进行自联接更昂贵。和往常一样,测试。
此处修改小提琴:http ://sqlfiddle.com/#!17/2cede7/2
1
PARTITION BY
在 SQL Server(可能还有其他一些商业平台)中,如果 andORDER BY
语句与聚集索引的排序顺序匹配,则窗口/分析函数不会强制进行排序。在 MySQL 中情况并非如此。2较新的 Postgres 版本允许 INCLUDE 语句强制将指定的非键列添加到 B-Tree。在这种情况下,您可以包含 ,
Waterway_Id
以便在不触及堆的情况下完成整个查询。这是被称为Tabibito-san的一类问题的一部分- 非常值得了解!既然我想我已经掌握了你的问题,这个答案已经被高度修改了。
我稍微更改了您的架构 - 我删除了带引号的标识符 - 它们通常是不必要的,只会增加复杂性并使查询不那么清晰。
我还将名为 (boat timestamp) 的字段更改为
timestamp
,bts
因为使用 SQL 关键字作为变量名不是一个好主意——它也使 SQL 难以阅读并干扰调试。我也只保留数据
boat_1
- 更容易推理。我使用的数据可以在小提琴和这篇文章的底部找到。您可以在这里找到小提琴(哦,顺便说一句,请在任何问题中始终包含您的 PostgreSQL 版本)- 对 sqlfiddle.com 不重要(他们只有 9.6),但如果您使用 dbfiddle.uk(更多服务器),它可以是最有帮助的。
修改后的 DDL:
然后我运行了以下查询:
结果(为简洁起见):
您可能不想要所有这些数据 - 酌情删除!
有一个“段落”列表,提供了关于它们的所有细节——正如我所说,也许不是必要的?
第一行告诉您的是,对于
boat_1
,它的第一个通道开始于水路OSDOK003
并2019-06-03T10:27:25Z
结束于2019-06-03T10:28:45Z
,并且在该通道期间进行了 4 次测量。然后它在时间 x 进入水路
OSDOK005
并在时间 y 完成 - 也是 4 次测量。然后对水路进行了1次测量
OUDSC001
随后在航道上进行了 8 次测量
OUDSC002
然后最后返回
OUDSC003
进行 3 次测量。我已经“目测”了数据,这看起来是正确的!
现在,您可能必须考虑日期 - 在这种情况下,只需添加
DATE(bts)
和...SELECT
GROUP BY
我在小提琴的底部留下了一些“人工制品”,这样你就可以看到(或多或少以相反的顺序)我的想法在哪里——Postgresql 的窗口函数非常强大,非常值得掌握——它们会回报任何努力 10时间超过 - 特别是。ROW_NUMBER() - 看看他们还有 LAG/LEAD (fiddle)...
=========================================
boat_1
此答案中使用的数据。假设所有涉及的表列
NOT NULL
。您添加的说明使它成为一个更简单的问题。
这仅计算每个段落的第一个小时:
db<>在这里摆弄
我们只需要考虑每艘船切换水道后的第一排。用窗口函数识别它
lag()
。用于lag(waterway_id, 1, '')
抑制每个分区中第一行的 NULL。(假设空字符串 (''
) 与任何现有的 . 不同。)然后用和 countwaterway_id
截断到整小时。date_trunc()
瞧。我最初的解决方案计算每个段落的每小时,这要复杂得多:
db<>在这里摆弄
有关的: