Optimieren einer "neuesten" Abfrage in Postgres für 20 Millionen Zeilen

10

Mein Tisch sieht wie folgt aus:

    Column             |    Type           |    
-----------------------+-------------------+
 id                    | integer           | 
 source_id             | integer           | 
 timestamp             | integer           | 
 observation_timestamp | integer           | 
 value                 | double precision  | 

Indizes existieren für source_id, timestamp und für eine Kombination aus timestamp und id ( CREATE INDEX timeseries_id_timestamp_combo_idx ON timeseries (id, timeseries DESC NULLS LAST))

Es gibt 20 Millionen Zeilen (OK, es gibt 120 Millionen, aber 20 Millionen mit source_id = 1). Es hat viele Einträge für das gleiche timestampmit variierenden observation_timestamp, die ein valueaufgetretenes bei timestampgemeldeten oder beobachteten bei beschreiben observation_timestamp. zB Die für morgen 14 Uhr vorhergesagte Temperatur, wie heute um 12 Uhr vorhergesagt.

Idealerweise macht dieser Tisch ein paar Dinge gut:

  • Batch-Einfügen neuer Einträge, manchmal 100 KB gleichzeitig
  • Auswahl der für Zeitbereiche beobachteten Daten ("Wie lauten die Temperaturvorhersagen für Januar bis März")
  • Auswählen von Daten, die für Zeitabschnitte beobachtet wurden, wie von einem bestimmten Punkt aus beobachtet ("Wie sehen die Temperaturvorhersagen für Januar bis März aus, wie wir am 1. November gedacht haben")

Der zweite ist derjenige, der für diese Frage von zentraler Bedeutung ist.

Die Daten in der Tabelle sehen wie folgt aus

id  source_id   timestamp   observation_timestamp   value
1   1           1531084900  1531083900              9999
2   1           1531084900  1531082900              1111
3   1           1531085900  1531083900              8888
4   1           1531085900  1531082900              7777
5   1           1531086900  1531082900              5555

und eine Ausgabe der Abfrage würde wie folgt aussehen (nur die Zeile des letzten beobachteten Beobachtungsstempels)

id  source_id   timestamp   observation_timestamp   value
1   1           1531084900  1531083900              9999
3   1           1531085900  1531083900              8888
5   1           1531086900  1531082900              5555

Ich habe bereits einige Materialien konsultiert, um diese Abfragen zu optimieren, nämlich

... mit begrenztem Erfolg.

Ich habe darüber nachgedacht, eine separate Tabelle mit timestampdieser Tabelle zu erstellen , damit sie leichter seitlich referenziert werden kann. Aufgrund der relativ hohen Kardinalität derjenigen bezweifle ich jedoch, dass sie mir helfen werden. Außerdem mache ich mir Sorgen, dass dies nicht möglich ist batch inserting new entries.


Ich sehe mir drei Fragen an, und alle geben mir eine schlechte Leistung

  • Rekursiver CTE mit LATERAL Join
  • Fensterfunktion
  • UNTERSCHEIDUNG EIN

(Ich bin mir bewusst, dass sie im Moment nicht ganz dasselbe tun, aber sie dienen meines Erachtens als gute Beispiele für die Art der Abfrage.)

Rekursiver CTE mit LATERAL Join

WITH RECURSIVE cte AS (
    (
        SELECT ts
        FROM timeseries ts
        WHERE source_id = 1
        ORDER BY id, "timestamp" DESC NULLS LAST
        LIMIT 1
    )
    UNION ALL
    SELECT (
        SELECT ts1
        FROM timeseries ts1
        WHERE id > (c.ts).id
        AND source_id = 1
        ORDER BY id, "timestamp" DESC NULLS LAST
        LIMIT 1
    )
    FROM cte c
    WHERE (c.ts).id IS NOT NULL
)
SELECT (ts).*
FROM cte
WHERE (ts).id IS NOT NULL
ORDER BY (ts).id;

Performance:

