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()
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
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é).
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).
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");
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
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: