使用 PostgreSQL 9.6。
该表有用户会话,我需要打印不同的非重叠会话。
CREATE TABLE SESSIONS(
id serial NOT NULL PRIMARY KEY,
ctn INT NOT NULL,
day DATE NOT NULL,
f_time TIME(0) NOT NULL,
l_time TIME(0) NOT NULL
);
INSERT INTO SESSIONS(id, ctn, day, f_time, l_time)
VALUES
(1, 707, '2019-06-18', '10:48:25', '10:56:17'),
(2, 707, '2019-06-18', '10:48:33', '10:56:17'),
(3, 707, '2019-06-18', '10:53:17', '11:00:49'),
(4, 707, '2019-06-18', '10:54:31', '10:57:37'),
(5, 707, '2019-06-18', '11:03:59', '11:10:39'),
(6, 707, '2019-06-18', '11:04:41', '11:08:02'),
(7, 707, '2019-06-18', '11:11:04', '11:19:39');
id ctn day f_time l_time
1 707 2019-06-18 10:48:25 10:56:17
2 707 2019-06-18 10:48:33 10:56:17
3 707 2019-06-18 10:53:17 11:00:49
4 707 2019-06-18 10:54:31 10:57:37
5 707 2019-06-18 11:03:59 11:10:39
6 707 2019-06-18 11:04:41 11:08:02
7 707 2019-06-18 11:11:04 11:19:39
现在我需要不同的非重叠用户会话,所以它应该给我:
1. start_time: 10:48:25 end_time: 11:00:49 duration: 12min,24 sec
2. start_time: 11:03:59 end_time: 11:10:39 duration: 6min,40 sec
3. start_time: 11:11:04 end_time: 11:19:39 duration: 8min,35 sec
为了解决这个问题,我做了以下事情:
“简单”的解释:
对于这一部分,我稍微添加了 OP 提供的表定义。我坚信 DDL 应该尽可能地用于“指导”整个数据库编程过程,并且可能会更强大——一个例子是
CHECK
约束中的 SQL——到目前为止只有 Firebird 提供(此处的示例)和 H2(请参阅此处的参考)。然而,这一切都很好,但我们必须处理 PostgreSQL 的 9.6 功能——OP 的版本。我为“简单”解释调整了 DDL(请参阅此处的整个小提琴):
索引:
只是需要注意一点:不要使用SQL 关键字作为表名或列名——
day
就是这样的关键字!调试 &c 可能会令人困惑——这根本不是一个好习惯。我已将您的原始字段名更改day
为f_day
- 注意所有小写和 python 大小写!无论你做什么,都有一个命名变量的标准方法并坚持下去——那里有许多编码标准文档。对“f_day”的更改对 SQL 的其余部分没有影响,因为我们不考虑跨越午夜的会话。考虑到这些可以通过执行以下操作相对容易地完成(参见小提琴)。
现在
GENERATED
列的出现,您甚至不必担心这一点 - 只需GENERATED
像上面那样有一个字段!如果对第二个的约束是不可行的 - 同时登录,您可能会使用它
TIME(2) (or 3..6)
来确保唯一性。如果 [你不想要 | can't have]UNIQUE
约束,您可以在DISTINCT
SQL 中输入相同的登录和注销时间——尽管这不太可能。事实上,像这样的一些简单的 DDL极大地简化了您的后续 SQL(请参阅下面“复杂”解释末尾的讨论)。
您可能还想将
ctn
和/或day
放入您的 DDLUNIQUE
约束中,如图所示?我还添加了我认为可能是好的索引!您可能还想调查OVERLAPS
运营商?至于样本数据,我还添加了一些记录来测试我的解决方案,如下所示:
我会一步一步地分析我的逻辑——也许对你有好处,但对我也有好处,因为它可以帮助我理清思路,并确保我从这个练习中学到的教训会留在我身边——“我听到并且我忘记了。我看到了,我记得了。我做了,我明白了。-孔子。
以下所有内容都包含在小提琴中。
第一步是使用
LAG
函数(文档)如下:结果:
因此,只要有新的间隔,
ovl
(重叠)列中就会有一个 1。接下来,我们将
SUM
这些 1 的累积值计算如下:结果:
所以,我们现在已经“拆分”了,并且有一种方法可以区分我们的区间——每个区间都有一个不同的值
s
——1..5。所以,现在我们想要得到这些区间的最小值
f_time
和最大值l_time
。我第一次尝试使用MAX()
andMIN()
如下:结果:
请注意,我们必须如何
rn
为第一个间隔获取 = 3,rn
为第四个间隔获取 = 3,以及为不同的间隔获取不同的值rn
- 如果有 7 个子间隔组成一个间隔,那么我们将不得不检索rn
= 7 - 这让我很困惑一阵子!然后 Window 函数的力量来拯救 - 如果你对
MAX()
和 进行MIN()
不同的排序,正确的结果就会落入我们的圈子:结果:
请注意,现在
rn
= 1始终是我们想要的记录 - 这是以下结果:请注意,对于
MIN()
,排序是 bylt DESC
并且对于MAX()
(按间隔分区 - ies
)它是 byft DESC
。这匹配了最小的和我们想要ft
的最大的。lt
这基本上是我们想要的结果 - 只需根据 OP 的要求添加一些整理和格式化,我们就可以开始了。这部分还演示了另一个非常有用的Window函数的使用——
ROW_NUMBER()
.最后结果:
如果有大量记录,我无法保证此查询的性能,请参阅
EXPLAIN (ANALYZE, BUFFERS)
小提琴末尾的结果。但是,我假设由于它是报告样式格式,它可能是给定的值ctn
和/或day
- 即没有太多的记录?“复杂”解释:
我不会展示每一步——去掉重复
f_time
的 s 和l_time
s 后,步骤是相同的。在这里,表定义和数据略有不同(小提琴可用here):
我保留的唯一限制是
CHECK (f_time < l_time)
(不可能有任何其他方式)和UNIQUE f_time, l_time
(可能添加day
和/或添加ctn
到那个 - 关于TIME(2) or (3...6)
上面的建议也适用。我将把它留给读者来应用和适用
UNIQUE
的组合!ctn
f_day
我添加了几个潜在的“麻烦”记录(2 和 4),它们具有相同的间隔
f_time
并且l_time
在相同的时间间隔内。因此,在相同的情况下f_time
,我们想要具有最大的子区间,l_time
反之亦然,对于相同的情况l_time
(即最小的f_time
)。因此,在这种情况下,我所做的是通过链接
CTE
's(也称为WITH
子句)来消除重复项,如下所示:结果:
然后我
cte2
在“简单”解释中将其视为该过程的起点。最终的SQL如下:
结果:
正如您所看到的,这是非常棘手的事情——
UNIQUE
在 DDL 中没有约束会使 SQL 的长度以及计划和执行阶段所花费的时间增加一倍,并且使交易变得非常可怕。有关两个查询的计划,请参见小提琴的结尾!那里要吸取教训!根据经验,计划越长,查询越慢!
I'm not sure that the indexes can play any role here since we're selecting from the entire table and it's very small! If we were filtering a large table by
ctn
and/orf_day
and/orf_time
, I'm pretty sure that we would start to see differences in the plans (and timings!) if there were no indexes!I used the accepted answer for my own needs until I found a limit:
What if we have these rows ?
Both queries create a new group starting on row 10 instead of grouping these 3 rows together.
The answer is to tweak the core query
needs to be replace by
then you'll get: