Requête efficace pour obtenir la plus grande valeur par groupe à partir d'une grande table


13

Compte tenu du tableau:

    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)

Le tableau compte 20 millions d'enregistrements, ce qui n'est pas, relativement parlant, un grand nombre. Mais cela ralentit les analyses séquentielles.

Comment puis-je obtenir le dernier enregistrement ( max(created_at)) de chacun equipment_id?

J'ai essayé les deux requêtes suivantes, avec plusieurs variantes que j'ai lues à travers de nombreuses réponses de ce sujet:

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;

J'ai également essayé de créer des index btree pour equipment_id,created_atmais Postgres trouve que l'utilisation d'un seqscan est plus rapide. Le forçage enable_seqscan = offn'est d'aucune utilité non plus, car la lecture de l'index est aussi lente que le scan séquentiel, probablement pire.

La requête doit être exécutée périodiquement en renvoyant toujours la dernière.

Utiliser Postgres 9.3.

Expliquez / analysez (avec 1,7 million d'enregistrements):

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"

enfin la dernière fois que j'ai vérifié qu'il n'y avait pas de NULLvaleurs dans equipment_idle pourcentage attendu est en dessous de 0,1%
Feyd

Réponses:


10

Un index b-tree multicolonne simple devrait fonctionner après tout:

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

Pourquoi DESC NULLS LAST?

Une fonction

Si vous ne parvenez pas à comprendre le sens dans le planificateur de requêtes, une fonction faisant une boucle dans la table d'équipement devrait faire l'affaire. La recherche d'un ID d'équipement à la fois utilise l'index. Pour un petit nombre (57 à en juger par votre EXPLAIN ANALYZEsortie), c'est rapide.
Est-il sûr de supposer que vous avez une equipmenttable?

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;

Fait aussi un bon appel:

SELECT * FROM f_latest_equip();

Sous-requêtes corrélées

À bien y penser, en utilisant ce equipmenttableau, vous pourriez faire le sale boulot avec des sous-requêtes faiblement corrélées à grand effet:

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;

Les performances sont très bonnes.

LATERAL rejoindre 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;

Explication détaillée:

Performances similaires à la sous-requête corrélée. Comparer les performances de max(), DISTINCT ON, la fonction, sous - requête corrélée et LATERALdans celle - ci:

SQL Fiddle .


1
@ErwinBrandstetter, c'est quelque chose que j'ai essayé après la réponse de Colin, mais je ne peux pas m'arrêter de penser que c'est une solution de contournement qui utilise une sorte de requêtes de base de données n + 1 (je ne sais pas si cela tombe dans l'anti-motif car il y a pas de frais généraux de connexion) ... Je me demande maintenant pourquoi le regroupement existe, s'il ne peut pas gérer correctement quelques millions d'enregistrements ... Cela n'a pas de sens, n'est-ce pas? être quelque chose qui nous manque. Enfin, la question a légèrement changé et nous supposons la présence d'une table d'équipement ... J'aimerais savoir s'il y a réellement une autre façon
Feyd

3

Tentative 1

Si

  1. J'ai une equipmenttable séparée et
  2. J'ai un index sur geoposition_records(equipment_id, created_at desc)

alors ce qui suit fonctionne pour moi:

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

Je n'ai pas pu forcer PG à effectuer une requête rapide pour déterminer à la fois la liste des equipment_ids et les éléments associés max(created_at). Mais je vais réessayer demain!

Tentative 2

J'ai trouvé ce lien: http://zogovic.com/post/44856908222/optimizing-postgresql-query-for-distinct-values En combinant cette technique avec ma requête de la tentative 1, j'obtiens:

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;

et cela fonctionne RAPIDEMENT! Mais tu as besoin

  1. ce formulaire de requête ultra-contorsionné, et
  2. un index sur geoposition_records(equipment_id, created_at desc).
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.