it-swarm.com.de

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

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()
9
Charlie Clark

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 LATERAL benötigt Version 9.3+):

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): GROUP BY-Abfrage optimieren, um den neuesten Datensatz pro Benutzer abzurufen

8
ypercubeᵀᴹ

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

Sie haben viele Zeilen pro Datum, das wurde festgelegt. 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 ist nicht erforderlich, pageid überhaupt (wie Sie kommentiert haben) einzubeziehen.

Index

Alles was Sie brauchen ist ein einfacher btree Index für "labelDate".
Mit mehr als ein paar NULL-Werten in der Spalte hilft ein Teilindex mehr (und ist 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 may ist immer noch sinnvoll, 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 date zu 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 Umwandlung in timestamp in generate_series()? Sehen:

Min und Max können günstig aus dem Index ausgewählt werden. Wenn Sie wissen das minimal und/oder maximal mögliche Datum, wird es noch ein bisschen billiger. 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 bereits bereitgestellt . 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 p ist effektiv 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 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:

6