Как получить первый и последний элемент для каждого устройства?

Я пытаюсь найти ответ о том, как наиболее эффективно получить первый и последний элементы в заданный интервал времени. У меня есть таблица interval_data (включая данные iot), которая связана с таблицей 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)

Но когда количество устройств увеличивается, время отклика увеличивается. У вас есть какие-либо предложения? Как я могу быстрее найти первый и последний элемент каждого устройства?


person Furkan Uyar    schedule 21.07.2018    source источник
comment
Ваша версия Postgres и определение таблицы (операторы CREATE TABLE, показывающие типы данных и ограничения) полезны для любого вопроса, связанного с SQL. В частности, по вопросам производительности. И любые дополнительные индексы, которые у вас могут быть. И можете ли вы изменить таблицу и индексы. Также: первый и последний по id или по created_at? И вы хотите, чтобы в результат были включены устройства без каких-либо данных об интервалах?   -  person Erwin Brandstetter    schedule 21.07.2018
comment
Также вы упоминаете result for each device, но ваша попытка запроса касается небольшого набора устройств: device_id in (1000022, 1000023, 1000024). Может иметь большое значение.   -  person Erwin Brandstetter    schedule 21.07.2018
comment
Спасибо, Эрвин, первое и последнее значения слова «created_at». Мне нужно только значение и created_at из interval_data и device_id из устройства. Я написал device_id in (1000022, 1000023, 1000024) часть в качестве примера, может быть два устройства, а иногда может быть восемьдесят устройств. Но ваше решение с логикой lateral и limit 1 работает очень хорошо.   -  person Furkan Uyar    schedule 23.07.2018


Ответы (2)


Вы можете использовать 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' 

Пример на SQL Fiddle.

person Andomar    schedule 21.07.2018
comment
Спасибо Андомар за ваш ответ. Это полезно для нескольких устройств, но если количество устройств увеличивается, это занимает некоторое время. - person Furkan Uyar; 23.07.2018

Наиболее эффективный запрос зависит от деталей вашей настройки. Вы можете использовать существующую таблицу 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 сохраняет устройства без данных интервала в результате.

Чтобы ограничиться заданным набором идентификаторов устройств, добавьте к запросу:

...
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 является необязательным для этого решения.

Подробное объяснение и обсуждение альтернатив:

Альтернатива для небольшого набора заданных идентификаторов устройств

Если вам удобно оконные функции, использующие настраиваемые оконные рамы, этот вариант не требует дополнительной таблицы device и может быть быстрее для небольшого набора идентификаторов:

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);

То же, что и в первом запросе выше:

  • Нет результатов для переданных идентификаторов устройств, которые не существуют.

Но отличается от первого запроса выше:

  • Нет результатов для переданных идентификаторов устройств, которые действительно существуют, но без каких-либо данных об интервалах.

Про оконные рамы:

db‹>скрипка здесь

person Erwin Brandstetter    schedule 21.07.2018
comment
Спасибо Эрвин за подробный ответ. Это очень полезно. Я не знал о LATERAL, который слишком полезен. - person Furkan Uyar; 23.07.2018
comment
@FurkanUyar: я добавил ссылку на подробности о LATERAL. - person Erwin Brandstetter; 23.07.2018