Optimieren von Abfragen für eine Reihe von Zeitstempeln (eine Spalte)

8

Ich benutze Postgres 9.3 über Heroku.

Ich habe eine Tabelle, "Verkehr", mit 1M + Datensätzen, die jeden Tag viele Einfügungen und Aktualisierungen enthält. Ich muss SUM-Operationen in dieser Tabelle über verschiedene Zeitbereiche ausführen. Diese Anrufe können bis zu 40 Sekunden dauern und würden gerne Vorschläge hören, wie dies verbessert werden kann.

Ich habe den folgenden Index für diese Tabelle:

CREATE INDEX idx_traffic_partner_only ON traffic (dt_created) WHERE campaign_id IS NULL AND uuid_self <> uuid_partner;

Hier ist ein Beispiel für eine SELECT-Anweisung:

SELECT SUM("clicks") AS clicks, SUM("impressions") AS impressions
FROM "traffic"
WHERE "uuid_self" != "uuid_partner"
AND "campaign_id" is NULL
AND "dt_created" >= 'Sun, 29 Mar 2015 00:00:00 +0000'
AND "dt_created" <= 'Mon, 27 Apr 2015 23:59:59 +0000' 

Und das ist die EXPLAIN ANALYZE:

Aggregate  (cost=21625.91..21625.92 rows=1 width=16) (actual time=41804.754..41804.754 rows=1 loops=1)
  ->  Index Scan using idx_traffic_partner_only on traffic  (cost=0.09..20085.11 rows=308159 width=16) (actual time=1.409..41617.976 rows=302392 loops=1)
      Index Cond: ((dt_created >= '2015-03-29'::date) AND (dt_created <= '2015-04-27'::date))
Total runtime: 41804.893 ms

http://explain.depesz.com/s/gGA

Diese Frage ist einer anderen in SE sehr ähnlich, aber diese verwendete einen Index über zwei Spalten-Zeitstempelbereiche und der Indexplaner für diese Abfrage hatte Schätzungen, die weit entfernt waren. Der Hauptvorschlag bestand darin, einen sortierten mehrspaltigen Index zu erstellen, jedoch für einspaltige Indizes, die keine großen Auswirkungen haben. Die anderen Vorschläge waren die Verwendung von CLUSTER / pg_repack- und GIST-Indizes, aber ich habe sie noch nicht ausprobiert, da ich gerne sehen würde, ob es eine bessere Lösung mit regulären Indizes gibt.

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

Als Referenz habe ich die folgenden Indizes ausprobiert, die von der DB nicht verwendet wurden:

INDEX idx_traffic_2 ON traffic (campaign_id, uuid_self, uuid_partner, dt_created);
INDEX idx_traffic_3 ON traffic (dt_created);
INDEX idx_traffic_4 ON traffic (uuid_self);
INDEX idx_traffic_5 ON traffic (uuid_partner);

EDIT : Ran EXPLAIN (ANALYSE, VERBOSE, KOSTEN, PUFFER) und dies waren Ergebnisse:

Aggregate  (cost=20538.62..20538.62 rows=1 width=8) (actual time=526.778..526.778 rows=1 loops=1)
  Output: sum(clicks), sum(impressions)
  Buffers: shared hit=47783 read=29803 dirtied=4
  I/O Timings: read=184.936
  ->  Index Scan using idx_traffic_partner_only on public.traffic  (cost=0.09..20224.74 rows=313881 width=8) (actual time=0.049..431.501 rows=302405 loops=1)
      Output: id, uuid_self, uuid_partner, impressions, clicks, dt_created... (other fields redacted)
      Index Cond: ((traffic.dt_created >= '2015-03-29'::date) AND (traffic.dt_created <= '2015-04-27'::date))
      Buffers: shared hit=47783 read=29803 dirtied=4
      I/O Timings: read=184.936
Total runtime: 526.881 ms

http://explain.depesz.com/s/7Gu6

Tabellendefinition:

CREATE TABLE traffic (
    id              serial,
    uuid_self       uuid not null,
    uuid_partner    uuid not null,
    impressions     integer NOT NULL DEFAULT 1,
    clicks          integer NOT NULL DEFAULT 0,
    campaign_id     integer,
    dt_created      DATE DEFAULT CURRENT_DATE NOT NULL,
    dt_updated      TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
)

id ist der Primärschlüssel und uuid_self, uuid_partner und Campaign_id sind alle Fremdschlüssel. Das Feld dt_updated wird mit einer Postgres-Funktion aktualisiert.

