Comment gérer un mauvais plan de requête causé par une égalité exacte sur le type de plage?


28

J'effectue une mise à jour où j'ai besoin d'une égalité exacte sur une tstzrangevariable. ~ 1 M de lignes sont modifiées et la requête prend environ 13 minutes. Le résultat de EXPLAIN ANALYZEpeut être vu ici , et les résultats réels sont extrêmement différents de ceux estimés par le planificateur de requêtes. Le problème est que l'analyse d'index t_rangeattend qu'une seule ligne soit renvoyée.

Cela semble être lié au fait que les statistiques sur les types de plage sont stockées différemment de celles des autres types. En regardant la pg_statsvue de la colonne, n_distinct-1 est et les autres champs (par exemple most_common_vals, most_common_freqs) sont vides.

Cependant, il doit y avoir des statistiques stockées t_rangequelque part. Une mise à jour extrêmement similaire où j'utilise un «dedans» sur t_range au lieu d'une égalité exacte prend environ 4 minutes à effectuer et utilise un plan de requête substantiellement différent (voir ici ). Le deuxième plan de requête est logique pour moi car chaque ligne de la table temporaire et une fraction substantielle de la table d'historique seront utilisées. Plus important encore, le planificateur de requêtes prédit un nombre approximativement correct de lignes pour le filtre activé t_range.

La distribution de t_rangeest un peu inhabituelle. J'utilise cette table pour stocker l'état historique d'une autre table, et les modifications apportées à l'autre table se produisent simultanément dans de grands vidages, il n'y a donc pas beaucoup de valeurs distinctes de t_range. Voici les chiffres correspondant à chacune des valeurs uniques de t_range:

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

Les décomptes pour les éléments t_rangeci-dessus sont terminés, la cardinalité est donc de ~ 3M (dont ~ 1M seront affectés par l'une ou l'autre des requêtes de mise à jour).

Pourquoi la requête 1 fonctionne-t-elle beaucoup moins bien que la requête 2? Dans mon cas, la requête 2 est un bon substitut, mais si une égalité de plage exacte était vraiment requise, comment pourrais-je amener Postgres à utiliser un plan de requête plus intelligent?

Définition de table avec index (suppression de colonnes non pertinentes):

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

Requête 1:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

