如何获取每个设备的第一个和最后一个元素?

Posted

技术标签:

【中文标题】如何获取每个设备的第一个和最后一个元素?【英文标题】:How to get first and last element per device? 【发布时间】:2018-12-29 11:45:39 【问题描述】:

我正在尝试找出在给定时间间隔内获取第一个元素和最后一个元素的最有效方法的答案。我有一张表interval_data(包括像物联网数据),它与device 表有关系。我想得到每个设备的第一个和最后一个元素的结果。

区间数据表:

    id           device_id          created_at           value
    15269665      1000206      2018-07-21 00:10:00    5099.550000
    15270533      1000206      2018-07-21 00:20:00    5099.610000
    15271400      1000206      2018-07-21 00:30:00    5099.760000
    15272269      1000206      2018-07-21 00:40:00    5099.850000
    15273132      1000206      2018-07-21 00:50:00    5099.910000
    15274040      1000206      2018-07-21 01:00:00    5099.970000
    15274909      1000206      2018-07-21 01:10:00    5100.030000
    15275761      1000206      2018-07-21 01:20:00    5100.110000
    15276629      1000206      2018-07-21 01:30:00    5100.160000
    15277527      1000206      2018-07-21 01:40:00    5100.340000
    15278351      1000206      2018-07-21 01:50:00    5100.400000
    15279219      1000206      2018-07-21 02:00:00    5100.450000
    15280085      1000206      2018-07-21 02:10:00    5100.530000
    15280954      1000206      2018-07-21 02:20:00    5100.590000
    15281858      1000206      2018-07-21 02:30:00    5100.640000
    15282724      1000206      2018-07-21 02:40:00    5100.750000
    15283627      1000206      2018-07-21 02:50:00    5100.870000
    15284495      1000206      2018-07-21 03:00:00    5100.930000
      ...           ...                ...                ...

我尝试了一些查询,例如:

select created_at, value from interval_data i inner join
(select min(created_at) minin, max(created_at) maxin, d.device_id from device 
d
inner join interval_data i on i.device_id = d.device_id
where d.device_id in (1000022, 1000023, 1000024)
and i.created_at between '2018-01-01 00:00:00' and '2019-01-01 00:00:00' 
group by d.device_id) s
on s.device_id = i.device_id and (s.minin = i.created_at or s.maxin = 
i.created_at)

但是当设备数量增加时,响应时间需要很长时间。你有什么建议吗?如何更快地找到每个设备的第一个和最后一个元素?

【问题讨论】:

您的 Postgres 版本和表定义(CREATE TABLE 显示数据类型和约束的语句)对于任何涉及 SQL 的问题都有指导意义。特别是对于性能问题。以及您可能拥有的任何其他索引。以及是否可以更改表和索引。另外:idcreated_at 的“第一”和“最后”?您是否希望结果中不包含任何区间数据的设备? 您还提到了result for each device,但您的查询尝试是针对一小部分设备:device_id in (1000022, 1000023, 1000024)可以大有作为。 感谢 Erwin,'created_at' 的第一个和最后一个含义。我只需要来自设备的 interval_data 和 device_id 的 value 和 created_at 。我写了device_id in (1000022, 1000023, 1000024) 部分作为示例,可以是两个设备,有时可以是八个设备。但是您的解决方案具有横向和限制 1 逻辑,效果非常好。 【参考方案1】:

您可以使用row_number 为具有相同device_id 的每一行分配一个递增的数字。如果你这样做两次,一次升序,一次降序,你可以抓住每组的第一行和最后一行:

select  device_id
,       created_at
,       value
from    (
        select  row_number() over (partition by device_id order by created_at) rn1
        ,       row_number() over (partition by device_id order by created_at desc) rn2
        ,       *
        from    interval_data
        ) i
where   device_id in (1, 3, 4)
        and (rn1 = 1 or rn2 = 1) -- First or last row per device
        and created_at between '2018-01-01 00:00:00' and '2019-01-01 00:00:00' 

