Index sur la clé primaire non utilisé dans la jointure simple

16

J'ai les définitions de table et d'index suivantes:

CREATE TABLE munkalap (
    munkalap_id serial PRIMARY KEY,
    ...
);

CREATE TABLE munkalap_lepes (
    munkalap_lepes_id serial PRIMARY KEY,
    munkalap_id integer REFERENCES munkalap (munkalap_id),
    ...
);

CREATE INDEX idx_munkalap_lepes_munkalap_id ON munkalap_lepes (munkalap_id);

Pourquoi aucun des index de munkalap_id n'est-il utilisé dans la requête suivante?

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id);

QUERY PLAN
Hash Join  (cost=119.17..2050.88 rows=38046 width=214) (actual time=0.824..18.011 rows=38046 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.005..4.574 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=3252 width=4) (actual time=0.810..0.810 rows=3253 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 115kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=3252 width=4) (actual time=0.003..0.398 rows=3253 loops=1)
Total runtime: 19.786 ms

C'est la même chose même si j'ajoute un filtre:

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id) WHERE NOT lezarva;

QUERY PLAN
Hash Join  (cost=79.60..1545.79 rows=1006 width=214) (actual time=0.616..10.824 rows=964 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.007..5.061 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=86 width=4) (actual time=0.587..0.587 rows=87 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 4kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=86 width=4) (actual time=0.014..0.560 rows=87 loops=1)
              Filter: (NOT lezarva)
Total runtime: 10.911 ms
dezso
la source

Réponses:

22

Beaucoup de gens ont entendu dire que "les analyses séquentielles sont mauvaises" et cherchent à les éliminer de leurs plans, mais ce n'est pas si simple. Si une requête doit couvrir chaque ligne d'un tableau, une analyse séquentielle est le moyen le plus rapide d'obtenir ces lignes. C'est pourquoi votre requête de jointure d'origine a utilisé l'analyse séquentielle, car toutes les lignes des deux tables étaient requises.

Lors de la planification d'une requête, le planificateur de Postgres estime les coûts de diverses opérations (calcul, séquentiel et E / S aléatoires) selon différents schémas possibles, et choisit le plan qu'il estime comme ayant le coût le plus bas. Lorsque vous effectuez des E / S à partir d'un stockage rotatif (disques), les E / S aléatoires sont généralement beaucoup plus lentes que les E / S séquentielles, la configuration pg par défaut pour random_page_cost et seq_page_cost estime une différence de coût de 4: 1.

