it-swarm.com.de

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

Ich benutze PostgreSQL 9.1 unter Ubuntu 12.04.

Ich muss Datensätze innerhalb eines bestimmten Zeitraums auswählen: Meine Tabelle time_limits Hat zwei timestamp Felder und eine integer Eigenschaft. In meiner aktuellen Tabelle befinden sich zusätzliche Spalten, die an dieser Abfrage nicht beteiligt 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 vernünftiger zu sein: irgendwo zwischen 40 und 90 Sekunden.

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

Ich habe zum ersten Mal versucht, explain analyze Zu erhalten, um 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 Scannen der beiden Zeitstempelspalten aufgewendet wird, sobald id_phi Auf 0 Gesetzt ist. Und ich verstehe den großen Scan (60K Zeilen!) Auf den Zeitstempeln nicht. Werden sie nicht durch den Primärschlüssel und idx_inversed Indiziert, die ich hinzugefügt habe?

Sollte ich von Zeitstempeltypen zu etwas anderem wechseln?

Ich habe ein wenig ü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?

105

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 in mehreren Spalten kann dies jedoch einen großen Unterschied bewirken. Eng verwandt:

Betrachten Sie Ihre Anfrage:

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_phi Im Index ist irrelevant. Da es auf Gleichheit (=) Geprüft ist, sollte es an erster Stelle stehen. Du hast das richtig verstanden. Mehr in dieser verwandten Antwort:

Postgres kann in kürzester Zeit zu id_phi = 0 Springen und die folgenden zwei Spalten des passenden Index berücksichtigen. Diese werden mit Bereichsbedingungen in umgekehrter Sortierreihenfolge (<=, >=) Abgefragt. In meinem Index stehen qualifizierende Zeilen an erster Stelle. Sollte der schnellstmögliche Weg mit einem B-Tree-Index sein1::

  • Sie möchten start_date_time <= something: Index hat den frühesten Zeitstempel zuerst.
    • Wenn es qualifiziert ist, überprüfen Sie auch Spalte 3.
      Wiederholen, bis sich die erste Reihe nicht mehr qualifiziert (superschnell).
  • Sie möchten end_date_time >= something: Der Index hat zuerst den neuesten Zeitstempel.
    • Wenn es qualifiziert ist, rufen Sie so lange Zeilen ab, bis die erste nicht mehr funktioniert (superschnell).
      Fahren Sie mit dem nächsten Wert für Spalte 2 fort.

Postgres können entweder vorwärts oder rückwärts scannen. So wie Sie den Index hatten, muss er all Zeilen lesen, die in den ersten beiden Spalten übereinstimmen, und dann filter in der dritten. Lesen Sie unbedingt das Kapitel Indizes und ORDER BY im Handbuch. Es passt ziemlich gut zu Ihrer Frage.

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

Planer schätzt

Der Planer schätzt rows=62682 Für Ihre Beispielabfrage. Von diesen ist keiner qualifiziert (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 denke, 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 mit dem verbesserten Index viel ausmacht.

CLUSTER/pg_repack

Wenn Sie es dennoch schneller möchten, können Sie die physische Reihenfolge der Zeilen in Ihrer Tabelle optimieren. Wenn Sie es sich leisten können, Ihre Tabelle ausschließlich 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 ordnen:

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 in der Tabelle die physische Sortierreihenfolge fragmentieren.

Hauptindex in Postgres 9.2+

1 Mit Seite 9.2+ gibt es eine andere, möglicherweise schnellere Option: einen Hauptindex für eine Bereichsspalte.

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

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

  • Für den vorliegenden mehrspaltigen Gist-Index muss außerdem das zusätzliche Modul btree_Gist installiert sein (einmal pro Datenbank), das die Operatorklassen mit einem integer bereitstellt.

Die Trifecta! A 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 ist für diese Art von Abfrage möglicherweise sogar noch schneller - außer dass, zitiert das Handbuch :

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

Immer noch wahr in Postgres 12.
Sie müssten einen spgist Index für nur (tsrange(...)) Mit einem zweiten btree Index für (id_phi) Kombinieren. Mit dem zusätzlichen Aufwand bin ich mir nicht sicher, ob dies mithalten kann.
Verwandte Antwort mit einem Benchmark für nur eine tsrange Spalte:

177

Erwins Antwort ist jedoch bereits umfassend:

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

Hinweis: hat eingeschränkte Funktionen (verwendet Timestamptz und Sie können nur die Überlappung des Stils '[) afaik haben). Es gibt auch viele andere gute Gründe für ein Upgrade auf PostgreSQL 9.2.

5
nathan-m

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 gepostet, die sich auch auf die Reihenfolge der Indizes in einem mehrspaltigen Index bezieht. Der Schlüssel versucht zunächst, die restriktivsten Bedingungen zu verwenden, um den Suchraum zu verkleinern.

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

3
jap1968

Es gelang mir, schnell zuzunehmen (von 1 Sek. Auf 70 ms).

Ich habe eine Tabelle mit Aggregationen vieler Messungen und vieler Ebenen (l Spalte) (30s, 1m, 1h usw.). Es gibt zwei bereichsgebundene Spalten: $s für Start und $e für Ende.

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

Ich habe die Auswahlabfrage angepasst: Wählen Sie Bereiche aus, deren Startgrenze in einem bestimmten Bereich liegt. Wählen Sie zusätzlich Bereiche aus, deren Endgrenze in einem bestimmten Bereich liegt.

Erklären zeigt zwei Zeilenströme, die unsere Indizes effizient nutzen.

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 Plan-Knoten nur gewünschte Zeilen enthalten. Zuvor hatten wir Tausende von Zeilen im Plan-Knoten, weil er all points from some point in time to the very end, dann entfernte der nächste Knoten unnötige Zeilen.

1
borovsky