Evan Appleby
quelle
explain (buffers, analyze, verbose) ...könnte mehr Licht werfen.
Craig Ringer
Hier fehlt eine wesentliche Information: die genaue Tabellendefinition von traffic. Außerdem: Warum zeigt die zweite EXPLAINeinen Abfall von 42 Sekunden auf 0,5 Sekunden? War der erste Lauf mit kaltem Cache?
Erwin Brandstetter
Fügte gerade die Tabellendefinition zur Frage hinzu. Ja, die 42 bis 0,5 Sekunden waren wahrscheinlich auf einen kalten Cache zurückzuführen, aber da es so viele Aktualisierungen gibt, ist dies wahrscheinlich ein ziemlich häufiges Ereignis. Ich habe gerade die EXPLAIN ANALYZE erneut ausgeführt und diesmal dauerte es 56s. Ich habe es noch einmal ausgeführt und es ging auf 0,4 s zurück.
Evan Appleby
Es ist sicher anzunehmen, dass es eine PK-Einschränkung gibt id. Irgendwelche anderen Einschränkungen? Ich sehe zwei Spalten, die NULL sein können. Wie viel Prozent der NULL-Werte sind jeweils enthalten? Was bekommst du dafür? SELECT count(*) AS ct, count(campaign_id)/ count(*) AS camp_pct, count(dt_updated)/count(*) AS upd_pct FROM traffic;
Erwin Brandstetter
Ja, ID hat eine PK-Einschränkung und uuid_self, uuid_partner und Campaign_id haben FK-Einschränkungen. Campaign_id ist 99% + NULL und dt_updated ist 0% NULL.
Evan Appleby

Antworten:

3

Zwei Dinge, die hier sehr seltsam sind:

  1. Die Abfrage wählt 300.000 Zeilen aus einer Tabelle mit mehr als 1 Million Zeilen aus. Für 30% (oder etwas über 5% - abhängig von der Zeilengröße und anderen Faktoren) lohnt es sich normalerweise nicht, überhaupt einen Index zu verwenden. Wir sollten einen sequentiellen Scan sehen .

    Die Ausnahme wären reine Index-Scans, die ich hier nicht sehe. Der vorgeschlagene mehrspaltige Index @Craig ist die beste Option, wenn Sie nur Index-Scans erhalten. Bei vielen Updates, wie Sie bereits erwähnt haben, funktioniert dies möglicherweise nicht. In diesem Fall sind Sie ohne die zusätzlichen Spalten und nur den Index, den Sie bereits haben, besser dran. Sie könnten in der Lage sein , machen es mit für Sie arbeiten aggressivere Einstellungen autovacuum für die Tabelle. Sie können Parameter für einzelne Tabellen anpassen.

  2. Während Postgres den Index verwenden wird, würde ich sicherlich einen Bitmap-Index-Scan für so viele Zeilen erwarten , keinen einfachen Index-Scan, der normalerweise die bessere Wahl für einen geringen Prozentsatz von Zeilen ist. Sobald Postgres mehrere Treffer pro Datenseite erwartet (gemessen an den Statistiken in der Tabelle), wechselt das Unternehmen normalerweise zu einem Bitmap-Index-Scan.

Demnach würde ich vermuten, dass Ihre Kosteneinstellungen unzureichend sind (und möglicherweise auch die Tabellenstatistik). Möglicherweise haben Sie relativ zu gesetzt random_page_costund / oder zu niedrig eingestellt . Folgen Sie den Links und lesen Sie das Handbuch.cpu_index_tuple_cost seq_page_cost

Würde auch zu der Beobachtung passen, dass Cold Cache ein großer Faktor ist, wie wir in Kommentaren herausgearbeitet haben. Entweder greifen Sie auf (Teile von) Tabellen zu, die seit langem niemand mehr berührt hat, oder Sie arbeiten auf einem Testsystem, auf dem der Cache (noch) nicht gefüllt ist?
Andernfalls steht Ihnen einfach nicht genügend RAM zur Verfügung, um die meisten relevanten Daten in Ihrer Datenbank zwischenzuspeichern. Folglich ist der Direktzugriff viel teurer als der sequentielle Zugriff, wenn sich die Daten im Cache befinden. Abhängig von der tatsächlichen Situation müssen Sie möglicherweise Anpassungen vornehmen, um bessere Abfragepläne zu erhalten.

Ein weiterer Faktor muss für eine langsame Reaktion beim ersten Lesevorgang erwähnt werden: Hinweisbits . Lesen Sie Details im Postgres-Wiki und zu dieser verwandten Frage:

