Эффективный запрос для получения наибольшего значения для группы из большой таблицы

13

Учитывая таблицу:

    Column    |            Type             
 id           | integer                     
 latitude     | numeric(9,6)                
 longitude    | numeric(9,6)                
 speed        | integer                     
 equipment_id | integer                     
 created_at   | timestamp without time zone
Indexes:
    "geoposition_records_pkey" PRIMARY KEY, btree (id)

Таблица содержит 20 миллионов записей, что, по большому счету , не так много. Но это делает последовательное сканирование медленным.

Как я могу получить последнюю запись ( max(created_at)) каждого equipment_id?

Я пробовал оба следующих запроса, с несколькими вариантами, которые я прочитал во многих ответах на эту тему:

select max(created_at),equipment_id from geoposition_records group by equipment_id;

select distinct on (equipment_id) equipment_id,created_at 
  from geoposition_records order by equipment_id, created_at desc;

Я также пытался создать индексы btree для, equipment_id,created_atно Postgres обнаружил, что использование seqscan быстрее. Принудительное enable_seqscan = offиспользование также бесполезно, поскольку чтение индекса происходит так же медленно, как и сканирование seq, возможно, и хуже.

Запрос должен выполняться периодически, возвращая всегда последний.

Использование Postgres 9.3.

Объясните / проанализируйте (с 1,7 миллионами записей):

set enable_seqscan=true;
explain analyze select max(created_at),equipment_id from geoposition_records group by equipment_id;
"HashAggregate  (cost=47803.77..47804.34 rows=57 width=12) (actual time=1935.536..1935.556 rows=58 loops=1)"
"  ->  Seq Scan on geoposition_records  (cost=0.00..39544.51 rows=1651851 width=12) (actual time=0.029..494.296 rows=1651851 loops=1)"
"Total runtime: 1935.632 ms"

set enable_seqscan=false;
explain analyze select max(created_at),equipment_id from geoposition_records group by equipment_id;
"GroupAggregate  (cost=0.00..2995933.57 rows=57 width=12) (actual time=222.034..11305.073 rows=58 loops=1)"
"  ->  Index Scan using geoposition_records_equipment_id_created_at_idx on geoposition_records  (cost=0.00..2987673.75 rows=1651851 width=12) (actual time=0.062..10248.703 rows=1651851 loops=1)"
"Total runtime: 11305.161 ms"
Фейд
источник
хорошо в прошлый раз, когда я проверил, не было никаких NULLзначений в equipment_idожидаемом проценте ниже 0,1%
Фейд

Ответы:

10

Простой многоколонный индекс b-дерева должен работать в конце концов:

CREATE INDEX foo_idx
ON geoposition_records (equipment_id, created_at DESC NULLS LAST);

Почему DESC NULLS LAST?

функция

Если вы не можете разобраться в планировщике запросов, функция, проходящая по таблице оборудования, должна помочь. Поиск по одному equipment_id за раз использует индекс. Для небольшого числа (57, судя по вашему EXPLAIN ANALYZEвыводу) это быстро.
Можно предположить, что у вас есть equipmentстол?

CREATE OR REPLACE FUNCTION f_latest_equip()
  RETURNS TABLE (equipment_id int, latest timestamp) AS
$func$
BEGIN
FOR equipment_id IN
   SELECT e.equipment_id FROM equipment e ORDER BY 1
LOOP
   SELECT g.created_at
   FROM   geoposition_records g
   WHERE  g.equipment_id = f_latest_equip.equipment_id
                           -- prepend function name to disambiguate
   ORDER  BY g.created_at DESC NULLS LAST
   LIMIT  1
   INTO   latest;

   RETURN NEXT;
END LOOP;
END  
$func$  LANGUAGE plpgsql STABLE;

Делает для приятного звонка тоже:

SELECT * FROM f_latest_equip();

Коррелированные подзапросы

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

SELECT equipment_id
     ,(SELECT created_at
       FROM   geoposition_records
       WHERE  equipment_id = eq.equipment_id
       ORDER  BY created_at DESC NULLS LAST
       LIMIT  1) AS latest
FROM   equipment eq;

Производительность очень хорошая.

LATERAL присоединиться к Postgres 9.3+

SELECT eq.equipment_id, r.latest
FROM   equipment eq
LEFT   JOIN LATERAL (
   SELECT created_at
   FROM   geoposition_records
   WHERE  equipment_id = eq.equipment_id
   ORDER  BY created_at DESC NULLS LAST
   LIMIT  1
   ) r(latest) ON true;

Детальное объяснение:

Производительность аналогична коррелированному подзапросу. Сравнение производительности max(), DISTINCT ON, функция, коррелируют подзапрос и LATERALв этом:

SQL Fiddle .

Эрвин Брандштеттер
источник
1
@ErwinBrandstetter - это то, что я пробовал после ответа Колина, но я не могу перестать думать, что это обходной путь, который использует вид n + 1 запросов со стороны базы данных (не уверен, попадает ли это в антипаттерн, так как никаких накладных расходов на соединение) ... Мне интересно, почему вообще существует группа by, если она не может правильно обрабатывать несколько миллионов записей ... Это просто не имеет смысла, верно? быть чем-то, чего нам не хватает. Наконец, вопрос немного изменился, и мы предполагаем наличие таблицы оборудования ... Я хотел бы знать, есть ли на самом деле другой путь
Фейд
3

Попытка 1

Если

  1. У меня есть отдельная equipmentтаблица, и
  2. У меня есть индекс на geoposition_records(equipment_id, created_at desc)

тогда у меня работает следующее:

select id as equipment_id, (select max(created_at)
                            from geoposition_records
                            where equipment_id = equipment.id
                           ) as max_created_at
from equipment;

Я не смог заставить PG сделать быстрый запрос , чтобы определить , как список equipment_idс и связанным с этим max(created_at). Но я собираюсь попробовать завтра!

Попытка 2

Я нашел эту ссылку: http://zogovic.com/post/44856908222/optimizing-postgresql-query-for-distinct-values Комбинируя эту технику с моим запросом из попытки 1, я получаю:

WITH RECURSIVE equipment(id) AS (
    SELECT MIN(equipment_id) FROM geoposition_records
  UNION
    SELECT (
      SELECT equipment_id
      FROM geoposition_records
      WHERE equipment_id > equipment.id
      ORDER BY equipment_id
      LIMIT 1
    )
    FROM equipment WHERE id IS NOT NULL
)
SELECT id AS equipment_id, (SELECT MAX(created_at)
                            FROM geoposition_records
                            WHERE equipment_id = equipment.id
                           ) AS max_created_at
FROM equipment;

и это работает БЫСТРО! Но вам нужно

  1. эта ультра-искаженная форма запроса, и
  2. индекс по geoposition_records(equipment_id, created_at desc).
Колин т Харт
источник