Postgres sta eseguendo la scansione sequenziale invece della scansione dell'indice


9

Ho una tabella con circa 10 milioni di righe e un indice su un campo data. Quando provo ad estrarre i valori univoci del campo indicizzato Postgres esegue una scansione sequenziale anche se il set di risultati ha solo 26 elementi. Perché l'ottimizzatore sta scegliendo questo piano? E cosa posso fare per evitarlo?

Da altre risposte sospetto che ciò sia tanto legato alla domanda quanto all'indice.

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)

Struttura del tavolo:

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()

Risposte:


8

Questo è un problema noto relativo all'ottimizzazione di Postgres. Se i valori distinti sono pochi - come nel tuo caso - e sei nella versione 8.4+, una soluzione molto veloce usando una query ricorsiva è descritta qui: Loose Indexscan .

La tua query potrebbe essere riscritta ( LATERALnecessita della versione 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 ha una spiegazione approfondita e diverse varianti della query in questa risposta (su un problema correlato ma diverso): Ottimizza la query GROUP BY per recuperare l'ultimo record per utente


6

La query migliore dipende molto dalla distribuzione dei dati .

Hai molte righe per data, è stato stabilito. Dal momento che il tuo caso si riduce a soli 26 valori nel risultato, tutte le seguenti soluzioni saranno incredibilmente veloci non appena verrà utilizzato l'indice.
(Per valori più distinti il ​​caso diventerebbe più interessante.)

Non è necessario coinvolgere pageid affatto (come hai commentato).

Indice

Tutto ciò che serve è un semplice indice btree su "labelDate".
Con più di alcuni valori NULL nella colonna, un indice parziale aiuta un po 'di più (ed è più piccolo):

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

Successivamente hai chiarito:

0% NULL ma solo dopo aver corretto le cose durante l'importazione.

L'indice parziale può comunque avere senso escludere stati intermedi di righe con valori NULL. Eviterebbe inutili aggiornamenti all'indice (con conseguente gonfia).

domanda

Basato su un intervallo provvisorio

Se le tue date appaiono in un intervallo continuo senza troppe lacune , possiamo utilizzare la natura del tipo di dati datea nostro vantaggio. C'è solo un numero finito e numerabile di valori tra due valori dati. Se le lacune sono poche, sarà più veloce:

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");

Perché il cast timestampin generate_series()? Vedere:

Min e max possono essere scelti dall'indice in modo economico. Se conosci la data minima e / o massima possibile, diventa ancora un po 'più economico. Esempio:

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");

Oppure, per un intervallo immutabile:

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");

Scansione indice libera

Questo funziona molto bene con qualsiasi distribuzione di date (purché abbiamo molte righe per data). Fondamentalmente ciò che @ypercube ha già fornito . Ma ci sono alcuni punti positivi e dobbiamo assicurarci che il nostro indice preferito possa essere usato ovunque.

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;
  • Il primo CTE pè effettivamente lo stesso di

    SELECT min("labelDate") FROM pages

    Ma la forma dettagliata assicura che venga utilizzato il nostro indice parziale. Inoltre, questo modulo è in genere un po 'più veloce nella mia esperienza (e nei miei test).

  • Solo per una singola colonna, le sottoquery correlate nel termine ricorsivo di rCTE dovrebbero essere un po 'più veloci. Ciò richiede di escludere le righe risultanti in NULL per "labelDate". Vedere:

  • Ottimizza la query GROUP BY per recuperare l'ultimo record per utente

asides

Gli identificatori non quotati, legali, minuscoli ti semplificano la vita.
Ordinare le colonne nella definizione della tabella in modo favorevole per risparmiare spazio su disco:


-2

Dalla documentazione postgresql:

CLUSTER può riordinare la tabella utilizzando una scansione dell'indice sull'indice specificato o (se l'indice è un albero b) una scansione sequenziale seguita dall'ordinamento . Tenterà di scegliere il metodo che sarà più veloce, in base ai parametri di costo del pianificatore e alle informazioni statistiche disponibili.

Il tuo indice su labelDate è un btree ..

Riferimento:

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


Anche con una condizione come "WHERE" labelDate "TRA '2000-01-01' e '2020-01-01' comporta ancora una scansione sequenziale.
Charlie Clark,

Clustering al momento (anche se i dati sono stati inseriti approssimativamente in quell'ordine). Ciò non spiega ancora la decisione del pianificatore di query di non utilizzare un indice anche con una clausola WHERE.
Charlie Clark,

Hai provato anche a disabilitare la scansione sequenziale per la sessione? set enable_seqscan=offIN ogni caso la documentazione è chiara. Se il cluster eseguirà una scansione sequenziale.
Fabrizio Mazzoni,

Sì, ho provato a disabilitare la scansione sequenziale ma non ha fatto molta differenza. La velocità di questa query non è in realtà cruciale poiché la utilizzo per creare una tabella di ricerca che può quindi essere utilizzata per JOIN in query reali.
Charlie Clark,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.