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

时间:2018-07-21 09:42:50

标签: sql postgresql performance greatest-n-per-group postgresql-performance

我正在尝试寻找在给定时间间隔内获取第一个元素和最后一个元素的最有效方法的答案。我有一个与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)

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

2 个答案:

答案 0 :(得分:0)

您可以使用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.

答案 1 :(得分:0)

最有效的查询取决于设置的详细信息。您可以建立在现有表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

请确保在interval_data(device_id, created_at)上有一个索引。如果结果中只需要一组有限的列,则可能需要向此索引追加更多列,以获取仅索引的扫描结果。

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约束是可选的。

有关替代方案的详细说明和讨论:

替代一小部分给定的设备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的结果,但没有任何间隔数据。

关于窗框:

db <>提琴here