Ces considérations entrent en jeu lorsque l'on considère une méthode de jointure ou de filtrage qui utilise un index par rapport à un qui analyse séquentiellement une table. Lorsque vous utilisez un index, le plan peut trouver une ligne rapidement via l'index, puis avoir à prendre en compte une lecture aléatoire de bloc pour résoudre les données de ligne. Dans le cas de votre deuxième requête qui a ajouté un prédicat de filtrage WHERE NOT lezarva, vous pouvez voir comment cela a affecté les estimations de planification dans les résultats EXPLAIN ANALYZE. Le planificateur estime 1006 lignes résultant de la jointure (ce qui correspond assez étroitement à l'ensemble de résultats réel de 964). Étant donné que la plus grande table munkalap_lepes contient environ 38K lignes, le planificateur voit que la jointure devra accéder à environ 1006/38046 ou 1/38 des lignes de la table. Il sait également que la largeur de ligne moyenne est de 214 octets et qu'un bloc est de 8 Ko, il y a donc environ 38 lignes / bloc.

Avec ces statistiques, le planificateur considère qu'il est probable que la jointure devra lire tout ou la plupart des blocs de données de la table. Étant donné que les recherches d'index ne sont pas gratuites non plus et que le calcul pour analyser un bloc évaluant une condition de filtre est très bon marché par rapport aux entrées-sorties, le planificateur a choisi d'analyser séquentiellement la table et d'éviter les frais généraux d'index et les lectures aléatoires lors du calcul de l'analyse séquentielle. sera plus rapide.

Dans le monde réel, les données sont souvent disponibles en mémoire via le cache de page du système d'exploitation, et donc chaque lecture de bloc ne nécessite pas d'E / S. Il peut être assez difficile de prédire l'efficacité d'un cache pour une requête donnée, mais le planificateur Pg utilise quelques heuristiques simples. La valeur de configuration effective_cache_size informe les planificateurs des estimations de la probabilité d'engager des coûts d'E / S réels. Une valeur plus élevée lui fera estimer un coût inférieur pour les entrées-sorties aléatoires et peut donc le biaiser vers une méthode pilotée par index sur un balayage séquentiel.

dbenhur
la source
Merci, c'est jusqu'à présent la meilleure description (et la plus concise) que j'ai lue. Clarifié quelques points clés.
dezso
1
Excellente explication. Le calcul des lignes / page de données est cependant un peu décalé. Vous devez prendre en compte l'en-tête de page (24 octets) + 4 octets pour chaque pointeur d'élément par ligne + l'en-tête de ligne HeapTupleHeader(23 octets par ligne) + masque binaire NULL + alignement selon MAXALIGN. Enfin, une quantité inconnue de remplissage en raison de l'alignement des données en fonction des types de données des colonnes et de leur séquence. Dans l'ensemble, il n'y a pas plus de 33 lignes sur une page de 8 ko dans ce cas. (Sans prendre en compte TOAST.)
Erwin Brandstetter
1
@ErwinBrandstetter Merci d'avoir rempli des calculs de taille de ligne plus précis. J'avais toujours supposé que la sortie d'estimation de la largeur de ligne par expliquer inclurait des considérations par ligne comme l'en-tête et le masque de bits NULL, mais pas la surcharge au niveau de la page.
dbenhur
1
@dbenhur: Vous pouvez exécuter un rapide EXPLAIN ANALYZE SELECT foo from baravec une table factice de base pour vérifier. En outre, l'espace réel sur le disque dépend de l'alignement des données, ce qui serait difficile à prendre en compte lorsque seules certaines lignes sont récupérées. La largeur de ligne en EXPLAINreprésente l'espace de base requis pour l'ensemble de colonnes récupéré.
Erwin Brandstetter
5

Vous récupérez toutes les lignes des deux tables, il n'y a donc aucun avantage réel à utiliser une analyse d'index. Une analyse d'index n'a de sens que si vous ne sélectionnez que quelques lignes dans une table (généralement moins de 10% -15%)

un cheval sans nom
la source
Oui, vous avez raison :) J'ai essayé de clarifier la situation avec un cas plus spécifique, voir la dernière requête.
dezso
@dezso: Même chose. Si un index (lezarva, munkalap_id)est activé et qu'il est suffisamment sélectif, il peut être utilisé. Le NOTfait que moins probable.
ypercubeᵀᴹ
J'ai ajouté un index partiel basé sur votre suggestion et il est utilisé, donc la moitié du problème est résolu. Mais je ne m'attendrais pas à ce que l'index sur la clé étrangère soit inutile étant donné que je veux REJOINDRE contre seulement 87 valeurs par rapport à l'original 3252.
dezso
1
@dezso Les lignes ont une largeur moyenne de 214 octets, vous aurez donc un peu moins de 40 lignes par bloc de données de 8 Ko. La sélectivité de l'indice est également d'environ 1/40 (1006/38046). Ainsi, Pg estime que la lecture séquentielle de tous les blocs est moins chère que la lecture probable d'environ le même nombre de blocs au hasard lors de l'utilisation de l'indice. Ces compromis estimés peuvent être influencés par les valeurs de configuration effective_cache_size et random_page_cost.
dbenhur
@dbenhur: pourriez-vous faire de votre commentaire une bonne réponse?
dezso