O Postgres está executando uma varredura sequencial em vez da varredura de índice

9

Eu tenho uma tabela com cerca de 10 milhões de linhas e um índice em um campo de data. Quando tento extrair os valores exclusivos do campo indexado, o Postgres executa uma varredura seqüencial, mesmo que o conjunto de resultados possua apenas 26 itens. Por que o otimizador está escolhendo este plano? E o que posso fazer para evitá-lo?

De outras respostas, suspeito que isso esteja relacionado tanto à consulta quanto ao índice.

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)

Estrutura da tabela:

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
fonte

Respostas:

8

Esse é um problema conhecido sobre a otimização do Postgres. Se os valores distintos são poucos - como no seu caso - e você está na versão 8.4+, uma solução muito rápida usando uma consulta recursiva é descrita aqui: Loose Indexscan .

Sua consulta pode ser reescrita (a LATERALversão 9.3 ou superior):

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 tem uma explicação completa e várias variações da consulta nesta resposta (em um problema relacionado, mas diferente): Otimize a consulta GROUP BY para recuperar o registro mais recente por usuário

ypercubeᵀᴹ
fonte
6

A melhor consulta depende muito da distribuição de dados .

Você tem muitas linhas por data, isso foi estabelecido. Como o seu gabinete reduz para apenas 26 valores no resultado, todas as soluções a seguir serão incrivelmente rápidas assim que o índice for usado.
(Para valores mais distintos, o caso seria mais interessante.)

Não há necessidade de envolver pageid em tudo (como você comentou).

Índice

Tudo o que você precisa é de um simples índice btree "labelDate".
Com mais de alguns valores NULL na coluna, um índice parcial ajuda um pouco mais (e é menor):

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

Você esclareceu mais tarde:

0% NULL, mas somente após a correção das coisas durante a importação.

O índice parcial ainda pode fazer sentido excluir estados intermediários de linhas com valores NULL. Evitaria atualizações desnecessárias no índice (com inchaço resultante).

Inquerir

Com base em um intervalo provisório

Se suas datas aparecerem em um intervalo contínuo, sem muitas lacunas , podemos usar a natureza do tipo de dados datepara nossa vantagem. Há apenas um número finito e contável de valores entre dois valores fornecidos. Se as lacunas forem poucas, isso será mais rápido:

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

Por que o elenco timestampno generate_series()? Vejo:

Mín e máximo podem ser selecionados no índice mais barato. Se você conhece a data mínima e / ou máxima possível, fica um pouco mais barata ainda. Exemplo:

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

Ou, por um intervalo imutável:

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

Varredura de índice frouxa

Isso funciona muito bem com qualquer distribuição de datas (desde que tenhamos muitas linhas por data). Basicamente, o que o @ypercube já forneceu . Mas existem alguns pontos positivos e precisamos garantir que nosso índice favorito possa ser usado em qualquer lugar.

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;
  • O primeiro CTE pé efetivamente o mesmo que

    SELECT min("labelDate") FROM pages

    Mas a forma detalhada garante que nosso índice parcial seja usado. Além disso, esse formulário geralmente é um pouco mais rápido na minha experiência (e nos meus testes).

  • Para apenas uma coluna, as subconsultas correlacionadas no termo recursivo do rCTE devem ser um pouco mais rápidas. Isso requer a exclusão de linhas resultando em NULL para "labelDate". Vejo:

  • Otimize a consulta GROUP BY para recuperar o registro mais recente por usuário

Apartes

Os identificadores legais, minúsculos e sem aspas facilitam sua vida.
Encomende colunas na sua definição de tabela de maneira favorável para economizar espaço em disco:

Erwin Brandstetter
fonte
-2

Na documentação do postgresql:

O CLUSTER pode reorganizar a tabela usando uma varredura de índice no índice especificado ou (se o índice for uma árvore b) uma varredura seqüencial seguida pela classificação . Ele tentará escolher o método que será mais rápido, com base nos parâmetros de custo do planejador e nas informações estatísticas disponíveis.

Seu índice no labelDate é um btree ..

Referência:

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

Fabrizio Mazzoni
fonte
Mesmo com uma condição como `WHERE" labelDate "ENTRE '2000-01-01' e '2020-01-01' ainda envolve uma varredura seqüencial.
Charlie Clark
Clustering no momento (embora os dados tenham sido inseridos aproximadamente nessa ordem). Isso ainda não explica realmente a decisão do planejador de consulta de não usar um índice, mesmo com uma cláusula WHERE.
Charlie Clark
Você também tentou desativar a verificação seqüencial para a sessão? set enable_seqscan=offEm qualquer caso, a documentação é clara. Se você agrupar, ele executará uma verificação seqüencial.
Fabrizio Mazzoni
Sim, tentei desativar a verificação seqüencial, mas não fez muita diferença. A velocidade dessa consulta não é realmente crucial, pois eu a uso para criar uma tabela de pesquisa que pode ser usada para JOINS em consultas reais.
Charlie Clark