如何在ClickHouse中按时间段分组并填充缺失数据为null/0

12

假设我有一个给定的时间范围。为了解释方便,我们考虑一些简单的情况,像整个2018年。我想从ClickHouse查询数据并对每个季度进行汇总,因此结果应该是4行。

问题在于,我只有两个季度的数据,因此当使用GROUP BY quarter时,仅返回两行。

SELECT
     toStartOfQuarter(created_at) AS time,
     sum(metric) metric
 FROM mytable
 WHERE
     created_at >= toDate(1514761200) AND created_at >= toDateTime(1514761200)
    AND
     created_at <= toDate(1546210800) AND created_at <= toDateTime(1546210800)
 GROUP BY time
 ORDER BY time
< p > < em > < code > 1514761200 - 2018-01-01
1546210800 - 2018-12-31

time       metric
2018-01-01 345
2018-04-01 123

我需要:

time       metric
2018-01-01 345
2018-04-01 123
2018-07-01 0
2018-10-01 0

这只是一个简化的例子,但在实际使用中,聚合可能是5分钟,而不是季度,并且GROUP BY将至少具有一个以上的属性,如GROUP BY attribute1, time,因此期望的结果是:

time        metric  attribute1
2018-01-01  345     1
2018-01-01  345     2
2018-04-01  123     1
2018-04-01  123     2
2018-07-01  0       1
2018-07-01  0       2
2018-10-01  0       1
2018-10-01  0       2

有没有一种方法能够填充整个给定的区间?就像InfluxDB有fill参数用于分组或TimescaleDb的time_bucket()函数与generate_series()一样。我尝试搜索了ClickHouse文档和github问题,看起来这还没有实现,所以问题也许是是否有任何解决方法。


1
自 Clickhouse 19.14 版本开始,可以使用 'WITH FILL'。不幸的是,在某些带有其他属性的分组情况下,这似乎只能正常工作于简单情况。 - VitalyZ
4个回答

8
从ClickHouse 19.14开始,您可以使用WITH FILL子句。它可以按以下方式填充季度:
WITH
    (
        SELECT toRelativeQuarterNum(toDate('1970-01-01'))
    ) AS init
SELECT
    -- build the date from the relative quarter number
    toDate('1970-01-01') + toIntervalQuarter(q - init) AS time,
    metric
FROM
(
    SELECT
        toRelativeQuarterNum(created_at) AS q,
        sum(rand()) AS metric
    FROM
    (
        -- generate some dates and metrics values with gaps
        SELECT toDate(arrayJoin(range(1514761200, 1546210800, ((60 * 60) * 24) * 180))) AS created_at
    )
    GROUP BY q
    ORDER BY q ASC WITH FILL FROM toRelativeQuarterNum(toDate(1514761200)) TO toRelativeQuarterNum(toDate(1546210800)) STEP 1
)

┌───────time─┬─────metric─┐
│ 2018-01-012950782089 │
│ 2018-04-012972073797 │
│ 2018-07-010 │
│ 2018-10-01179581958 │
└────────────┴────────────┘

如果不清楚,range()输入值是时代时间。使用https://www.epochconverter.com/进行简单转换。 - Hengjie

5
您可以使用 "number" 函数生成零值。然后使用 UNION ALL 连接您的查询和零值,并根据获取的数据进行 GROUP BY。
因此,您的查询将如下所示:
SELECT SUM(metric),
       time
  FROM (
        SELECT toStartOfQuarter(toDate(1514761200+number*30*24*3600))  time,
               toUInt16(0) AS metric
          FROM numbers(30)

     UNION ALL 

          SELECT toStartOfQuarter(created_at) AS time,
               metric
          FROM mytable
         WHERE created_at >= toDate(1514761200)
           AND created_at >= toDateTime(1514761200)
           AND created_at <= toDate(1546210800)
           AND created_at <= toDateTime(1546210800)
       )
 GROUP BY time
 ORDER BY time

请注意,toUInt16(0) - 零值必须与metrics具有相同的类型。


