Postgres está realizando una exploración secuencial en lugar de una exploración de índice

9

Tengo una tabla con aproximadamente 10 millones de filas y un índice en un campo de fecha. Cuando intento extraer los valores únicos del campo indexado, Postgres ejecuta una exploración secuencial a pesar de que el conjunto de resultados tiene solo 26 elementos. ¿Por qué el optimizador elige este plan? ¿Y qué puedo hacer para evitarlo?

De otras respuestas sospecho que esto está tan relacionado con la consulta como con el í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)

Estructura de la mesa:

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
fuente

Respuestas:

8

Este es un problema conocido con respecto a la optimización de Postgres. Si los valores distintos son pocos, como en su caso, y está en la versión 8.4+, aquí se describe una solución muy rápida utilizando una consulta recursiva: Loose Indexscan .

Su consulta podría ser reescrita ( LATERALnecesita la versión 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 tiene una explicación detallada y varias variaciones de la consulta en esta respuesta (en un tema relacionado pero diferente): Optimice la consulta GROUP BY para recuperar el último registro por usuario

ypercubeᵀᴹ
fuente
6

La mejor consulta depende mucho de la distribución de datos .

Tiene muchas filas por fecha, eso se ha establecido. Dado que su caso se reduce a solo 26 valores en el resultado, todas las siguientes soluciones serán increíblemente rápidas tan pronto como se use el índice.
(Para valores más distintos, el caso se volvería más interesante).

No hay necesidad de involucrarse pageid en absoluto (como comentaste).

Índice

Todo lo que necesitas es un simple índice btree en "labelDate".
Con más de unos pocos valores NULL en la columna, un índice parcial ayuda un poco más (y es más pequeño):

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

Más tarde aclaraste:

0% NULL pero solo después de arreglar las cosas al importar.

El índice parcial aún puede tener sentido para descartar estados intermedios de filas con valores NULL. Evitaría actualizaciones innecesarias del índice (con la hinchazón resultante).

Consulta

Basado en un rango provisional

Si sus fechas aparecen en un rango continuo con no demasiados vacíos , podemos utilizar la naturaleza del tipo de datos datepara nuestro beneficio. Solo hay un número finito y contable de valores entre dos valores dados. Si las brechas son pocas, esto será más 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 el molde timestampen generate_series()? Ver:

Min y max pueden seleccionarse del índice a bajo precio. Si conoce la fecha mínima y / o máxima posible, todavía se vuelve un poco más barata. Ejemplo:

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

O, para un intervalo inmutable:

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

Escaneo de índice suelto

Esto funciona muy bien con cualquier distribución de fechas (siempre que tengamos muchas filas por fecha). Básicamente lo que @ypercube ya proporcionó . Pero hay algunos puntos finos y debemos asegurarnos de que nuestro índice favorito se pueda usar en todas partes.

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;
  • El primer CTE pes efectivamente el mismo que

    SELECT min("labelDate") FROM pages

    Pero la forma detallada asegura que se use nuestro índice parcial. Además, este formulario suele ser un poco más rápido en mi experiencia (y en mis pruebas).

  • Para solo una columna, las subconsultas correlacionadas en el término recursivo del rCTE deberían ser un poco más rápidas. Esto requiere excluir filas que den como resultado NULL para "labelDate". Ver:

  • Optimice la consulta GROUP BY para recuperar el último registro por usuario

Aparte

Los identificadores en minúsculas, legales y sin comillas le hacen la vida más fácil.
Ordene columnas en la definición de su tabla favorablemente para ahorrar algo de espacio en disco:

Erwin Brandstetter
fuente
-2

De la documentación postgresql:

CLUSTER puede reordenar la tabla utilizando una exploración de índice en el índice especificado o (si el índice es un árbol b) una exploración secuencial seguida de una ordenación . Intentará elegir el método que será más rápido, según los parámetros de costo del planificador y la información estadística disponible.

Su índice en labelDate es un btree.

Referencia:

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

Fabrizio Mazzoni
fuente
Incluso con una condición como 'WHERE "labelDate" ENTRE' 2000-01-01 'y' 2020-01-01 'todavía implica un escaneo secuencial.
Charlie Clark
Agrupación en este momento (aunque los datos se ingresaron aproximadamente en ese orden). Eso todavía no explica realmente la decisión del planificador de consultas de no usar un índice incluso con una cláusula WHERE.
Charlie Clark
¿Ha intentado también deshabilitar la exploración secuencial para la sesión? set enable_seqscan=offEn cualquier caso la documentación es clara. Si agrupa, realizará una exploración secuencial.
Fabrizio Mazzoni
Sí, intenté deshabilitar el escaneo secuencial pero no hizo mucha diferencia. La velocidad de esta consulta no es realmente crucial, ya que la uso para crear una tabla de búsqueda que luego se puede usar para UNIRSE en consultas reales.
Charlie Clark