Example at SQL Fiddle.

【讨论】:

感谢安多玛的回答。它对少数设备很有用,但如果设备数量增加,则需要一些时间。【参考方案2】:

最有效的查询取决于您的设置细节。您可以在现有表 device 上进行构建,并提及许多设计并显示每个设备的大量间隔数据。所以通常,包含两个 LATERAL 子查询的查询应该是最快的:

SELECT *  -- or just the columns you need
FROM device d
LEFT JOIN LATERAL (
   SELECT id AS first_intv_id, created_at AS first_created_at, value AS first_value
   FROM   interval_data
   WHERE  device_id = d.id
   ORDER  BY created_at
   LIMIT  1
   ) f ON true
LEFT JOIN LATERAL (
   SELECT id AS last_intv_id, created_at AS last_created_at, value AS last_value
   FROM   interval_data
   WHERE  device_id = d.id
   ORDER  BY created_at DESC  -- NULLS LAST if column isn't NOT NULL
   LIMIT  1
   ) l ON true;

Postgres 可以将其转换为仅对大表 interval_data 进行快速索引扫描的查询计划。

关于LATERAL

What is the difference between LATERAL and a subquery in PostgreSQL?

确保在interval_data(device_id, created_at) 上有一个索引。如果您只需要结果中的一组有限的列,则可能需要将更多列附加到该索引以获得 index-only 扫描。

LEFT JOIN ... ON true 保留结果中没有间隔数据的设备。

要限制为一组给定的设备 ID,请附加到查询中:

...
WHERE  d.id IN (1000022, 1000023, 1000024);

并且在device(id) 上有一个索引——无论如何这都是典型的情况。

假设当前的 Postgres 版本和 设置 如下:

CREATE TABLE device (
   id     serial PRIMARY KEY
 , device text NOT NULL
);

CREATE TABLE interval_data (
   id         serial PRIMARY KEY
 , device_id  int NOT NULL
 , created_at timestamp NOT NULL
 , value      numeric NOT NULL
 , CONSTRAINT device_fkey FOREIGN KEY (device_id) REFERENCES device (id)
);

如果某些涉及的列未定义NOT NULL,您可能需要调整详细信息。

FK 约束对于此解决方案是可选的。

替代方案的详细解释和讨论:

Select first row in each GROUP BY group? Optimize GROUP BY query to retrieve latest record per user PostgreSQL: running count of rows for a query 'by minute'

一小组给定设备 ID 的替代方案

如果您对使用自定义窗口框架的窗口函数感到满意,则此替代方法不需要额外的表 device,并且对于一小组 ID 可能更快:

SELECT DISTINCT ON (device_id)
       device_id
     , first_value(created_at) OVER w AS first_created_at
     , first_value(value)      OVER w AS first_value
     , last_value (created_at) OVER w AS last_created_at
     , last_value (value)      OVER w AS last_value
FROM   interval_data
WHERE  device_id IN (1000022, 1000023, 1000024)
WINDOW w AS (PARTITION BY device_id ORDER BY created_at
             RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);

与上面的第一个查询相同:

传递的不存在的设备 ID 没有结果。

与上面的第一个查询不同

确实存在但没有任何间隔数据的已传递设备 ID 没有结果。

关于窗框:

PostgreSQL query with max and min date plus associated id per row How to use a ring data structure in window functions

db小提琴here

【讨论】:

感谢 Erwin 的详细解答。这是非常有帮助的。我不知道 LATERAL 太有用了。 @FurkanUyar:我添加了关于LATERAL的详细信息的链接。

以上是关于如何获取每个设备的第一个和最后一个元素?的主要内容,如果未能解决你的问题,请参考以下文章

获取数组中的第一个和最后一个元素

如何使用 SQL 获取列中每个分区的第一个和最后一个值

php获取并删除数组的第一个和最后一个元素

php获取并删除数组的第一个和最后一个元素

jquery如何获取第一个或最后一个子元素?

jquery如何获取ul中第一个li和最后一个li