Effiziente Abfrage, um den größten Wert pro Gruppe am großen Tisch zu erzielen

13

Angesichts der Tabelle:

    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)

Die Tabelle enthält 20 Millionen Datensätze , was relativ gesehen keine große Anzahl ist. Aber es macht sequentielle Scans langsam.

Wie kann ich den letzten Datensatz ( max(created_at)) von jedem equipment_idabrufen?

Ich habe die beiden folgenden Abfragen mit mehreren Varianten ausprobiert, die ich in vielen Antworten zu diesem Thema gelesen habe:

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;

Ich habe auch versucht, Btree-Indizes für zu erstellen, equipment_id,created_ataber Postgres stellt fest, dass die Verwendung eines Seqscan schneller ist. Forcen enable_seqscan = offnützt auch nichts, da das Lesen des Index so langsam ist wie der Seq-Scan, wahrscheinlich sogar noch schlimmer.

Die Abfrage muss regelmäßig ausgeführt werden und immer die letzte zurückgeben.

Verwenden von Postgres 9.3.

Erklären / analysieren (mit 1,7 Millionen Datensätzen):

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
quelle
Nun, das letzte Mal, als ich überprüft habe, dass es keine NULLWerte im equipment_iderwarteten Prozentsatz gibt, liegt unter 0,1%
Feyd

Antworten:

10

Ein einfacher mehrspaltiger B-Tree-Index sollte schließlich funktionieren:

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

Warum DESC NULLS LAST?

Funktion

Wenn Sie keinen Sinn in den Abfrageplaner einbringen können, sollte eine Funktion, die die Ausrüstungstabelle durchläuft, den Trick tun. Wenn Sie jeweils eine equipment_id nachschlagen, wird der Index verwendet. Für eine kleine Zahl (57 nach Ihrer EXPLAIN ANALYZEAusgabe) ist das schnell.
Nehmen wir an, Sie haben einen equipmentTisch?

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;

Auch für einen netten Anruf:

SELECT * FROM f_latest_equip();

Korrelierte Unterabfragen

equipmentWenn Sie sich diese Tabelle einmal überlegen, können Sie die Drecksarbeit mit niedrig korrelierten Unterabfragen zu einem großen Erfolg machen:

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;

Die Leistung ist sehr gut.

LATERAL Machen Sie mit bei 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;

Ausführliche Erklärung:

Ähnliche Leistung wie die korrelierte Unterabfrage. Vergleicht man die Leistung von max(), DISTINCT ON, Funktion, korrelierte Unterabfrage und LATERALin dieser:

SQL-Geige .

Erwin Brandstetter
quelle
1
@ErwinBrandstetter das ist etwas, was ich nach der Antwort von Colin ausprobiert habe, aber ich kann nicht aufhören zu glauben, dass dies eine Problemumgehung ist, die eine Art datenbankseitiger n + 1-Abfragen verwendet (nicht sicher, ob das in das Antipattern fällt, da es gibt) kein Verbindungsaufwand) ... Ich frage mich jetzt, warum group by überhaupt existiert, wenn es nicht ein paar Millionen Datensätze richtig handhaben kann ... Es macht einfach keinen Sinn, oder? Sei etwas, was wir vermissen. Schließlich hat sich die Frage leicht geändert, und wir gehen davon aus, dass eine Ausrüstungstabelle vorhanden ist. Ich würde gerne wissen, ob es tatsächlich einen anderen Weg gibt
Feyd,
3

Versuch 1

Wenn

  1. Ich habe einen separaten equipmentTisch und
  2. Ich habe einen Index über geoposition_records(equipment_id, created_at desc)

dann funktioniert bei mir folgendes:

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

Ich konnte PG nicht zwingen, eine schnelle Abfrage durchzuführen, um beide Listen zu ermittelnequipment_id s als auch die zugehörige zu ermitteln max(created_at). Aber ich werde es morgen noch einmal versuchen!

Versuch 2

Ich habe diesen Link gefunden: http://zogovic.com/post/44856908222/optimizing-postgresql-query-for-distinct-values Wenn ich diese Technik mit meiner Abfrage aus Versuch 1 kombiniere, erhalte ich:

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;

und das funktioniert SCHNELL! Aber du brauchst

  1. dieses ultra-verzerrte Abfrageformular und
  2. ein Index auf geoposition_records(equipment_id, created_at desc).
Colin 't Hart
quelle