如何创建一个有效的查询来按特定时间间隔计算记录?
How to create an efficient query which will count of the records by a specific time interval?
我使用哪个数据库?
我使用的是 PostgreSQL 9.5。
我需要什么?
这是我的 data_store
表格的一部分:
id | starttime
-----+----------------------------
185 | 2011-09-12 15:24:03.248+02
189 | 2011-09-12 15:24:03.256+02
312 | 2011-09-12 15:24:06.112+02
313 | 2011-09-12 15:24:06.119+02
450 | 2011-09-12 15:24:09.196+02
451 | 2011-09-12 15:24:09.203+02
452 | 2011-09-12 15:24:09.21+02
... | ...
我想创建一个查询,该查询将按特定时间间隔对记录进行计数。例如,对于 4 秒的时间间隔 - 查询应该 return 对我来说是这样的:
starttime-from | starttime-to | count
---------------------+---------------------+---------
2011-09-12 15:24:03 | 2011-09-12 15:24:07 | 4
2011-09-12 15:24:07 | 2011-09-12 15:24:11 | 3
2011-09-12 15:24:11 | 2011-09-12 15:24:15 | 0
... | ... | ...
最重要的事情:
- 时间间隔取决于用户的选择。它可以是
1 second
、37 seconds
、50 minutes
或一些组合:2 month and 30 mintues
。时间间隔的可用单位:millisecond
、second
、minute
、hour
、day
、month
、year
。如您所见,我需要一些 generic/universal 查询 但是 我也可以为每个单元创建多个查询 - 这不是问题。
- 查询应该是高效的,因为我在一个大型数据库中工作(2000 万行和更多但在查询中我只使用该数据库的一部分,例如:100 万)。
问题是:查询应该如何实现?
我试图转换我在以下线程中找到的解决方案,但我没有成功:
- PostgreSQL: running count of rows for a query 'by minute',
- Group by data intervals,
- Best way to count records by arbitrary time intervals in Rails+Postgres.
我有什么?
我删除了 post 的这一部分,以提高 post 的透明度。本节不是回答我的问题所必需的。如果你想看看这里是什么,看看 post 的历史。
您的查询似乎很复杂。您只需要生成时间序列,然后使用 left join
将它们组合在一起即可。 . .和汇总:
select g.ts, g.ts + interval '4 second', count(ds.id)
from (select generate_series(min(starttime), max(strttime), interval '4 second') as ts
from data_store
) g left join
data_store ds
on ds.starttime >= g.ts and ds.starttime < g.ts + interval '4 second'
group by g.ts
order by g.ts;
注意:如果您希望间隔从精确的秒开始(并且没有奇怪的毫秒数 1000 次中有 999 次),请使用 date_trunc()
.
编辑:
可能值得看看相关子查询是否更快:
select gs.ts,
(select count(*)
from data_store ds
where ds.starttime >= g.ts and ds.starttime < g.ts + interval '4 second'
) as cnt
from (select generate_series(min(starttime), max(strttime), interval '4 second') as ts
from data_store
) g;
如果有帮助,我会使用 UDF 创建动态 date/time 范围。
在 SomeDate>=DateR1 和 SomeDate 的 Join 中使用结果
Range、DatePart、Increment是参数
Declare @Date1 DateTime = '2011-09-12 15:24:03 '
Declare @Date2 DateTime = '2011-09-12 15:30:00 '
Declare @DatePart varchar(25)='SS'
Declare @Incr int=3
Select DateR1 = RetVal
,DateR2 = LEAD(RetVal,1,@Date2) OVER (ORDER BY RetVal)
From (Select * from [dbo].[udf-Create-Range-Date](@Date1,@Date2,@DatePart,@Incr) ) A
Where RetVal<@Date2
Returns
DateR1 DateR2
2011-09-12 15:24:03.000 2011-09-12 15:24:06.000
2011-09-12 15:24:06.000 2011-09-12 15:24:09.000
2011-09-12 15:24:09.000 2011-09-12 15:24:12.000
2011-09-12 15:24:12.000 2011-09-12 15:24:15.000
2011-09-12 15:24:15.000 2011-09-12 15:24:18.000
2011-09-12 15:24:18.000 2011-09-12 15:24:21.000
...
2011-09-12 15:29:48.000 2011-09-12 15:29:51.000
2011-09-12 15:29:51.000 2011-09-12 15:29:54.000
2011-09-12 15:29:54.000 2011-09-12 15:29:57.000
2011-09-12 15:29:57.000 2011-09-12 15:30:00.000
UDF
CREATE FUNCTION [dbo].[udf-Create-Range-Date] (@DateFrom datetime,@DateTo datetime,@DatePart varchar(10),@Incr int)
Returns
@ReturnVal Table (RetVal datetime)
As
Begin
With DateTable As (
Select DateFrom = @DateFrom
Union All
Select Case @DatePart
When 'YY' then DateAdd(YY, @Incr, df.dateFrom)
When 'QQ' then DateAdd(QQ, @Incr, df.dateFrom)
When 'MM' then DateAdd(MM, @Incr, df.dateFrom)
When 'WK' then DateAdd(WK, @Incr, df.dateFrom)
When 'DD' then DateAdd(DD, @Incr, df.dateFrom)
When 'HH' then DateAdd(HH, @Incr, df.dateFrom)
When 'MI' then DateAdd(MI, @Incr, df.dateFrom)
When 'SS' then DateAdd(SS, @Incr, df.dateFrom)
End
From DateTable DF
Where DF.DateFrom < @DateTo
)
Insert into @ReturnVal(RetVal) Select DateFrom From DateTable option (maxrecursion 32767)
Return
End
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2020-10-01','YY',1)
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2020-10-01','DD',1)
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2016-10-31','MI',15)
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2016-10-02','SS',1)
改进了 selected 答案中的查询。
我刚刚改进了您可以在 selected 答案中找到的查询。
最终查询如下:
SELECT gp.tp AS starttime_from, gp.tp + interval '4 second' AS starttime_to, count(ds.id)
FROM (SELECT generate_series(min(starttime),max(starttime), interval '4 second') as tp
FROM data_store
WHERE id_user_table=1 and sip='147.32.84.138'
ORDER BY 1
) gp
LEFT JOIN data_store ds
ON ds.id_user_table=1 and ds.sip='147.32.84.138'
and ds.starttime >= gp.tp and ds.starttime < gp.tp + interval '4 second'
GROUP BY starttime_from
我已将 ORDER BY
移动到子查询。现在快了一点。我还在 WHERE
子句中添加了 requried 列。最后,我在查询中经常使用的列上创建了多列索引:
CREATE INDEX my_index ON data_store (id_user_table, sip, starttime);
目前查询速度非常快。 注意:对于非常小的时间间隔,查询结果包含大量零计数行。这些行吃光了 space。在这种情况下,查询应包含 HAVING count(ds.id) > 0
限制,但您必须在客户端处理这些 0。
另一种解决方案
这个解决方案不如以前的解决方案快,但下面的查询没有使用多列索引,它仍然很快。
您可以在本答案末尾找到查询中的两个重要内容:
'second'
是截断输入值的精度。您还可以选择其他精度,例如:millisecond
、minute
、day
等
'4 second'
是时间间隔。时间间隔可以有其他单位如millisecond
、minute
、day
等
在这里您可以找到查询的解释:
generate_period
查询生成从指定日期时间到特定日期时间的间隔。您可以手动或通过 table 的列(如我的情况)来指示此特定日期时间。对于4秒interval时间间隔,查询returns:
tp
---------------------
2011-09-12 15:24:03
2011-09-12 15:24:07
2011-09-12 15:24:11
...
data_series
查询计算日期时间特定精度的记录:for 1 second time interval
、for 1 day time interval
等。在我的例子中,特定精度是 'second'
,所以 for 1 second time interval
但 select 操作的结果不包括未发生的日期时间的 0
值。在我的例子中,data_series
查询 returns:
starttime | ct
---------------------+-----------
2011-09-12 15:24:03 | 2
2011-09-12 15:24:06 | 2
2011-09-12 15:24:09 | 3
... | ...
最后,查询的最后一部分汇总了特定时间段的 ct
列。查询returns this:
starttime-from | starttime-to | ct
---------------------+---------------------+---------
2011-09-12 15:24:03 | 2011-09-12 15:24:07 | 4
2011-09-12 15:24:07 | 2011-09-12 15:24:11 | 3
2011-09-12 15:24:11 | 2011-09-12 15:24:15 | 0
... | ... | ...
查询如下:
WITH generate_period AS(
SELECT generate_series(date_trunc('second',min(starttime)),
date_trunc('second',max(starttime)),
interval '4 second') as tp
FROM data_store
WHERE id_user_table=1 --other restrictions
), data_series AS(
SELECT date_trunc('second', starttime) AS starttime, count(*) AS ct
FROM data_store
WHERE id_user_table=1 --other restrictions
GROUP BY 1
)
SELECT gp.tp AS starttime-from,
gp.tp + interval '4 second' AS starttime-to,
COALESCE(sum(ds.ct),0) AS ct
FROM generate_period gp
LEFT JOIN data_series ds ON date_trunc('second',ds.starttime) >= gp.tp
and date_trunc('second',ds.starttime) < gp.tp + interval '4 second'
GROUP BY 1
ORDER BY 1;
我使用哪个数据库?
我使用的是 PostgreSQL 9.5。
我需要什么?
这是我的 data_store
表格的一部分:
id | starttime
-----+----------------------------
185 | 2011-09-12 15:24:03.248+02
189 | 2011-09-12 15:24:03.256+02
312 | 2011-09-12 15:24:06.112+02
313 | 2011-09-12 15:24:06.119+02
450 | 2011-09-12 15:24:09.196+02
451 | 2011-09-12 15:24:09.203+02
452 | 2011-09-12 15:24:09.21+02
... | ...
我想创建一个查询,该查询将按特定时间间隔对记录进行计数。例如,对于 4 秒的时间间隔 - 查询应该 return 对我来说是这样的:
starttime-from | starttime-to | count
---------------------+---------------------+---------
2011-09-12 15:24:03 | 2011-09-12 15:24:07 | 4
2011-09-12 15:24:07 | 2011-09-12 15:24:11 | 3
2011-09-12 15:24:11 | 2011-09-12 15:24:15 | 0
... | ... | ...
最重要的事情:
- 时间间隔取决于用户的选择。它可以是
1 second
、37 seconds
、50 minutes
或一些组合:2 month and 30 mintues
。时间间隔的可用单位:millisecond
、second
、minute
、hour
、day
、month
、year
。如您所见,我需要一些 generic/universal 查询 但是 我也可以为每个单元创建多个查询 - 这不是问题。 - 查询应该是高效的,因为我在一个大型数据库中工作(2000 万行和更多但在查询中我只使用该数据库的一部分,例如:100 万)。
问题是:查询应该如何实现?
我试图转换我在以下线程中找到的解决方案,但我没有成功:
- PostgreSQL: running count of rows for a query 'by minute',
- Group by data intervals,
- Best way to count records by arbitrary time intervals in Rails+Postgres.
我有什么?
我删除了 post 的这一部分,以提高 post 的透明度。本节不是回答我的问题所必需的。如果你想看看这里是什么,看看 post 的历史。
您的查询似乎很复杂。您只需要生成时间序列,然后使用 left join
将它们组合在一起即可。 . .和汇总:
select g.ts, g.ts + interval '4 second', count(ds.id)
from (select generate_series(min(starttime), max(strttime), interval '4 second') as ts
from data_store
) g left join
data_store ds
on ds.starttime >= g.ts and ds.starttime < g.ts + interval '4 second'
group by g.ts
order by g.ts;
注意:如果您希望间隔从精确的秒开始(并且没有奇怪的毫秒数 1000 次中有 999 次),请使用 date_trunc()
.
编辑:
可能值得看看相关子查询是否更快:
select gs.ts,
(select count(*)
from data_store ds
where ds.starttime >= g.ts and ds.starttime < g.ts + interval '4 second'
) as cnt
from (select generate_series(min(starttime), max(strttime), interval '4 second') as ts
from data_store
) g;
如果有帮助,我会使用 UDF 创建动态 date/time 范围。
在 SomeDate>=DateR1 和 SomeDate 的 Join 中使用结果
Range、DatePart、Increment是参数
Declare @Date1 DateTime = '2011-09-12 15:24:03 '
Declare @Date2 DateTime = '2011-09-12 15:30:00 '
Declare @DatePart varchar(25)='SS'
Declare @Incr int=3
Select DateR1 = RetVal
,DateR2 = LEAD(RetVal,1,@Date2) OVER (ORDER BY RetVal)
From (Select * from [dbo].[udf-Create-Range-Date](@Date1,@Date2,@DatePart,@Incr) ) A
Where RetVal<@Date2
Returns
DateR1 DateR2
2011-09-12 15:24:03.000 2011-09-12 15:24:06.000
2011-09-12 15:24:06.000 2011-09-12 15:24:09.000
2011-09-12 15:24:09.000 2011-09-12 15:24:12.000
2011-09-12 15:24:12.000 2011-09-12 15:24:15.000
2011-09-12 15:24:15.000 2011-09-12 15:24:18.000
2011-09-12 15:24:18.000 2011-09-12 15:24:21.000
...
2011-09-12 15:29:48.000 2011-09-12 15:29:51.000
2011-09-12 15:29:51.000 2011-09-12 15:29:54.000
2011-09-12 15:29:54.000 2011-09-12 15:29:57.000
2011-09-12 15:29:57.000 2011-09-12 15:30:00.000
UDF
CREATE FUNCTION [dbo].[udf-Create-Range-Date] (@DateFrom datetime,@DateTo datetime,@DatePart varchar(10),@Incr int)
Returns
@ReturnVal Table (RetVal datetime)
As
Begin
With DateTable As (
Select DateFrom = @DateFrom
Union All
Select Case @DatePart
When 'YY' then DateAdd(YY, @Incr, df.dateFrom)
When 'QQ' then DateAdd(QQ, @Incr, df.dateFrom)
When 'MM' then DateAdd(MM, @Incr, df.dateFrom)
When 'WK' then DateAdd(WK, @Incr, df.dateFrom)
When 'DD' then DateAdd(DD, @Incr, df.dateFrom)
When 'HH' then DateAdd(HH, @Incr, df.dateFrom)
When 'MI' then DateAdd(MI, @Incr, df.dateFrom)
When 'SS' then DateAdd(SS, @Incr, df.dateFrom)
End
From DateTable DF
Where DF.DateFrom < @DateTo
)
Insert into @ReturnVal(RetVal) Select DateFrom From DateTable option (maxrecursion 32767)
Return
End
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2020-10-01','YY',1)
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2020-10-01','DD',1)
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2016-10-31','MI',15)
-- Syntax Select * from [dbo].[udf-Create-Range-Date]('2016-10-01','2016-10-02','SS',1)
改进了 selected 答案中的查询。
我刚刚改进了您可以在 selected 答案中找到的查询。
最终查询如下:
SELECT gp.tp AS starttime_from, gp.tp + interval '4 second' AS starttime_to, count(ds.id)
FROM (SELECT generate_series(min(starttime),max(starttime), interval '4 second') as tp
FROM data_store
WHERE id_user_table=1 and sip='147.32.84.138'
ORDER BY 1
) gp
LEFT JOIN data_store ds
ON ds.id_user_table=1 and ds.sip='147.32.84.138'
and ds.starttime >= gp.tp and ds.starttime < gp.tp + interval '4 second'
GROUP BY starttime_from
我已将 ORDER BY
移动到子查询。现在快了一点。我还在 WHERE
子句中添加了 requried 列。最后,我在查询中经常使用的列上创建了多列索引:
CREATE INDEX my_index ON data_store (id_user_table, sip, starttime);
目前查询速度非常快。 注意:对于非常小的时间间隔,查询结果包含大量零计数行。这些行吃光了 space。在这种情况下,查询应包含 HAVING count(ds.id) > 0
限制,但您必须在客户端处理这些 0。
另一种解决方案
这个解决方案不如以前的解决方案快,但下面的查询没有使用多列索引,它仍然很快。
您可以在本答案末尾找到查询中的两个重要内容:
'second'
是截断输入值的精度。您还可以选择其他精度,例如:millisecond
、minute
、day
等'4 second'
是时间间隔。时间间隔可以有其他单位如millisecond
、minute
、day
等
在这里您可以找到查询的解释:
generate_period
查询生成从指定日期时间到特定日期时间的间隔。您可以手动或通过 table 的列(如我的情况)来指示此特定日期时间。对于4秒interval时间间隔,查询returns:tp --------------------- 2011-09-12 15:24:03 2011-09-12 15:24:07 2011-09-12 15:24:11 ...
data_series
查询计算日期时间特定精度的记录:for 1 second time interval
、for 1 day time interval
等。在我的例子中,特定精度是'second'
,所以for 1 second time interval
但 select 操作的结果不包括未发生的日期时间的0
值。在我的例子中,data_series
查询 returns:starttime | ct ---------------------+----------- 2011-09-12 15:24:03 | 2 2011-09-12 15:24:06 | 2 2011-09-12 15:24:09 | 3 ... | ...
最后,查询的最后一部分汇总了特定时间段的
ct
列。查询returns this:starttime-from | starttime-to | ct ---------------------+---------------------+--------- 2011-09-12 15:24:03 | 2011-09-12 15:24:07 | 4 2011-09-12 15:24:07 | 2011-09-12 15:24:11 | 3 2011-09-12 15:24:11 | 2011-09-12 15:24:15 | 0 ... | ... | ...
查询如下:
WITH generate_period AS(
SELECT generate_series(date_trunc('second',min(starttime)),
date_trunc('second',max(starttime)),
interval '4 second') as tp
FROM data_store
WHERE id_user_table=1 --other restrictions
), data_series AS(
SELECT date_trunc('second', starttime) AS starttime, count(*) AS ct
FROM data_store
WHERE id_user_table=1 --other restrictions
GROUP BY 1
)
SELECT gp.tp AS starttime-from,
gp.tp + interval '4 second' AS starttime-to,
COALESCE(sum(ds.ct),0) AS ct
FROM generate_period gp
LEFT JOIN data_series ds ON date_trunc('second',ds.starttime) >= gp.tp
and date_trunc('second',ds.starttime) < gp.tp + interval '4 second'
GROUP BY 1
ORDER BY 1;