Sort  (cost=164999681.98..164999682.23 rows=100 width=28)
  Sort Key: ((cte.ts).id)
  CTE cte
    ->  Recursive Union  (cost=1653078.24..164999676.64 rows=101 width=52)
          ->  Subquery Scan on *SELECT* 1  (cost=1653078.24..1653078.26 rows=1 width=52)
                ->  Limit  (cost=1653078.24..1653078.25 rows=1 width=60)
                      ->  Sort  (cost=1653078.24..1702109.00 rows=19612304 width=60)
                            Sort Key: ts.id, ts.timestamp DESC NULLS LAST
                            ->  Bitmap Heap Scan on timeseries ts  (cost=372587.92..1555016.72 rows=19612304 width=60)
                                  Recheck Cond: (source_id = 1)
                                  ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367684.85 rows=19612304 width=0)
                                        Index Cond: (source_id = 1)
          ->  WorkTable Scan on cte c  (cost=0.00..16334659.64 rows=10 width=32)
                Filter: ((ts).id IS NOT NULL)
                SubPlan 1
                  ->  Limit  (cost=1633465.94..1633465.94 rows=1 width=60)
                        ->  Sort  (cost=1633465.94..1649809.53 rows=6537435 width=60)
                              Sort Key: ts1.id, ts1.timestamp DESC NULLS LAST
                              ->  Bitmap Heap Scan on timeseries ts1  (cost=369319.21..1600778.77 rows=6537435 width=60)
                                    Recheck Cond: (source_id = 1)
                                    Filter: (id > (c.ts).id)
                                    ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367684.85 rows=19612304 width=0)
                                          Index Cond: (source_id = 1)
  ->  CTE Scan on cte  (cost=0.00..2.02 rows=100 width=28)
        Filter: ((ts).id IS NOT NULL)

(nur EXPLAIN, EXPLAIN ANALYZEkonnte nicht abgeschlossen werden, dauerte> 24 Stunden, um die Abfrage abzuschließen)

Fensterfunktion

WITH summary AS (
  SELECT ts.id, ts.source_id, ts.value,
    ROW_NUMBER() OVER(PARTITION BY ts.timestamp ORDER BY ts.observation_timestamp DESC) AS rn
  FROM timeseries ts
  WHERE source_id = 1
)
SELECT s.*
FROM summary s
WHERE s.rn = 1;

Performance:

CTE Scan on summary s  (cost=5530627.97..5971995.66 rows=98082 width=24) (actual time=150368.441..226331.286 rows=88404 loops=1)
  Filter: (rn = 1)
  Rows Removed by Filter: 20673704
  CTE summary
    ->  WindowAgg  (cost=5138301.13..5530627.97 rows=19616342 width=32) (actual time=150368.429..171189.504 rows=20762108 loops=1)
          ->  Sort  (cost=5138301.13..5187341.98 rows=19616342 width=24) (actual time=150368.405..165390.033 rows=20762108 loops=1)
                Sort Key: ts.timestamp, ts.observation_timestamp DESC
                Sort Method: external merge  Disk: 689752kB
                ->  Bitmap Heap Scan on timeseries ts  (cost=372675.22..1555347.49 rows=19616342 width=24) (actual time=2767.542..50399.741 rows=20762108 loops=1)
                      Recheck Cond: (source_id = 1)
                      Rows Removed by Index Recheck: 217784
                      Heap Blocks: exact=48415 lossy=106652
                      ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367771.13 rows=19616342 width=0) (actual time=2757.245..2757.245 rows=20762630 loops=1)
                            Index Cond: (source_id = 1)
Planning time: 0.186 ms
Execution time: 234883.090 ms

UNTERSCHEIDUNG EIN

SELECT DISTINCT ON (timestamp) *
FROM timeseries
WHERE source_id = 1
ORDER BY timestamp, observation_timestamp DESC;

Performance:

Unique  (cost=5339449.63..5437531.34 rows=15991 width=28) (actual time=112653.438..121397.944 rows=88404 loops=1)
  ->  Sort  (cost=5339449.63..5388490.48 rows=19616342 width=28) (actual time=112653.437..120175.512 rows=20762108 loops=1)
        Sort Key: timestamp, observation_timestamp DESC
        Sort Method: external merge  Disk: 770888kB
        ->  Bitmap Heap Scan on timeseries  (cost=372675.22..1555347.49 rows=19616342 width=28) (actual time=2091.585..56109.942 rows=20762108 loops=1)
              Recheck Cond: (source_id = 1)
              Rows Removed by Index Recheck: 217784
              Heap Blocks: exact=48415 lossy=106652
              ->  Bitmap Index Scan on ix_timeseries_source_id  (cost=0.00..367771.13 rows=19616342 width=0) (actual time=2080.054..2080.054 rows=20762630 loops=1)
                    Index Cond: (source_id = 1)