Oder die Tabelle ist extrem aufgebläht . In diesem Fall wäre ein Index-Scan sinnvoll und ich würde aufCLUSTER / pg_repackin meiner vorherigen Antwort, die Sie zitiert haben, zurückgreifen . (Oder einfachVACUUM FULL)und untersuchen Sie IhreVACUUMEinstellungen. Diese sind wichtig mitmany inserts and updates every day.

UPDATEBerücksichtigen FILLFACTORSie je nach Muster auch einen Wert unter 100. Wenn Sie meistens nur neu hinzugefügte Zeilen aktualisieren, stellen Sie FILLFACTER nach dem Komprimieren Ihrer Tabelle die niedrigere ein , damit nur neue Seiten Spielraum für Aktualisierungen haben.

Schema

campaign_idist 99% + NULL und dt_updatedist 0% NULL.

Passen Sie die Spaltenfolge leicht an, um 8 Bytes pro Zeile zu sparen (in 99% der Fälle, in denen campaign_idNULL ist):

CREATE TABLE traffic (
    uuid_self       uuid not null REFERENCES ... ,
    uuid_partner    uuid not null REFERENCES ... ,
    id              serial PRIMARY KEY,
    impressions     integer NOT NULL DEFAULT 1,
    clicks          integer NOT NULL DEFAULT 0,
    campaign_id     integer,
    dt_created      DATE DEFAULT CURRENT_DATE NOT NULL,
    dt_updated      TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
);

Detaillierte Erklärung und Links zu mehr:

Messen:

Erwin Brandstetter
quelle
Danke für den Vorschlag. Ich verlasse mich derzeit auf das integrierte automatische Staubsaugen, das durch Heroku eingerichtet wurde, und der Verkehrstisch wird fast jeden Tag gesaugt. Ich werde mich mehr mit dem Ändern der Tabellenstatistik und des Füllfaktors sowie der Verwendung von pg_repack befassen und einen Bericht erstellen.
Evan Appleby
2

Es sieht für mich so aus, als würden Sie viele Daten in einem großen Index abfragen, also ist es langsam. Da ist nichts Besonderes los.

Wenn Sie mit PostgreSQL 9.3 oder 9.4 arbeiten, können Sie versuchen, einen Nur-Index-Scan zu erhalten, indem Sie daraus einen abdeckenden Index machen.

CREATE INDEX idx_traffic_partner_only 
ON traffic (dt_created, clicks, impressions)
WHERE campaign_id IS NULL 
  AND uuid_self <> uuid_partner;

PostgreSQL bietet keine echten Indexe oder keine Unterstützung für Indexbegriffe, die nur Werte sind und nicht Teil des B-Baums sind. Dies ist also langsamer und teurer als bei diesen Funktionen. Es könnte immer noch ein Gewinn für einen einfachen Index-Scan sein, wenn das Vakuum oft genug läuft, um die Sichtbarkeitskarte auf dem neuesten Stand zu halten.


Im Idealfall unterstützt PostgreSQL zusätzliche Datenfelder in einem Index wie in MS-SQL Server ( diese Syntax funktioniert in PostgreSQL NICHT ):

-- This will not work in PostgreSQL (at least 9.5)
-- it's an example of what I wish did work. Don't
-- comment to say it doesn't work.
--
CREATE INDEX idx_traffic_partner_only 
ON traffic (dt_created)
INCLUDING (clicks, impressions) -- auxillary data columns
WHERE campaign_id IS NULL 
  AND uuid_self <> uuid_partner;
Craig Ringer
quelle
Danke für den Vorschlag. Ich habe den Deckungsindex ausprobiert und die DB hat ihn ignoriert und trotzdem den anderen Index verwendet. Würden Sie vorschlagen, den anderen Index zu entfernen und nur den Deckungsindex zu verwenden (oder alternativ nur mehrere Deckungsindizes für jede Situation zu verwenden, die dies erfordert)? Ich habe auch die EXPLAIN (ANALYSE, VERBOSE, COSTS, BUFFERS) in die ursprüngliche Frage eingefügt.
Evan Appleby
Seltsam. Vielleicht ist der Planer nicht klug genug, um einen Nur-Index-Scan auszuwählen, wenn er mehr als ein Aggregat sieht, aber ich hätte gedacht, dass dies möglich ist. Versuchen Sie, mit den Kostenparametern ( random_page_costusw.) zu spielen. Auch für Prüfzwecke nur sehen , ob set enable_indexscan = offund set enable_seqscan = offdann wieder laufen Kräfte ein Index-Only - Scan, und wenn ja, was ihre Kostenschätzungen von explain analysieren sind.
Craig Ringer