J'effectue une mise à jour où j'ai besoin d'une égalité exacte sur une tstzrange
variable. ~ 1 M de lignes sont modifiées et la requête prend environ 13 minutes. Le résultat de EXPLAIN ANALYZE
peut ê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_range
attend 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_stats
vue 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_range
quelque 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_range
est 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_range
ci-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).
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 .
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.
la source
(a = b)
à deux « contient » conditions:(a @> b AND b @> a)
? Le plan change-t-il?(lower(t_range),upper(t_range))
depuis que vous vérifiez l'égalité.Réponses:
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_timing
et 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_id
ordre 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.
la source
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.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
tstzrange
colonne. Bien que cela reste intéressant en soi, cela ne semble pas pertinent pour votre cas particulier.Puisque votre
UPDATE
modifie 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 à votreUPDATE
.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 leUPDATE
. Ce serait encore plus rapide que de maintenir l'indice pendant le grandUPDATE
.Pour un
UPDATE
sur un tiers de toutes les lignes, il sera probablement avantageux de supprimer également tous les autres index - et de les recréer après leUPDATE
. 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 utilisezCREATE 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 iltstzrange
ne 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:
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:Et les requêtes devraient utiliser les mêmes expressions dans la
WHERE
clause.On pourrait être tenté d'indexer simplement la valeur entière convertie en
text
:- mais cette expression ne l'est pas(cast(t_range AS text))
IMMUTABLE
car la représentation textuelle destimestamptz
valeurs dépend dutimezone
paramètre actuel . Vous auriez besoin de mettre des étapes supplémentaires dans uneIMMUTABLE
fonction wrapper qui produit une forme canonique, et de créer un index fonctionnel sur cela ...Mesures supplémentaires / idées alternatives
Si
shape_dist_traveled
peut déjà avoir la même valeur quett.shape_dist_traveled
pour plusieurs de vos lignes mises à jour (et que vous ne comptez pas sur les effets secondaires de vosUPDATE
déclencheurs similaires ...), vous pouvez rendre votre requête plus rapide en excluant les mises à jour vides: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 FULL
serait un poison pour vous, car certains tuples morts (ou l'espace réservé parFILLFACTOR
) sont bénéfiques pour lesUPDATE
performances.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:
la source