Requête 2:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Q1 met à jour 999753 lignes et Q2 met à jour 999753 + 36791 = 1036544 (c'est-à-dire que la table temporaire est telle que chaque ligne correspondant à la condition de plage de temps est mise à jour).

J'ai essayé cette requête en réponse au commentaire de @ ypercube :

Requête 3:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

Le plan de requête et les résultats (voir ici ) étaient intermédiaires entre les deux cas précédents (~ 6 minutes).

2016/02/05 EDIT

N'ayant plus accès aux données au bout d'un an et demi, j'ai créé une table de test avec la même structure (sans index) et une cardinalité similaire. La réponse de jjanes a proposé que la cause pourrait être l'ordre de la table temporaire utilisée pour la mise à jour. Je n'ai pas pu tester l'hypothèse directement car je n'y ai pas accès track_io_timing(en utilisant Amazon RDS).

  1. Les résultats globaux ont été beaucoup plus rapides (d'un facteur de plusieurs). Je suppose que cela est dû à la suppression des indices, conformément à la réponse d' Erwin .

  2. Dans ce cas de test, les requêtes 1 et 2 prenaient essentiellement le même temps, car elles utilisaient toutes deux la jointure de fusion. C'est-à-dire que je n'ai pas pu déclencher ce qui a poussé Postgres à choisir la jointure de hachage, donc je ne sais pas pourquoi Postgres a choisi la jointure de hachage peu performante en premier lieu.


1
Que faire si vous avez converti la condition d'égalité (a = b)à deux « contient » conditions: (a @> b AND b @> a)? Le plan change-t-il?
ypercubeᵀᴹ

@ypercube: le plan change substantiellement, bien qu'il ne soit pas encore tout à fait optimal - voir ma modification # 2.
abeboparebop

1
Une autre idée serait d'ajouter un index btree régulier (lower(t_range),upper(t_range))depuis que vous vérifiez l'égalité.
ypercubeᵀᴹ

Réponses:


9

La plus grande différence de temps dans vos plans d'exécution est sur le nœud supérieur, la MISE À JOUR elle-même. Cela suggère que la plupart de votre temps est consacré aux E / S pendant la mise à jour. Vous pouvez le vérifier en activant track_io_timinget en exécutant les requêtes avecEXPLAIN (ANALYZE, BUFFERS)

Les différents plans présentent des lignes à mettre à jour dans différents ordres. L'un est en trip_idordre et l'autre est dans l'ordre dans lequel ils se trouvent physiquement présents dans la table temporaire.

La table en cours de mise à jour semble avoir son ordre physique corrélé avec la colonne trip_id, et la mise à jour des lignes dans cet ordre conduit à des modèles d'E / S efficaces avec des lectures à lecture anticipée / séquentielle. Alors que l'ordre physique de la table temporaire semble conduire à de nombreuses lectures aléatoires.

Si vous pouvez ajouter un order by trip_idà l'instruction qui a créé la table temporaire, cela pourrait résoudre le problème pour vous.

PostgreSQL ne prend pas en compte les effets de la commande IO lors de la planification de l'opération UPDATE. (Contrairement aux opérations SELECT, où il les prend en compte). Si PostgreSQL était plus intelligent, il se rendrait compte qu'un plan produit un ordre plus efficace, ou il interjecterait un nœud de tri explicite entre la mise à jour et son nœud enfant afin que la mise à jour soit alimentée en lignes dans l'ordre ctid.

Vous avez raison de dire que PostgreSQL fait un mauvais travail d'estimation de la sélectivité des jointures d'égalité sur les plages. Cependant, cela n'est que tangentiellement lié à votre problème fondamental. Une requête plus efficace sur la partie sélectionnée de votre mise à jour peut accidentellement arriver à alimenter les lignes dans la mise à jour proprement dite dans un meilleur ordre, mais si c'est le cas, c'est surtout à la chance.


Malheureusement, je ne suis pas en mesure de modifier track_io_timing, et (depuis un an et demi!) Je n'ai plus accès aux données d'origine. Cependant, j'ai testé votre théorie en créant des tables avec le même schéma et une taille similaire (des millions de lignes), et en exécutant deux mises à jour différentes - une dans laquelle la table de mise à jour temporaire a été triée comme la table d'origine, et une autre dans laquelle elle a été triée quasi-aléatoirement. Malheureusement, les deux mises à jour prennent à peu près le même temps, ce qui implique que l'ordre de la table de mise à jour n'affecte pas cette requête.
abeboparebop

7

Je ne sais pas exactement pourquoi la sélectivité d'un prédicat d'égalité est si radicalement surestimée par l'indice GiST sur la tstzrangecolonne. Bien que cela reste intéressant en soi, cela ne semble pas pertinent pour votre cas particulier.

Puisque votre UPDATEmodifie un tiers (!) De toutes les lignes 3M existantes, un index ne va pas aider du tout . Au contraire, la mise à jour incrémentielle de l'index en plus du tableau va ajouter un coût substantiel à votre UPDATE.

Gardez simplement votre requête simple 1 . La solution simple et radicale consiste à baisser l'indice avant le UPDATE. Si vous en avez besoin à d'autres fins, recréez-le après le UPDATE. Ce serait encore plus rapide que de maintenir l'indice pendant le grand UPDATE.

Pour un UPDATEsur un tiers de toutes les lignes, il sera probablement avantageux de supprimer également tous les autres index - et de les recréer après le UPDATE. Le seul inconvénient: vous avez besoin de privilèges supplémentaires et d'un verrou exclusif sur la table (uniquement pour un bref instant si vous utilisez CREATE INDEX CONCURRENTLY).

L'idée de @ ypercube d'utiliser un btree au lieu de l'index GiST semble bonne en principe. Mais pas pour un tiers de toutes les lignes (où aucun index n'est bon pour commencer), et pas seulement (lower(t_range),upper(t_range)), car il tstzrangene s'agit pas d'un type de plage discrète.

La plupart des types de plages discrètes ont une forme canonique, ce qui rend le concept «d'égalité» plus simple: les bornes inférieure et supérieure de la valeur sous forme canonique la définissent. La documentation:

Un type de plage discrète doit avoir une fonction de canonisation qui connaît la taille d'étape souhaitée pour le type d'élément. La fonction de canonisation est chargée de convertir des valeurs équivalentes du type plage pour avoir des représentations identiques, en particulier des bornes systématiquement inclusives ou exclusives. Si une fonction de canonisation n'est pas spécifiée, les plages de formatage différent seront toujours traitées comme inégales, même si elles peuvent représenter le même ensemble de valeurs dans la réalité.

Le haut-types de plage int4range, int8rangeet daterangetoute utilisation d' une forme canonique qui comprend la borne inférieure et la borne supérieure ne comprend pas; c'est-à-dire [),. Cependant, les types de plage définis par l'utilisateur peuvent utiliser d'autres conventions.

Ce n'est pas le cas tstzrange, où l'inclusivité des bornes supérieure et inférieure doit être prise en compte pour l'égalité. Un éventuel indice btree devrait être sur:

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

Et les requêtes devraient utiliser les mêmes expressions dans la WHEREclause.

On pourrait être tenté d'indexer simplement la valeur entière convertie en text: (cast(t_range AS text))- mais cette expression ne l'est pas IMMUTABLEcar la représentation textuelle des timestamptzvaleurs dépend du timezoneparamètre actuel . Vous auriez besoin de mettre des étapes supplémentaires dans une IMMUTABLEfonction wrapper qui produit une forme canonique, et de créer un index fonctionnel sur cela ...

Mesures supplémentaires / idées alternatives

Si shape_dist_traveledpeut déjà avoir la même valeur que tt.shape_dist_traveledpour plusieurs de vos lignes mises à jour (et que vous ne comptez pas sur les effets secondaires de vos UPDATEdéclencheurs similaires ...), vous pouvez rendre votre requête plus rapide en excluant les mises à jour vides:

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

Bien sûr, tous les conseils généraux pour l'optimisation des performances s'appliquent. Le wiki Postgres est un bon point de départ.

VACUUM FULLserait un poison pour vous, car certains tuples morts (ou l'espace réservé par FILLFACTOR) sont bénéfiques pour les UPDATEperformances.

Avec autant de lignes mises à jour, et si vous pouvez vous le permettre (pas d'accès simultané ou d'autres dépendances), il pourrait être encore plus rapide d'écrire une nouvelle table au lieu de la mettre à jour sur place. Instructions dans cette réponse connexe:

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.