Ефективний запит, щоб отримати найбільшу цінність для групи з великої таблиці


14

Враховуючи таблицю:

    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не приносить користі, оскільки читання індексу відбувається так само повільно, як і сканування послідовностей, можливо, гірше.

Запит повинен запускатися періодично, повертаючи завжди останній.

Використання 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?

Функція

Якщо ви не можете переказати сенс у планувальнику запитів, функція, що пробирається через таблицю обладнання, повинна зробити свою справу. Переглядаючи одночасно обладнання_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 на базі бази даних (не впевнений, що це потрапляє в антипаттерн, оскільки є немає з'єднання накладних) ... Мені цікаво, чому група взагалі існує, якщо вона не може нормально обробити кілька мільйонів записів ... Це просто не має сенсу? бути чимось, чого нам не вистачає. Нарешті, питання дещо змінилося, і ми припускаємо наявність таблиці обладнання ... Я хотів би дізнатися, чи є насправді інший шлях
Фейд

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_ids, так і пов'язані з ними 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).
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.