Optimieren von Abfragen für eine Reihe von Zeitstempeln (zwei Spalten)

96

Ich benutze PostgreSQL 9.1 unter Ubuntu 12.04.

Ich muss Datensätze innerhalb eines bestimmten Zeitraums auswählen: Meine Tabelle time_limitsenthält zwei timestampFelder und eine integerEigenschaft. In meiner aktuellen Tabelle befinden sich zusätzliche Spalten, die nicht mit dieser Abfrage verknüpft sind.

create table (
   start_date_time timestamp,
   end_date_time timestamp, 
   id_phi integer, 
   primary key(start_date_time, end_date_time,id_phi);

Diese Tabelle enthält ungefähr 2 Millionen Datensätze.

Abfragen wie die folgenden haben enorm viel Zeit in Anspruch genommen:

select * from time_limits as t 
where t.id_phi=0 
and t.start_date_time <= timestamp'2010-08-08 00:00:00'
and t.end_date_time   >= timestamp'2010-08-08 00:05:00';

Also habe ich versucht, einen weiteren Index hinzuzufügen - die Umkehrung der PK:

create index idx_inversed on time_limits(id_phi, start_date_time, end_date_time);

Ich hatte den Eindruck, dass sich die Leistung verbessert hat: Die Zeit für den Zugriff auf Datensätze in der Mitte der Tabelle scheint angemessener zu sein: irgendwo zwischen 40 und 90 Sekunden.

Bei Werten in der Mitte des Zeitbereichs sind es jedoch noch einige zehn Sekunden. Und zweimal mehr, wenn Sie das Ende der Tabelle anvisieren (chronologisch gesehen).

Ich habe explain analyzezum ersten Mal versucht , diesen Abfrageplan zu erhalten:

 Bitmap Heap Scan on time_limits  (cost=4730.38..22465.32 rows=62682 width=36) (actual time=44.446..44.446 rows=0 loops=1)
   Recheck Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
   ->  Bitmap Index Scan on idx_time_limits_phi_start_end  (cost=0.00..4714.71 rows=62682 width=0) (actual time=44.437..44.437 rows=0 loops=1)
         Index Cond: ((id_phi = 0) AND (start_date_time <= '2011-08-08 00:00:00'::timestamp without time zone) AND (end_date_time >= '2011-08-08 00:05:00'::timestamp without time zone))
 Total runtime: 44.507 ms

Siehe die Ergebnisse auf depesz.com.

Was kann ich tun, um die Suche zu optimieren? Sie können sehen, wie viel Zeit für das Durchsuchen der beiden Zeitstempelspalten aufgewendet wurde, wenn auf eingestellt id_phiist 0. Und ich verstehe den großen Scan (60K Zeilen!) Auf den Zeitstempeln nicht. Werden sie nicht durch den Primärschlüssel indiziert und idx_inversedich fügte hinzu?

Sollte ich von Zeitstempeltypen zu etwas anderem wechseln?

Ich habe etwas über GIST- und GIN-Indizes gelesen. Ich gehe davon aus, dass sie unter bestimmten Bedingungen für benutzerdefinierte Typen effizienter sein können. Ist es eine praktikable Option für meinen Anwendungsfall?

Stephane Rolland
quelle
1
Nun, es ist 45 Jahre. Ich weiß nicht, warum es 45ms sagt. Ich würde mich nicht einmal beschweren, wenn das so schnell wie 45ms wäre ... :-) Vielleicht ein Fehler in der Ausgabe von EXPLAIN ANALYSE. Oder vielleicht ist es an der Zeit, die Analyse durchzuführen. Keine Ahnung. Aber 40/50 Sekunden messe ich.
Stephane Rolland
2
Die in der explain analyzeAusgabe angegebene Zeit ist die Zeit, die die Abfrage auf dem Server benötigt . Wenn Ihre Abfrage 45 Sekunden dauert, wird die zusätzliche Zeit für die Übertragung der Daten von der Datenbank an das Programm aufgewendet, in dem die Abfrage ausgeführt wird. Immerhin sind es 62682 Zeilen. Wenn jede Zeile groß ist (z. B. lang varcharoder spaltenweise text), kann dies die Übertragungszeit beeinträchtigen drastisch.
a_horse_with_no_name
@a_horse_with_no_name: rows=62682 rowsist die Schätzung des Planers . Die Abfrage gibt 0 Zeilen zurück. (actual time=44.446..44.446 rows=0 loops=1)
Erwin Brandstetter
@ ErwinBrandstetter: ah, richtig. Ich habe das übersehen. Trotzdem habe ich die Ausgabe von EXPLAIN ANALYSE noch nie über die Ausführungszeit gesehen.
a_horse_with_no_name

Antworten:

162

