Query efficiente per ottenere il massimo valore per gruppo dalla grande tabella


13

Data la tabella:

    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)

La tabella ha 20 milioni di record che non sono, relativamente parlando, un gran numero. Ma rallenta le scansioni sequenziali.

Come posso ottenere l'ultimo record ( max(created_at)) di ciascuno equipment_id?

Ho provato entrambe le seguenti query, con diverse varianti che ho letto attraverso molte risposte di questo argomento:

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;

Ho anche provato a creare indici btree per, equipment_id,created_atma Postgres scopre che usare un seqscan è più veloce. La forzatura non enable_seqscan = offè di alcuna utilità poiché la lettura dell'indice è lenta quanto la scansione seq, probabilmente peggio.

La query deve essere eseguita periodicamente restituendo sempre l'ultimo.

Usare Postgres 9.3.

Spiegare / analizzare (con 1,7 milioni di record):

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"

bene, l'ultima volta che ho verificato che non c'erano NULLvalori nella equipment_idpercentuale attesa inferiore allo 0,1%
Feyd

Risposte:


10

Un semplice indice b-tree a più colonne dovrebbe funzionare dopo tutto:

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

Perché DESC NULLS LAST?

Funzione

Se non riesci a parlare in modo corretto nel pianificatore di query, una funzione che scorre ciclicamente attraverso la tabella delle apparecchiature dovrebbe fare il trucco. La ricerca di un equipment_id alla volta utilizza l'indice. Per un piccolo numero (57 a giudicare dalla tua EXPLAIN ANALYZEuscita), è veloce.
È sicuro supporre che tu abbia un equipmenttavolo?

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;

Fa anche una bella telefonata:

SELECT * FROM f_latest_equip();

Sottoquery correlate

Vieni a pensarci bene, usando questa equipmenttabella, potresti fare il lavoro sporco con subquery scarsamente correlate con grande effetto:

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;

Le prestazioni sono molto buone.

LATERAL iscriviti a 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;

Spiegazione dettagliata:

Prestazioni simili alla sottoquery correlata. Confrontare le prestazioni di max(), DISTINCT ON, funzione, correlato subquery e LATERALin questo:

SQL Fiddle .


1
@ErwinBrandstetter questo è qualcosa che ho provato dopo la risposta di Colin, ma non riesco a smettere di pensare che si tratta di una soluzione alternativa che utilizza una sorta di query lato database n + 1 (non sono sicuro che ciò cada nell'antipasto poiché c'è nessun sovraccarico di connessione) ... Mi chiedo ora perché esiste il raggruppamento, se non riesce a gestire correttamente alcuni milioni di record ... Semplicemente non ha senso, lì? essere qualcosa che ci manca. Infine, la domanda è leggermente cambiata e stiamo ipotizzando la presenza di un tavolo per attrezzature ... Mi piacerebbe sapere se esiste effettivamente un altro modo
Feyd,

3

Tentativo 1

Se

  1. Ho un equipmenttavolo separato e
  2. Ho un indice attivo geoposition_records(equipment_id, created_at desc)

quindi il seguente funziona per me:

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

Non sono stato in grado di forzare PG a fare una query veloce per determinare sia l'elenco di se equipment_idil relativo max(created_at). Ma ci riproverò domani!

Tentativo 2

Ho trovato questo link: http://zogovic.com/post/44856908222/optimizing-postgresql-query-for-distinct-values Combinando questa tecnica con la mia query dal tentativo 1, ottengo:

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;

e questo funziona VELOCEMENTE! Ma tu hai bisogno

  1. questo modulo di query ultra-contorto, e
  2. un indice su geoposition_records(equipment_id, created_at desc).
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.