web-dev-qa-db-fra.com

Postgres effectue un scan séquentiel au lieu d'un scan d'index

J'ai une table avec environ 10 millions de lignes et un index sur un champ de date. Lorsque j'essaie d'extraire les valeurs uniques du champ indexé, Postgres exécute une analyse séquentielle même si l'ensemble de résultats ne contient que 26 éléments. Pourquoi l'optimiseur choisit-il ce plan? Et que puis-je faire pour l'éviter?

D'après les autres réponses, je soupçonne que cela est autant lié à la requête qu'à l'index.

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)

Structure du tableau:

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

Il s'agit d'un problème connu concernant l'optimisation Postgres. Si les valeurs distinctes sont peu nombreuses - comme dans votre cas - et que vous êtes dans la version 8.4+, une solution de contournement très rapide utilisant une requête récursive est décrite ici: Loose Indexscan .

Votre requête pourrait être réécrite (le LATERAL a besoin de la 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 a une explication approfondie et plusieurs variantes de la requête dans cette réponse (sur un problème connexe mais différent): Optimiser la requête GROUP BY pour récupérer le dernier enregistrement par utilisateur

8
ypercubeᵀᴹ

La meilleure requête dépend beaucoup de la distribution des données.

Vous avez beaucoup lignes par date, cela a été établi. Étant donné que votre cas brûle à seulement 26 valeurs dans le résultat, toutes les solutions suivantes seront extrêmement rapides dès que l'index sera utilisé.
(Pour des valeurs plus distinctes, le cas deviendrait plus intéressant.)

Il n'est pas nécessaire d'impliquer pageid du tout (comme vous l'avez commenté).

Indice

Tout ce dont vous avez besoin est un simple index btree sur "labelDate".
Avec plus de quelques valeurs NULL dans la colonne, un index partiel aide un peu plus (et est plus petit):

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

Vous avez clarifié plus tard:

0% NULL mais seulement après avoir corrigé les choses lors de l'importation.

L'index partiel mai a toujours du sens pour exclure les états intermédiaires des lignes avec des valeurs NULL. Éviterait les mises à jour inutiles de l'index (avec ballonnement résultant).

Requete

Basé sur une gamme provisoire

Si vos dates apparaissent dans une plage continue avec pas trop de lacunes , nous pouvons utiliser la nature du type de données date à notre avantage . Il n'y a qu'un nombre fini et dénombrable de valeurs entre deux valeurs données. Si les écarts sont peu nombreux, ce sera le plus rapide:

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

Pourquoi le transtypage en timestamp dans generate_series()? Voir:

Min et max peuvent être choisis dans l'indice à moindre coût. Si vous savez la date minimum et/ou maximum possible, cela devient un peu moins cher, pour le moment. Exemple:

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, pour un intervalle immuable:

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

Balayage d'index lâche

Cela fonctionne très bien avec n'importe quelle distribution de dates (tant que nous avons plusieurs lignes par date). Fondamentalement, ce @ ypercube déjà fourni . Mais il y a quelques points fins et nous devons nous assurer que notre index préféré peut être utilisé partout.

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;
  • Le premier CTE p est en fait le même que

    SELECT min("labelDate") FROM pages
    

    Mais la forme verbeuse garantit que notre index partiel est utilisé. De plus, ce formulaire est généralement un peu plus rapide dans mon expérience (et dans mes tests).

  • Pour une seule colonne, les sous-requêtes corrélées dans le terme récursif du rCTE devraient être un peu plus rapides. Cela nécessite d'exclure les lignes résultant en NULL pour "labelDate". Voir:

  • Optimiser la requête GROUP BY pour récupérer le dernier enregistrement par utilisateur

À part

Les identificateurs minuscules, légaux et non cotés vous facilitent la vie.
Ordonnez favorablement les colonnes de votre définition de table pour économiser de l'espace disque:

6
Erwin Brandstetter