Für Postgres 9.1 oder höher:

CREATE INDEX idx_time_limits_ts_inverse
ON time_limits (id_phi, start_date_time, end_date_time DESC);

In den meisten Fällen ist die Sortierreihenfolge eines Index kaum relevant. Postgres können praktisch genauso schnell rückwärts scannen. Bei Bereichsabfragen für mehrere Spalten kann dies jedoch einen großen Unterschied bewirken . Eng verwandt:

Betrachten Sie Ihre Frage:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    start_date_time <= '2010-08-08 00:00'
AND    end_date_time   >= '2010-08-08 00:05';

Die Sortierreihenfolge der ersten Spalte id_phiim Index spielt keine Rolle. Da es auf Gleichheit geprüft wird ( =), sollte es an erster Stelle stehen. Das hast du richtig erkannt. Mehr in dieser verwandten Antwort:

Postgres kann id_phi = 0in kürzester Zeit zu den folgenden zwei Spalten des übereinstimmenden Index springen . Diese werden mit Bereichsbedingungen der umgekehrten Sortierreihenfolge ( <=, >=) abgefragt . In meinem Index stehen qualifizierende Zeilen an erster Stelle. Sollte mit einem B-Tree-Index 1 der schnellste Weg sein :

  • Sie möchten start_date_time <= something: index hat den frühesten Zeitstempel zuerst.
    • Wenn es qualifiziert ist, überprüfen Sie auch Spalte 3.
      Rekursieren Sie, bis die erste Zeile nicht mehr qualifiziert ist (superschnell).
  • Sie möchten end_date_time >= something: index hat den neuesten Zeitstempel zuerst.
    • Wenn dies der Fall ist, holen Sie so lange Zeilen, bis dies beim ersten nicht mehr der Fall ist (superschnell).
      Fahren Sie mit dem nächsten Wert für Spalte 2 fort.

Postgres kann vorwärts oder rückwärts scannen . So wie Sie den Index hatten, muss er alle übereinstimmenden Zeilen in den ersten beiden Spalten lesen und dann in der dritten filtern . Lesen Sie unbedingtORDER BY das Kapitel Indexe und das Handbuch. Es passt ziemlich gut zu deiner Frage.

Wie viele Zeilen stimmen in den ersten beiden Spalten überein?
Nur wenige mit start_date_timeknapp vor dem Beginn des Zeitbereichs der Tabelle. Aber fast alle Zeilen mit id_phi = 0am chronologischen Ende der Tabelle! Daher verschlechtert sich die Leistung mit späteren Startzeiten.

Planer schätzt

Der Planer schätzt rows=62682für Ihre Beispielabfrage. Von diesen qualifizieren sich keine ( rows=0). Sie erhalten möglicherweise bessere Schätzungen, wenn Sie das Statistikziel für die Tabelle erhöhen. Für 2.000.000 Zeilen ...

ALTER TABLE time_limits ALTER start_date_time SET STATISTICS 1000;
ALTER TABLE time_limits ALTER end_date_time   SET STATISTICS 1000;

... könnte bezahlen. Oder noch höher. Mehr in dieser verwandten Antwort:

Ich vermute, Sie brauchen das nicht für id_phi(nur wenige unterschiedliche Werte, gleichmäßig verteilt), sondern für die Zeitstempel (viele unterschiedliche Werte, ungleich verteilt).
Ich denke auch nicht, dass es für den verbesserten Index wichtig ist.

CLUSTER / pg_repack

Wenn Sie es dennoch schneller wollen, können Sie die physische Reihenfolge der Zeilen in Ihrer Tabelle optimieren. Wenn Sie es sich leisten können, Ihre Tabelle nur für einen kurzen Zeitraum (z. B. außerhalb der Geschäftszeiten) zu sperren, um Ihre Tabelle neu zu schreiben und Zeilen gemäß dem Index zu bestellen:

ALTER TABLE time_limits CLUSTER ON idx_time_limits_inversed;

Berücksichtigen Sie bei gleichzeitigem Zugriff pg_repack , was auch ohne exklusive Sperre möglich ist.

In beiden Fällen müssen weniger Blöcke aus der Tabelle gelesen werden, und alles ist vorsortiert. Es ist ein einmaliger Effekt, der sich mit der Zeit verschlechtert, wenn Schreibvorgänge auf den Tisch die physische Sortierreihenfolge fragmentieren.

GiST-Index in Postgres 9.2+

