【问题标题】:Postgresql SQL GROUP BY time interval with arbitrary accuracy (down to milli seconds)具有任意精度的 Postgresql SQL GROUP BY 时间间隔(低至毫秒)
【发布时间】:2023-03-04 02:36:01
【问题描述】:

我的测量数据存储在以下结构中:

CREATE TABLE measurements(
measured_at TIMESTAMPTZ,
val INTEGER
);

已经知道使用

(a)date_trunc('hour',measured_at)

(b)generate_series

我可以通过以下方式汇总我的数据:

microseconds,
milliseconds
.
.
.

但是是否可以按 5 分钟或任意秒来聚合数据?是否可以按任意秒数聚合测量数据?

我需要按不同时间分辨率聚合的数据,以将它们输入 FFT 或 AR 模型,以便查看可能的季节性。

【问题讨论】:

    标签: postgresql group-by


    【解决方案1】:

    也许,您可以extract(epoch from measured_at) 并从那里开始?

    【讨论】:

      【解决方案2】:

      您可以通过添加由 generate_series() 创建的间隔来生成“桶”表。此 SQL 语句将在您的数据中生成第一天的五分钟存储桶表(min(measured_at) 的值)。

      select 
        (select min(measured_at)::date from measurements) + ( n    || ' minutes')::interval start_time,
        (select min(measured_at)::date from measurements) + ((n+5) || ' minutes')::interval end_time
      from generate_series(0, (24*60), 5) n
      

      that 语句包装在一个公用表表达式中,您可以将其加入和分组,就像它是一个基表一样。

      with five_min_intervals as (
        select 
          (select min(measured_at)::date from measurements) + ( n    || ' minutes')::interval start_time,
          (select min(measured_at)::date from measurements) + ((n+5) || ' minutes')::interval end_time
        from generate_series(0, (24*60), 5) n
      )
      select f.start_time, f.end_time, avg(m.val) avg_val 
      from measurements m
      right join five_min_intervals f 
              on m.measured_at >= f.start_time and m.measured_at < f.end_time
      group by f.start_time, f.end_time
      order by f.start_time
      

      按任意秒数分组类似——使用date_trunc()


      generate_series() 的更一般用法可以让您避免猜测五分钟存储桶的上限。在实践中,您可能会将其构建为视图或函数。您可能会从基表中获得更好的性能。

      select 
        (select min(measured_at)::date from measurements) + ( n    || ' minutes')::interval start_time,
        (select min(measured_at)::date from measurements) + ((n+5) || ' minutes')::interval end_time
      from generate_series(0, ((select max(measured_at)::date - min(measured_at)::date from measurements) + 1)*24*60, 5) n;
      

      【讨论】:

      • generate_series(timestamp, timestamp, interval) 更适合这种情况,请参阅下面比尔的回答了解原因。
      • @AlexanderGonchiy:我们不知道 generate_series(timestamp, timestamp, interval) 是否是这个 4 岁问题的更好选择。问题中没有版本信息。时间戳参数在 8.4 中引入;早期版本在 2012 年仍然被广泛使用。即使在今天,GreenPlum 和 Amazon Redshift 等一些衍生产品也不支持 generate_series 中的时间戳参数。
      • 此解决方案使区间边界根据数据中可用的最小和最大时间戳移动。它可能不适用于动态(又名“实时”)图表,因为沿 X 轴的标签会发生变化。如果是这种情况,绝对间隔(不限于最小/最大数据)可能会更好。
      【解决方案3】:

      Catcall 有一个很好的答案。我使用它的示例演示了固定存储桶 - 在这种情况下,从午夜开始每隔 30 分钟。它还说明了在 Catcall 的第一个版本中可能会产生一个额外的桶以及如何消除它。我想要一天 48 桶。在我的问题中,观察有单独的日期和时间列,我想在一个月内对多个不同服务的 30 分钟内的观察进行平均。

      with intervals as (
          select
              (n||' minutes')::interval as start_time, 
              ((n+30)|| ' minutes')::interval as end_time
          from generate_series(0, (23*60+30), 30) n
      )
      select i.start_time, o.service, avg(o.o)
      from
      observations o right join intervals i
      on o.time >= i.start_time and o.time < i.end_time
      where o.date between '2013-01-01' and '2013-01-31'
      group by i.start_time, i.end_time, o.service
      order by i.start_time
      

      【讨论】:

        【解决方案4】:

        我想查看过去 24 小时的数据,并以每小时为增量进行计数。我开始使用 Cat Recall 的解决方案,它非常巧妙。但是,它与数据有关,而不仅仅是过去 24 小时内发生的事情。所以我进行了重构,最终得到了一些非常接近 Julian 的解决方案,但 CTE 更多的东西。所以这是两个答案的结合。

        WITH interval_query AS (
            SELECT (ts ||' hour')::INTERVAL AS hour_interval
            FROM generate_series(0,23) AS ts
        ), time_series AS (
            SELECT date_trunc('hour', now()) + INTERVAL '60 min' * ROUND(date_part('minute', now()) / 60.0) - interval_query.hour_interval AS start_time
            FROM interval_query
        ), time_intervals AS (
            SELECT start_time, start_time + '1 hour'::INTERVAL AS end_time
            FROM time_series ORDER BY start_time
        ), reading_counts AS (
            SELECT f.start_time, f.end_time, br.minor, count(br.id) readings
            FROM beacon_readings br
            RIGHT JOIN time_intervals f
                            ON br.reading_timestamp >= f.start_time AND br.reading_timestamp < f.end_time AND br.major = 4
            GROUP BY f.start_time, f.end_time, br.minor
            ORDER BY f.start_time, br.minor
        )
        SELECT * FROM reading_counts
        

        请注意,我希望在最终查询中进行的任何其他限制都需要在 RIGHT JOIN 中完成。我并不是说这一定是最好的(甚至是一个好方法),但它是我在仪表板中运行的(至少目前)。

        【讨论】:

          【解决方案5】:

          怎么样

          SELECT MIN(val), 
          EXTRACT(epoch FROM measured_at) / EXTRACT(epoch FROM INTERVAL '5 min') AS int 
          FROM measurements 
          GROUP BY int
          

          其中 '5 min' 可以是 INTERVAL 支持的任何表达式

          【讨论】:

            【解决方案6】:

            以下内容将为您提供任何大小的存储桶,即使它们不能很好地与分钟/小时/任何边界保持一致。值“300”用于 5 分钟分组,但可以替换为任何值:

            select measured_at, 
                   val, 
                   (date_trunc('seconds', (measured_at - timestamptz 'epoch') / 300) * 300 + timestamptz 'epoch') as aligned_measured_at
            from measurements;
            

            然后,您可以在“val”周围使用所需的任何聚合,并根据需要使用“group by aligned_measured_at”。

            【讨论】:

              【解决方案7】:

              这基于 Mike Sherrill 的回答,只是它使用时间戳间隔而不是单独的开始/结束列。

              with intervals as (
                  select tstzrange(s, s + '5 minutes') das_interval
                  from (select generate_series(min(lower(time_range)), max(upper(time_rage)), '5 minutes') s
                        from your_table) x)
              select das_interval, your_table.*
              from   your_table
              right join intervals on time_range && das_interval
              order by das_interval;
              

              【讨论】:

                【解决方案8】:

                我综合了以上所有内容,试图想出一些更容易使用的东西;

                create or replace function interval_generator(start_ts timestamp with TIME ZONE, end_ts timestamp with TIME ZONE, round_interval INTERVAL)
                    returns TABLE(start_time timestamp with TIME ZONE, end_time timestamp with TIME ZONE) as $$
                BEGIN
                return query
                        SELECT
                            (n)       start_time,
                            (n + round_interval) end_time
                        FROM generate_series(date_trunc('minute', start_ts), end_ts, round_interval) n;
                END
                $$
                    LANGUAGE 'plpgsql';
                

                此函数是Mikes answer 的时间戳抽象,它 (IMO) 使事情变得更简洁,尤其是当您在客户端生成查询时。

                还使用内连接消除了之前出现的NULLs 的海洋。

                with intervals as (select * from interval_generator(NOW() - INTERVAL '24 hours' , NOW(), '30 seconds'::INTERVAL))
                select f.start_time, m.session_id, m.metric, min(m.value) min_val, avg(m.value) avg_val, max(m.value) max_val
                from ts_combined as m
                inner JOIN intervals f
                    on m.time >= f.start_time and m.time < f.end_time
                GROUP BY f.start_time, f.end_time, m.metric, m.session_id
                ORDER BY f.start_time desc
                

                (出于我的目的,我还添加了一些聚合字段)

                【讨论】:

                  【解决方案9】:

                  PostgreSQL 的Timescale extension 提供了按任意时间间隔分组的能力。该函数称为time_bucket(),语法与date_trunc() 函数相同,但第一个参数采用间隔而不是时间精度。 Here 你可以找到它的 API Docs。这是一个例子:

                  SELECT
                    time_bucket('5 minutes', observation_time) as bucket,
                    device_id,
                    avg(metric) as metric_avg,
                    max(metric) - min(metric) as metric_spread
                  FROM
                    device_readings
                  GROUP BY bucket, device_id;
                  

                  如果您希望使用新摄取的数据自动更新 “按间隔分组” 视图,并且如果您想经常查询这些视图,您也可以查看 continuous aggregate views基础。这可以为您节省大量资源,并使您的查询速度更快。

                  【讨论】:

                  • 这是一个商业产品
                  • @geotheory 只有他们的托管云服务。该扩展是在 apache 2.0 许可下开源的。所以我看不出有什么问题。您无需使用他们的托管服务。
                  • 我也是这么想的。 @TomBöttger,您应该链接到开源插件而不是产品主页。
                  • 感谢@danielricecodes 我已经更新了链接,现在使用github而不是商业网站链接
                  【解决方案10】:

                  从 PostgreSQL v14 开始,您可以使用 date_bin 函数:

                  SELECT date_bin(
                            INTERVAL '5 minutes',
                            measured_at,
                            TIMSTAMPTZ '2000-01-01'
                         ),
                         sum(val)
                  FROM measurements
                  GROUP BY 1;
                  

                  【讨论】:

                    猜你喜欢
                    • 2020-03-18
                    • 2013-02-23
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2022-01-24
                    相关资源
                    最近更新 更多