Wydajne zapytanie, aby uzyskać największą wartość na grupę z dużej tabeli

14

Biorąc pod uwagę tabelę:

    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)

Tabela zawiera 20 milionów rekordów, co nie jest stosunkowo dużą liczbą. Powoduje to jednak spowolnienie skanowania sekwencyjnego.

Jak mogę uzyskać ostatni rekord ( max(created_at)) każdego z nich equipment_id?

Próbowałem obu poniższych zapytań, z kilkoma wariantami, które przeczytałem w wielu odpowiedziach na ten temat:

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;

Próbowałem również utworzyć indeksy btree dla, equipment_id,created_atale Postgres odkrył, że użycie seqscan jest szybsze. Wymuszanie również enable_seqscan = offnie ma sensu, ponieważ odczyt indeksu jest tak wolny jak skanowanie seq, prawdopodobnie gorszy.

Kwerenda musi być uruchamiana okresowo, zwracając zawsze ostatnią.

Korzystanie z Postgres 9.3.

Wyjaśnij / przeanalizuj (z 1,7 mln rekordów):

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"
Feyd
źródło
cóż, ostatnim razem, gdy sprawdziłem, nie było żadnych NULLwartości w equipment_idoczekiwanym procencie poniżej 0,1%
Feyd

Odpowiedzi:

10

W końcu powinien działać zwykły wielokolumnowy indeks b-drzewa:

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

Dlaczego DESC NULLS LAST?

Funkcjonować

Jeśli nie możesz mówić rozsądnie w narzędziu do planowania zapytań, funkcja zapętlająca tabelę wyposażenia powinna załatwić sprawę. Wyszukiwanie jednego id_urządzenia na raz korzysta z indeksu. W przypadku małej liczby (57 sądząc po EXPLAIN ANALYZEwynikach) jest to szybkie.
Można bezpiecznie założyć, że masz equipmentstolik?

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;

Sprawia również, że jest to miły telefon:

SELECT * FROM f_latest_equip();

Skorelowane podzapytania

Pomyśl o tym, korzystając z tej equipmenttabeli, możesz wykonać brudną pracę z mało skorelowanymi podkwerendami, aby uzyskać doskonały efekt:

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;

Wydajność jest bardzo dobra.

LATERAL dołącz do 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;

Szczegółowe wyjaśnienie:

Podobna wydajność jak skorelowane podzapytanie. Porównywanie wydajności max(), DISTINCT ON, funkcja, skorelowane podzapytania i LATERALw ten sposób:

SQL Fiddle .

Erwin Brandstetter
źródło
1
@ErwinBrandstetter to jest coś, czego próbowałem po odpowiedzi od Colina, ale nie mogę przestać myśleć, że jest to obejście, które wykorzystuje rodzaj zapytań n + 1 po stronie bazy danych (nie jestem pewien, czy to wchodzi w antypattern, ponieważ istnieje brak narzutu na połączenie) ... Zastanawiam się teraz, dlaczego w ogóle istnieje grupa, jeśli nie może poprawnie obsłużyć kilku milionów rekordów ... To po prostu nie ma sensu, prawda? być czymś, za czym tęsknimy. Wreszcie pytanie nieznacznie się zmieniło i zakładamy obecność stołu z wyposażeniem ... Chciałbym wiedzieć, czy istnieje inny sposób
Feyd
3

Próba 1

Gdyby

  1. Mam osobny equipmentstolik i
  2. Mam indeks na geoposition_records(equipment_id, created_at desc)

to dla mnie działa:

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

Nie byłem w stanie zmusić PG do wykonania szybkiego zapytania w celu ustalenia zarówno listy equipment_ids, jak i pokrewnych max(created_at). Ale jutro spróbuję ponownie!

Próba 2

Znalazłem ten link: http://zogovic.com/post/44856908222/optimizing-postgresql-query-for-distinct-values Łącząc tę ​​technikę z moim zapytaniem z próby 1, otrzymuję:

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;

i to działa SZYBKO! Ale ty potrzebujesz

  1. ten ultra-wykrzywiony formularz zapytania i
  2. indeks na geoposition_records(equipment_id, created_at desc).
Colin 't Hart
źródło