1 Ab S. 9.2 gibt es eine weitere, möglicherweise schnellere Option: einen GiST-Index für eine Range-Spalte.

  • Es gibt integrierte Bereichstypen für timestampund timestamp with time zone: tsrange,tstzrange . Ein Btree-Index ist in der Regel schneller für eine zusätzliche integerSpalte wie id_phi. Auch kleiner und billiger zu warten. Aber die Abfrage wird mit dem kombinierten Index insgesamt wahrscheinlich noch schneller sein.

  • Ändern Sie Ihre Tabellendefinition oder verwenden Sie einen Ausdrucksindex .

  • Für den vorliegenden mehrspaltigen GiST-Index muss außerdem das Zusatzmodul btree_gistinstalliert sein (einmal pro Datenbank), das den Operator-Klassen die Möglichkeit bietet, ein einzuschließen integer.

Die Trifecta! Ein mehrspaltiger funktionaler GiST-Index :

CREATE EXTENSION IF NOT EXISTS btree_gist;  -- if not installed, yet

CREATE INDEX idx_time_limits_funky ON time_limits USING gist
(id_phi, tsrange(start_date_time, end_date_time, '[]'));

Verwenden Sie jetzt den Operator "enthält Bereich"@> in Ihrer Abfrage:

SELECT *
FROM   time_limits
WHERE  id_phi = 0
AND    tsrange(start_date_time, end_date_time, '[]')
    @> tsrange('2010-08-08 00:00', '2010-08-08 00:05', '[]')

SP-GiST-Index in Postgres 9.3+

Ein SP-GiST- Index könnte für diese Art von Abfrage sogar noch schneller sein - mit der Ausnahme, dass das Handbuch wie folgt zitiert wird :

Derzeit unterstützen nur die Indextypen B-Tree, GiST, GIN und BRIN mehrspaltige Indizes.

In Postgres 12 immer noch wahr.
Sie müssten einen spgistIndex für nur (tsrange(...))mit einem zweiten btreeIndex für kombinieren (id_phi). Mit dem zusätzlichen Aufwand bin ich nicht sicher, ob dies konkurrieren kann.
Verwandte Antwort mit einem Benchmark für nur eine tsrangeSpalte:

Erwin Brandstetter
quelle
78
Ich sollte dies mindestens einmal erwähnen , dass jede Ihrer Antworten zu SO und DBA einen wirklich hohen Mehrwert / Sachverstand aufweist und die meiste Zeit die vollständigste ist. Nur um es einmal zu sagen: Respekt !.
Stephane Rolland
1
Merci bien! :) Hast du also schnellere Ergebnisse erzielt?
Erwin Brandstetter
Ich muss die große Massenkopie fertigstellen lassen, die aus meiner äußerst umständlichen Abfrage generiert wurde, sodass der Prozess sehr langsam wurde und sich stundenlang drehte, bevor ich die Frage stellte. Aber ich habe gerechnet, und ich habe beschlossen, es bis morgen früh drehen zu lassen, es wird fertig sein und der neue Tisch kann morgen gefüllt werden. Ich habe versucht, Ihren Index gleichzeitig während des Jobs zu erstellen, aber aufgrund von zu viel Zugriff (glaube ich) sollte die Erstellung des Index gesperrt werden. Ich werde diesen Test morgen mit Ihrer Lösung noch einmal wiederholen. Ich habe mir auch angesehen, wie ein Upgrade auf 9.2 ;-) für Debian / Ubuntu durchgeführt wird.
Stephane Rolland
2
@StephaneRolland: Es wäre immer noch interessant, warum die EXPLAIN-Analyse-Ausgabe 45 Millisekunden anzeigt, während die Abfrage mehr als 40 Sekunden dauert.
a_horse_with_no_name
1
@John: Postgres kann einen Index vorwärts oder rückwärts durchlaufen, aber nicht die Richtung im selben Scan ändern. Idealerweise haben Sie alle qualifizierenden Zeilen pro Knoten zuerst (oder zuletzt), aber es muss dieselbe Ausrichtung (übereinstimmende Abfrageprädikate) für alle Spalten vorliegen, um die besten Ergebnisse zu erzielen.
Erwin Brandstetter
5

Erwins Antwort ist jedoch bereits umfassend:

Bereichstypen für Zeitstempel sind in PostgreSQL 9.1 mit der temporären Erweiterung von Jeff Davis verfügbar: https://github.com/jeff-davis/PostgreSQL-Temporal

Hinweis: Hat eingeschränkte Funktionen (verwendet Timestamptz, und Sie können nur den Stil '[)' überlappen lassen). Es gibt auch viele andere gute Gründe für ein Upgrade auf PostgreSQL 9.2.

Nathan-m
quelle
3

Sie können versuchen, den mehrspaltigen Index in einer anderen Reihenfolge zu erstellen:

primary key(id_phi, start_date_time,end_date_time);

Ich habe einmal eine ähnliche Frage gestellt, die sich auch auf die Reihenfolge von Indizes in einem mehrspaltigen Index bezieht. Der Schlüssel ist, zuerst die restriktivsten Bedingungen zu verwenden, um den Suchraum zu reduzieren.