Planning time: 0.132 ms
Execution time: 161651.006 ms

Wie soll ich meine Daten strukturieren, gibt es Scans, die nicht vorhanden sein sollten, ist es im Allgemeinen möglich, diese Abfragen auf ~ 1s (anstelle von ~ 120s) zu bringen?

Gibt es eine andere Möglichkeit, die Daten abzufragen, um die gewünschten Ergebnisse zu erzielen?

Wenn nicht, welche unterschiedliche Infrastruktur / Architektur sollte ich betrachten?

Pepijn Schön
quelle
Was Sie im Wesentlichen wollen, ist ein Lose-Index-Scan oder ein Skip-Scan. Die kommen bald. Sie können den Patch jetzt anwenden, wenn Sie ihn durcheinander bringen möchten. Postgresql-archive.org/Index-Skip-Scan-td6025532.html Er ist kaum einen Monat alt = P
Evan Carroll
Livin 'am Rande @EvanCarroll = P - das scheint mir etwas zu früh, wenn man bedenkt, dass ich Postgres auf Azure verwende, was nicht einmal machbar ist.
Pepijn Schoen
Bitte zeigen Sie EXPLAIN ANALYZE-Pläne ohne die LIMITs (da dies optimiert werden muss), aber mit den Änderungen, die ich in meiner ersten Antwort empfohlen habe. Aber ohne die GRENZEN, denke ich, fordern Sie eine unmögliche Menge an Arbeit, die in ~ 1s erledigt werden muss. Vielleicht können Sie einige Dinge vorberechnen.
Jjanes
@jjanes absolut - danke für den Vorschlag. Ich habe die entfernt LIMITvon der Frage ist nun, und addierte Ausgabe mit EXPLAIN ANALYZE(nur EXPLAINauf dem recursiveTeil though)
Pepijn Schoen

Antworten:

1

Bei Ihrer rekursiven CTE-Abfrage ist das Finale ORDER BY (ts).idnicht erforderlich, da der CTE sie automatisch in dieser Reihenfolge erstellt. Wenn Sie dies entfernen, um die Abfrage viel schneller zu machen, kann sie vorzeitig beendet werden, anstatt 20.180.572 Zeilen zu generieren, um alle bis auf 500 wegzuwerfen. Durch den Aufbau des Index (source_id, id, timestamp desc nulls last)sollte dieser weiter verbessert werden.

Bei den beiden anderen Abfragen würde es einigen helfen, work_mem so weit zu erhöhen, dass die Bitmaps in den Speicher passen (um verlustbehaftete Heap-Blöcke zu entfernen). Aber nicht so viel wie benutzerdefinierte Indizes, wie (source_id, "timestamp", observation_timestamp DESC)oder besser noch für Index-Scans (source_id, "timestamp", observation_timestamp DESC, value, id).

jjanes
quelle
Vielen Dank für den Vorschlag - ich werde mich sicherlich mit der benutzerdefinierten Indizierung befassen, wie Sie es vorschlagen. Das LIMIT 500war für mich gedacht, um die Ausgabe zu begrenzen, aber im Produktionscode passiert dies nicht. Ich werde meinen Beitrag bearbeiten, um dies widerzuspiegeln.
Pepijn Schoen
Ohne das LIMIT könnten die Indizes viel weniger effektiv sein. Aber immer noch einen Versuch wert.
jjanes
Sie haben Recht - mit dem LIMITund Ihren Vorschlägen ist die Ausführung derzeit 356.482 ms( Index Scan using ix_timeseries_source_id_timestamp_observation_timestamp on timeseries (cost=0.57..62573201.42 rows=18333374 width=28) (actual time=174.098..356.097 rows=2995 loops=1)), aber ohne LIMITwie zuvor. Wie würde ich Index Scanin diesem Fall auch das nutzen und nicht das Bitmap Index/Heap Scan?
Pepijn Schoen