Postgres führt einen sequentiellen Scan anstelle eines Index-Scans durch

9

Ich habe eine Tabelle mit ungefähr 10 Millionen Zeilen und einen Index für ein Datumsfeld. Wenn ich versuche, die eindeutigen Werte des indizierten Feldes zu extrahieren, führt Postgres einen sequentiellen Scan durch, obwohl die Ergebnismenge nur 26 Elemente enthält. Warum wählt der Optimierer diesen Plan? Und was kann ich tun, um das zu vermeiden?

Aufgrund anderer Antworten vermute ich, dass dies sowohl mit der Abfrage als auch mit dem Index zusammenhängt.

explain select "labelDate" from pages group by "labelDate";
                              QUERY PLAN
-----------------------------------------------------------------------
 HashAggregate  (cost=524616.78..524617.04 rows=26 width=4)
   Group Key: "labelDate"
   ->  Seq Scan on pages  (cost=0.00..499082.42 rows=10213742 width=4)
(3 rows)

Tabellenstruktur:

http=# \d pages
                                       Table "public.pages"
     Column      |          Type          |        Modifiers
-----------------+------------------------+----------------------------------
 pageid          | integer                | not null default nextval('...
 createDate      | integer                | not null
 archive         | character varying(16)  | not null
 label           | character varying(32)  | not null
 wptid           | character varying(64)  | not null
 wptrun          | integer                | not null
 url             | text                   |
 urlShort        | character varying(255) |
 startedDateTime | integer                |
 renderStart     | integer                |
 onContentLoaded | integer                |
 onLoad          | integer                |
 PageSpeed       | integer                |
 rank            | integer                |
 reqTotal        | integer                | not null
 reqHTML         | integer                | not null
 reqJS           | integer                | not null
 reqCSS          | integer                | not null
 reqImg          | integer                | not null
 reqFlash        | integer                | not null
 reqJSON         | integer                | not null
 reqOther        | integer                | not null
 bytesTotal      | integer                | not null
 bytesHTML       | integer                | not null
 bytesJS         | integer                | not null
 bytesCSS        | integer                | not null
 bytesHTML       | integer                | not null
 bytesJS         | integer                | not null
 bytesCSS        | integer                | not null
 bytesImg        | integer                | not null
 bytesFlash      | integer                | not null
 bytesJSON       | integer                | not null
 bytesOther      | integer                | not null
 numDomains      | integer                | not null
 labelDate       | date                   |
 TTFB            | integer                |
 reqGIF          | smallint               | not null
 reqJPG          | smallint               | not null
 reqPNG          | smallint               | not null
 reqFont         | smallint               | not null
 bytesGIF        | integer                | not null
 bytesJPG        | integer                | not null
 bytesPNG        | integer                | not null
 bytesFont       | integer                | not null
 maxageMore      | smallint               | not null
 maxage365       | smallint               | not null
 maxage30        | smallint               | not null
 maxage1         | smallint               | not null
 maxage0         | smallint               | not null
 maxageNull      | smallint               | not null
 numDomElements  | integer                | not null
 numCompressed   | smallint               | not null
 numHTTPS        | smallint               | not null
 numGlibs        | smallint               | not null
 numErrors       | smallint               | not null
 numRedirects    | smallint               | not null
 maxDomainReqs   | smallint               | not null
 bytesHTMLDoc    | integer                | not null
 maxage365       | smallint               | not null
 maxage30        | smallint               | not null
 maxage1         | smallint               | not null
 maxage0         | smallint               | not null
 maxageNull      | smallint               | not null
 numDomElements  | integer                | not null
 numCompressed   | smallint               | not null
 numHTTPS        | smallint               | not null
 numGlibs        | smallint               | not null
 numErrors       | smallint               | not null
 numRedirects    | smallint               | not null
 maxDomainReqs   | smallint               | not null
 bytesHTMLDoc    | integer                | not null
 fullyLoaded     | integer                |
 cdn             | character varying(64)  |
 SpeedIndex      | integer                |
 visualComplete  | integer                |
 gzipTotal       | integer                | not null
 gzipSavings     | integer                | not null
 siteid          | numeric                |
Indexes:
    "pages_pkey" PRIMARY KEY, btree (pageid)
    "pages_date_url" UNIQUE CONSTRAINT, btree ("urlShort", "labelDate")
    "idx_pages_cdn" btree (cdn)
    "idx_pages_labeldate" btree ("labelDate") CLUSTER
    "idx_pages_urlshort" btree ("urlShort")
Triggers:
    pages_label_date BEFORE INSERT OR UPDATE ON pages
      FOR EACH ROW EXECUTE PROCEDURE fix_label_date()
Charlie Clark
quelle

Antworten:

8

Dies ist ein bekanntes Problem bei der Postgres-Optimierung. Wenn es nur wenige eindeutige Werte gibt - wie in Ihrem Fall - und Sie sich in der Version 8.4+ befinden, wird hier eine sehr schnelle Problemumgehung mithilfe einer rekursiven Abfrage beschrieben: Loose Indexscan .

Ihre Anfrage könnte umgeschrieben werden (die LATERALVersion 9.3+ benötigt):

WITH RECURSIVE pa AS 
( ( SELECT labelDate FROM pages ORDER BY labelDate LIMIT 1 ) 
  UNION ALL
    SELECT n.labelDate 
    FROM pa AS p
         , LATERAL 
              ( SELECT labelDate 
                FROM pages 
                WHERE labelDate > p.labelDate 
                ORDER BY labelDate 
                LIMIT 1
              ) AS n
) 
SELECT labelDate 
FROM pa ;

Erwin Brandstetter hat eine ausführliche Erklärung und verschiedene Variationen der Abfrage in dieser Antwort (zu einem verwandten, aber anderen Problem): Optimieren Sie die GROUP BY-Abfrage, um den neuesten Datensatz pro Benutzer abzurufen

ypercubeᵀᴹ
quelle
6

Die beste Abfrage hängt stark von der Datenverteilung ab .

Sie haben viele Zeilen pro Datum, das wurde eingerichtet. Da Ihr Fall im Ergebnis nur auf 26 Werte herunterbrennt, sind alle folgenden Lösungen blitzschnell, sobald der Index verwendet wird.
(Für eindeutigere Werte würde der Fall interessanter werden.)

Es gibt keine Notwendigkeit, die pageid überhaupt (wie Sie kommentiert).

Index

Alles was Sie brauchen ist ein einfacher btree Index auf "labelDate".
Mit mehr als einigen NULL - Werte in der Spalte, ein Teilindex hilft etwas mehr (und kleiner):

CREATE INDEX pages_labeldate_nonull_idx ON big ("labelDate")
WHERE  "labelDate" IS NOT NULL;

Sie haben später klargestellt:

0% NULL, aber erst nach dem Korrigieren beim Importieren.

Der Teilindex kann dennoch sinnvoll sein, um Zwischenzustände von Zeilen mit NULL-Werten auszuschließen. Vermeiden Sie unnötige Aktualisierungen des Index (mit daraus resultierendem Aufblähen).

Abfrage

Basierend auf einem vorläufigen Bereich

Wenn Ihre Daten in einem kontinuierlichen Bereich mit nicht zu vielen Lücken angezeigt werden , können wir die Art des Datentyps datezu unserem Vorteil nutzen. Es gibt nur eine endliche, zählbare Anzahl von Werten zwischen zwei gegebenen Werten. Wenn die Lücken gering sind, ist dies am schnellsten:

SELECT d."labelDate"
FROM  (
   SELECT generate_series(min("labelDate")::timestamp
                        , max("labelDate")::timestamp
                        , interval '1 day')::date AS "labelDate"
   FROM   pages
   ) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");

Warum die Besetzung zu timestampin generate_series()? Sehen:

Min und Max können günstig aus dem Index ausgewählt werden. Wenn Sie wissen , die Mindest- und / oder maximal mögliche Datum, wird es ein bisschen billiger, noch nicht. Beispiel:

SELECT d."labelDate"
FROM  (SELECT date '2011-01-01' + g AS "labelDate"
       FROM   generate_series(0, now()::date - date '2011-01-01' - 1) g) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");

Oder für ein unveränderliches Intervall:

SELECT d."labelDate"
FROM  (SELECT date '2011-01-01' + g AS "labelDate"
       FROM generate_series(0, 363) g) d
WHERE  EXISTS (SELECT FROM pages WHERE "labelDate" = d."labelDate");

Loser Index-Scan

Dies funktioniert sehr gut bei jeder Datumsverteilung (solange wir viele Zeilen pro Datum haben). Grundsätzlich was @ypercube schon zur Verfügung gestellt hat . Aber es gibt einige feine Punkte und wir müssen sicherstellen, dass unser Lieblingsindex überall verwendet werden kann.

WITH RECURSIVE p AS (
   ( -- parentheses required for LIMIT
   SELECT "labelDate"
   FROM   pages
   WHERE  "labelDate" IS NOT NULL
   ORDER  BY "labelDate"
   LIMIT  1
   ) 
   UNION ALL
   SELECT (SELECT "labelDate" 
           FROM   pages 
           WHERE  "labelDate" > p."labelDate" 
           ORDER  BY "labelDate" 
           LIMIT  1)
   FROM   p
   WHERE  "labelDate" IS NOT NULL
   ) 
SELECT "labelDate" 
FROM   p
WHERE  "labelDate" IS NOT NULL;
  • Der erste CTE pist praktisch der gleiche wie

    SELECT min("labelDate") FROM pages

    Die ausführliche Form stellt jedoch sicher, dass unser Teilindex verwendet wird. Außerdem ist dieses Formular meiner Erfahrung nach (und in meinen Tests) normalerweise etwas schneller.

  • Für nur eine einzelne Spalte sollten korrelierte Unterabfragen im rekursiven Term des rCTE etwas schneller sein. Dies erfordert das Ausschließen von Zeilen, die für "labelDate" zu NULL führen. Sehen:

  • Optimieren Sie die GROUP BY-Abfrage, um den neuesten Datensatz pro Benutzer abzurufen

Nebenbei

Nicht zitierte, legale Kleinbuchstaben erleichtern Ihnen das Leben.
Ordnen Sie Spalten in Ihrer Tabellendefinition günstig an, um Speicherplatz zu sparen:

Erwin Brandstetter
quelle
-2

Aus der postgresql-Dokumentation:

CLUSTER kann die Tabelle entweder mithilfe eines Indexscans für den angegebenen Index oder (wenn der Index ein B-Baum ist) eines sequentiellen Scans gefolgt von einer Sortierung neu sortieren . Es wird versucht, die Methode auszuwählen, die schneller ist, basierend auf den Kostenparametern des Planers und den verfügbaren statistischen Informationen.

Ihr Index auf labelDate ist ein btree ..

Referenz:

http://www.postgresql.org/docs/9.1/static/sql-cluster.html

Fabrizio Mazzoni
quelle
Selbst bei einer Bedingung wie "WHERE" labelDate "ZWISCHEN" 2000-01-01 "und" 2020-01-01 "wird immer noch ein sequentieller Scan durchgeführt.
Charlie Clark
Clustering im Moment (obwohl die Daten ungefähr in dieser Reihenfolge eingegeben wurden). Das erklärt immer noch nicht wirklich die Entscheidung des Abfrageplaners, selbst mit einer WHERE-Klausel keinen Index zu verwenden.
Charlie Clark
Haben Sie auch versucht, den sequentiellen Scan für die Sitzung zu deaktivieren? set enable_seqscan=offIn jedem Fall ist die Dokumentation klar. Wenn Sie ein Cluster erstellen, wird ein sequentieller Scan durchgeführt.
Fabrizio Mazzoni
Ja, ich habe versucht, den sequentiellen Scan zu deaktivieren, aber es machte keinen großen Unterschied. Die Geschwindigkeit dieser Abfrage ist eigentlich nicht entscheidend, da ich damit eine Nachschlagetabelle erstelle, die dann für JOINS in realen Abfragen verwendet werden kann.
Charlie Clark