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

9

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

Réponses:

8

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 (la LATERALversion 9.3+ des besoins):

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

ypercubeᵀᴹ
la source
6

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

Vous avez plusieurs lignes par date, c'est é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 en aide un peu plus (et est plus petit):

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

Vous avez ensuite précisé:

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

L'index partiel peut toujours avoir un 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 les acteurs à timestampen generate_series()? Voir:

Min et max peuvent être choisis à partir de l'indice à moindre coût. Si vous connaissez la date minimale et / ou maximale 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 que @ypercube a 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 pest effectivement 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 dans votre définition de table pour économiser de l'espace disque:

Erwin Brandstetter
la source
-2

De la documentation postgresql:

CLUSTER peut trier à nouveau la table en utilisant soit une analyse d'index sur l'index spécifié, soit (si l'index est un arbre b) une analyse séquentielle suivie d'un tri . Il tentera de choisir la méthode qui sera la plus rapide, en fonction des paramètres de coût du planificateur et des informations statistiques disponibles.

Votre index sur labelDate est un btree ..

Référence:

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

Fabrizio Mazzoni
la source
Même avec une condition telle que `WHERE" labelDate "BETWEEN '2000-01-01' et '2020-01-01' implique toujours un balayage séquentiel.
Charlie Clark
Clustering pour le moment (bien que les données aient été entrées à peu près dans cet ordre). Cela n'explique toujours pas vraiment la décision du planificateur de requêtes de ne pas utiliser d'index même avec une clause WHERE.
Charlie Clark
Avez-vous également essayé de désactiver l'analyse séquentielle pour la session? set enable_seqscan=offDans tous les cas, la documentation est claire. Si vous mettez en cluster, il effectuera une analyse séquentielle.
Fabrizio Mazzoni
Oui, j'ai essayé de désactiver le scan séquentiel mais cela n'a pas fait beaucoup de différence. La vitesse de cette requête n'est pas vraiment cruciale car je l'utilise pour créer une table de recherche qui peut ensuite être utilisée pour JOINS dans de vraies requêtes.
Charlie Clark