不知道number函数,可以轻松地用于生成系列,谢谢!但是有没有一种方法可以针对未知数量的属性来做到这一点? - simPod
在您的情况下,您可以计算您的期间内有多少个季度,这就是您需要一个数字的原因。 - mikhail
可以通过从时间范围派生来动态完成,但我指的是Q中提到的“attribute1”。无论如何,与此同时,我通过使用CROSS JOIN连接子查询给出不同的“attribute1”值来使其工作。现在唯一缺少的是对Postgres中CTE的支持。谢谢! - simPod

2
作为一种替代方案,某些情况下可以使用range和数组函数来代替numbers()函数。
例如:对于每一对(id1,id2),应生成前7天的日期。
SELECT
  id1,
  id2,
  arrayJoin(
    arrayMap( x -> today() - 7 + x, range(7) )
  ) as date2
FROM table
WHERE date >= now() - 7
GROUP BY id1, id2

那个select的结果可以在UNION ALL中使用,以填补数据中的“空洞”。
SELECT id1, id2, date, sum(column1)
FROM (
  SELECT
    id1,
    id2,
    date,
    column1 
  FROM table
  WHERE date >= now() - 7

  UNION ALL 

  SELECT
    id1,
    id2,
    arrayJoin(
      arrayMap( x -> today() - 7 + x, range(7) )
    ) as date2,
    0 as column1
  FROM table
  WHERE date >= now() - 7
  GROUP BY id1, id2
)
GROUP BY id1, id2, date
ORDER BY date, id1, id2

2

这是我在小时桶方面的做法(需要在Grafana中可视化),感谢@filimonov和@mikhail的帮助。

最初的回答:

SELECT t, SUM(metric) as metric FROM (
    SELECT 
        arrayJoin(
          arrayMap( x -> toStartOfHour(addHours(toDateTime($from),x)),
              range(toUInt64(
                  dateDiff('hour', 
                      toDateTime($from), 
                      toDateTime($to)) + 1)))
        ) as t,
        0 as metric

    UNION ALL

    SELECT
        toStartOfHour(my_date) as t,
        COUNT(metric)
        FROM my_table
        WHERE t BETWEEN toDateTime($from) AND toDateTime($to)
        GROUP BY t
)
GROUP BY t ORDER BY t

例如,对于从2019年01月01日到2019年01月02日的范围,它会给出:

最初的回答:

SELECT t, SUM(metric) as metric FROM (
    SELECT 
        arrayJoin(
          arrayMap( x -> toStartOfHour(addHours(toDateTime('2019-01-01 00:00:00'),x)),
              range(toUInt64(
                  dateDiff('hour', 
                      toDateTime('2019-01-01 00:00:00'), 
                      toDateTime('2019-01-02 00:00:00')) + 1)))
        ) as t,
        0 as metric

    UNION ALL

    SELECT
        toStartOfHour(my_date) as t,
        COUNT(1) as metric
        FROM my_table
        WHERE t BETWEEN toDateTime('2019-01-01 00:00:00') AND toDateTime('2019-01-02 00:00:00')
        GROUP BY t
)
GROUP BY t ORDER BY t;

t                  |metric|
-------------------|------|
2019-01-01 00:00:00|     0|
2019-01-01 01:00:00|     0|
2019-01-01 02:00:00|     0|
2019-01-01 03:00:00|     0|
2019-01-01 04:00:00|     0|
2019-01-01 05:00:00|     0|
2019-01-01 06:00:00|     0|
2019-01-01 07:00:00|105702|
2019-01-01 08:00:00|113315|
2019-01-01 09:00:00|149837|
2019-01-01 10:00:00|185314|
2019-01-01 11:00:00|246106|
2019-01-01 12:00:00|323036|
2019-01-01 13:00:00|     0|
2019-01-01 14:00:00|409160|
2019-01-01 15:00:00|379113|
2019-01-01 16:00:00|256634|
2019-01-01 17:00:00|286601|
2019-01-01 18:00:00|280039|
2019-01-01 19:00:00|248504|
2019-01-01 20:00:00|218642|
2019-01-01 21:00:00|186152|
2019-01-01 22:00:00|148478|
2019-01-01 23:00:00|109721|
2019-01-02 00:00:00|     0|

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接