Edit : Mein Fehler. Jetzt sehe ich, dass Sie diesen Index bereits definiert haben.

jap1968
quelle
Ich habe bereits beide Index. Außer der Primärschlüssel ist der andere, aber der von Ihnen vorgeschlagene Index existiert bereits und wird verwendet, wenn Sie sich die Erklärung ansehen:Bitmap Index Scan on idx_time_limits_phi_start_end
Stephane Rolland
1

Ich habe es geschafft, schnell zuzunehmen (von 1 Sekunde auf 70 ms)

Ich habe eine Tabelle mit Aggregationen von vielen Messungen und vielen Ebenen ( lSpalte) (30s, 1m, 1h, usw.). Es gibt zwei bereichsgebundene Spalten: $sfür Anfang und $efür Ende.

Ich habe zwei mehrspaltige Indizes erstellt: einen für Start und einen für Ende.

Ich habe die Auswahlabfrage angepasst: Wähle Bereiche aus, in denen die Startgrenze im angegebenen Bereich liegt. Wählen Sie zusätzlich Bereiche aus, deren Endgrenze im angegebenen Bereich liegt.

Erklären Sie, dass zwei Zeilenströme mit unseren Indizes effizient verwendet werden.

Indizes:

drop index if exists agg_search_a;
CREATE INDEX agg_search_a
ON agg (measurement_id, l, "$s");

drop index if exists agg_search_b;
CREATE INDEX agg_search_b
ON agg (measurement_id, l, "$e");

Abfrage auswählen:

select "$s", "$e", a, t, b, c from agg
where 
    measurement_id=0 
    and l =  '30s'
    and (
        (
            "$s" > '2013-05-01 02:05:05'
            and "$s" < '2013-05-01 02:18:15'
        )
        or 
        (
             "$e" > '2013-05-01 02:00:05'
            and "$e" < '2013-05-01 02:18:05'
        )
    )

;

Erklären:

[
  {
    "Execution Time": 0.058,
    "Planning Time": 0.112,
    "Plan": {
      "Startup Cost": 10.18,
      "Rows Removed by Index Recheck": 0,
      "Actual Rows": 37,
      "Plans": [
    {
      "Startup Cost": 10.18,
      "Actual Rows": 0,
      "Plans": [
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 26,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone))",
          "Plan Rows": 29,
          "Parallel Aware": false,
          "Actual Total Time": 0.016,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.016,
          "Total Cost": 5,
          "Actual Loops": 1,
          "Index Name": "agg_search_a"
        },
        {
          "Startup Cost": 0,
          "Plan Width": 0,
          "Actual Rows": 36,
          "Node Type": "Bitmap Index Scan",
          "Index Cond": "((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone))",
          "Plan Rows": 39,
          "Parallel Aware": false,
          "Actual Total Time": 0.011,
          "Parent Relationship": "Member",
          "Actual Startup Time": 0.011,
          "Total Cost": 5.15,
          "Actual Loops": 1,
          "Index Name": "agg_search_b"
        }
      ],
      "Node Type": "BitmapOr",
      "Plan Rows": 68,
      "Parallel Aware": false,
      "Actual Total Time": 0.027,
      "Parent Relationship": "Outer",
      "Actual Startup Time": 0.027,
      "Plan Width": 0,
      "Actual Loops": 1,
      "Total Cost": 10.18
    }
      ],
      "Exact Heap Blocks": 1,
      "Node Type": "Bitmap Heap Scan",
      "Plan Rows": 68,
      "Relation Name": "agg",
      "Alias": "agg",
      "Parallel Aware": false,
      "Actual Total Time": 0.037,
      "Recheck Cond": "(((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$s\" > '2013-05-01 02:05:05'::timestamp without time zone) AND (\"$s\" < '2013-05-01 02:18:15'::timestamp without time zone)) OR ((measurement_id = 0) AND ((l)::text = '30s'::text) AND (\"$e\" > '2013-05-01 02:00:05'::timestamp without time zone) AND (\"$e\" < '2013-05-01 02:18:05'::timestamp without time zone)))",
      "Lossy Heap Blocks": 0,
      "Actual Startup Time": 0.033,
      "Plan Width": 44,
      "Actual Loops": 1,
      "Total Cost": 280.95
    },
    "Triggers": []
  }
]

Der Trick ist, dass Ihre Planknoten nur gewünschte Zeilen enthalten. Zuvor hatten wir Tausende von Zeilen im Plan-Knoten, weil dieser ausgewählt all points from some point in time to the very endund dann vom nächsten Knoten nicht benötigte Zeilen entfernt wurde.

borovsky
quelle