获取分组的第一个和最后一个时间戳之间另一个字段的差异
Posted
技术标签:
【中文标题】获取分组的第一个和最后一个时间戳之间另一个字段的差异【英文标题】:Get difference of another field between first and last timestamps of grouping 【发布时间】:2013-12-13 11:28:35 【问题描述】:我有一个名为sensor_values
的非常大的表,其中包含timestamp
、value
、sensor_id
和另一个名为sensors
的列,其中包含sensor_id
、name
。
我经常执行数据透视查询来获取按天分组的汇总数据,如下所示:
SELECT MIN(to_char(s1.timestamp::timestamptz, 'YYYY-MM-DD HH24:MI:SS TZ')) AS time,
SUM(CASE WHEN s1.sensor_id = 572 THEN s1.value ELSE 0.0 END) AS "Nickname1",
SUM(CASE WHEN s1.sensor_id = 542 THEN s1.value ELSE 0.0 END) AS "Nickname2",
SUM(CASE WHEN s1.sensor_id = 571 THEN s1.value ELSE 0.0 END) AS "Nickname3"
FROM sensor_values s1
WHERE s1.timestamp::timestamptz >= '2013-10-14T00:00:00+00:00'::timestamptz
AND s1.timestamp::timestamptz <= '2013-10-18T00:00:00+00:00'::timestamptz
AND s1.sensor_id IN (572, 542, 571, 540, 541, 573)
GROUP BY date_trunc('day', s1.timestamp) ORDER BY 1 ;
如果有点慢,这可以正常工作。但是,是否可以编写一个类似的查询 而不是对各组求和,而是得到每个分组中最新和最早时间戳之间的差异,即在这种情况下是天?
这是因为我有一些不断增加的传感器数据(电度表) 并想知道特定时间范围内的消耗量。
【问题讨论】:
sensor_values_cleaned
没有解释?您确实需要为您的表定义提供这样的问题(psql 中的\d tbl
)和一个可以使用的测试用例——最好是sqlfiddle。而且,始终,您的 Postgres 版本。
对不起,我已经编辑了 sql。 sensor_values_cleaned 是与 sensor_values 相同的表,但具有修改的值。这对问题并不重要,因此已将其删除。谢谢
【参考方案1】:
第 1 步:松开手刹
...如果有点慢
SELECT to_char(MIN(ts)::timestamptz, 'YYYY-MM-DD HH24:MI:SS TZ') AS min_time
,SUM(CASE WHEN sensor_id = 572 THEN value ELSE 0.0 END) AS nickname1
,SUM(CASE WHEN sensor_id = 542 THEN value ELSE 0.0 END) AS nickname2
,SUM(CASE WHEN sensor_id = 571 THEN value ELSE 0.0 END) AS nickname3
FROM sensor_values
-- LEFT JOIN sensor_values_cleaned s2 USING (sensor_id, ts)
WHERE ts >= '2013-10-14T00:00:00+00:00'::timestamptz::timestamp
AND ts < '2013-10-18T00:00:00+00:00'::timestamptz::timestamp
AND sensor_id IN (572, 542, 571, 540, 541, 573)
GROUP BY ts::date AS day
ORDER BY 1;
要点
在您的标识符中替换 reserved words(在标准 SQL 中)。timestamp
-> ts
time
-> min_time
由于连接在相同的列名上,您可以在连接条件中使用更简单的USING
clause:USING (sensor_id, ts)
但是,由于第二个表 sensor_values_cleaned
与此查询 100% 无关,因此我将其完全删除。
正如@joop 已经建议的那样,在您的第一个输出列中切换min()
和to_char()
。这样,Postgres 可以从 原始列值 中确定最小值,这通常更快并且可能能够利用索引。在这种特定情况下,date
订购 也比 text
订购便宜,后者还必须考虑整理规则。
类似的考虑适用于您的WHERE
条件:WHERE ts::timestamptz >= '2013-10-14T00:00:00+00:00'::timestamptz
WHERE ts >= '2013-10-14T00:00:00+00:00'::timestamptz::timestamp
第二个是sargable,可以在ts
上使用普通索引 - 对大表的性能有很大影响!
使用ts::date
代替date_trunc('day', ts)
。更简单、更快、结果相同。
很可能您的第二个 WHERE 条件稍微不正确。通常,您会排除上边框:AND ts <b><=</b> '2013-10-18T00:00:00+00:00' ...
AND ts <b><</b> '2013-10-18T00:00:00+00:00' ...
当混合timestamp
和timestamptz
时,需要注意效果。例如,您的 WHERE
条件不会在当地时间 00:00 结束(除非当地时间与 UTC 重合)。详情看这里:Ignoring timezones altogether in Rails and PostgreSQL
第 2 步:您的请求
...每个分组中最新和最早时间戳之间的差异
我想你的意思是: ...值最新和最早时间戳之间的差异 ... 否则会简单很多。
为此使用window functions,尤其是first_value()
和last_value()
。小心组合,在这种情况下,您需要一个 non-standard window frame 用于 last_value() 。比较:PostgreSQL aggregate or window function to return just the last value
我将它与DISTINCT ON
结合使用,在这种情况下它比GROUP BY
更方便(需要另一个子查询级别):
SELECT DISTINCT ON (ts::date, sensor_id)
ts::date AS day
,to_char((min(ts) OVER (PARTITION BY ts::date))::timestamptz
,'YYYY-MM-DD HH24:MI:SS TZ') AS min_time
,sensor_id
,last_value(value) OVER (PARTITION BY ts::date, sensor_id ORDER BY ts
RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
- first_value(value) OVER (PARTITION BY ts::date, sensor_id ORDER BY ts)
AS val_range
FROM sensor_values
WHERE ts >= '2013-10-14T00:00:00+0'::timestamptz::timestamp
AND ts < '2013-10-18T00:00:00+0'::timestamptz::timestamp
AND sensor_id IN (540, 541, 542, 571, 572, 573)
ORDER BY ts::date, sensor_id;
-> SQLfiddle demo.
第 3 步:数据透视表
基于上面的查询,我使用附加模块 tablefunc
中的 crosstab()
:
SELECT * FROM crosstab(
$$SELECT DISTINCT ON (1,3)
ts::date AS day
,to_char((min(ts) OVER (PARTITION BY ts::date))::timestamptz,'YYYY-MM-DD HH24:MI:SS TZ') AS min_time
,sensor_id
,last_value(value) OVER (PARTITION BY ts::date, sensor_id ORDER BY ts RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
- first_value(value) OVER (PARTITION BY ts::date, sensor_id ORDER BY ts) AS val_range
FROM sensor_values
WHERE ts >= '2013-10-14T00:00:00+0'::timestamptz::timestamp
AND ts < '2013-10-18T00:00:00+0'::timestamptz::timestamp
AND sensor_id IN (540, 541, 542, 571, 572, 573)
ORDER BY 1, 3$$
,$$VALUES (540), (541), (542), (571), (572), (573)$$
)
AS ct (day date, min_time text, s540 numeric, s541 numeric, s542 numeric, s571 numeric, s572 numeric, s573 numeric);
返回(并且比以前快得多):
day | min_time | s540 | s541 | s542 | s571 | s572 | s573
------------+--------------------------+-------+-------+-------+-------+-------+-------
2013-10-14 | 2013-10-14 03:00:00 CEST | 18.82 | 18.98 | 19.97 | 19.47 | 17.56 | 21.27
2013-10-15 | 2013-10-15 00:15:00 CEST | 22.59 | 24.20 | 22.90 | 21.27 | 22.75 | 22.23
2013-10-16 | 2013-10-16 00:16:00 CEST | 23.74 | 22.52 | 22.23 | 23.22 | 23.03 | 22.98
2013-10-17 | 2013-10-17 00:17:00 CEST | 21.68 | 24.54 | 21.15 | 23.58 | 23.04 | 21.94
【讨论】:
很好的建议。感谢您的详细回答。【参考方案2】:尝试替换
SELECT MIN(to_char(s1.timestamp::timestamptz, 'YYYY-MM-DD HH24:MI:SS TZ')) AS time,
作者:
SELECT to_char(MIN(s1.timestamp)::timestamptz, 'YYYY-MM-DD HH24:MI:SS TZ') AS zztime,
甚至:
SELECT MIN(s1.timestamp) AS zztime,
因为您指定的日期时间戳格式或多或少是默认值
这将避免计算表达式的最小选择。
顺便说一句:timestamp
和 time
都是(postgres)SQL 中的保留字(类型名称)。尽量避免将它们用作标识符。
【讨论】:
在这种情况下,我实际上想要每个组的最短时间。 在这种情况下,您不需要转换为字符。日期可订购。to_char()
在这里并不多余,因为它输出时区 name(与 tz 偏移或 tz 缩写相对)。
想要 name 区域的 最小值 非常有意义 ...(鉴于日期时间部分的关系)以上是关于获取分组的第一个和最后一个时间戳之间另一个字段的差异的主要内容,如果未能解决你的问题,请参考以下文章
根据另一个字段的最大值获取字段的第一个值,同时还使用其他最大值字段