web-dev-qa-db-fra.com

SUPPRESSION très lente dans PostgreSQL, solution de contournement?

J'ai une base de données sur PostgreSQL 9.2 qui a un schéma principal avec environ 70 tables et un nombre variable de schémas par client à structure identique de 30 tables chacun. Les schémas clients ont des clés étrangères référençant le schéma principal et non l'inverse.

Je viens de commencer à remplir la base de données avec de vraies données tirées de la version précédente. La base de données avait atteint environ 1,5 Go (elle devrait atteindre plusieurs dizaines de Go en quelques semaines) lorsque j'ai dû effectuer une suppression en masse dans une table très centrale du schéma principal. Toutes les clés étrangères concernées sont marquées ON DELETE CASCADE.

Il n'était pas surprenant que cela prenne beaucoup de temps, mais après 12 heures, il est devenu clair que je ferais mieux de recommencer, de supprimer la base de données et de relancer la migration. Mais que se passe-t-il si je dois répéter cette opération plus tard lorsque la base de données est active et beaucoup plus grande? Existe-t-il des méthodes alternatives plus rapides?

Serait-ce beaucoup plus rapide si j'écrivais un script qui va parcourir les tables dépendantes, en commençant par la table la plus éloignée de la table centrale, en supprimant les lignes dépendantes table par table?

Un détail important est qu'il existe des déclencheurs sur certaines tables.

36
jd.

J'avais un problème similaire. Il s'avère que ces ON DELETE CASCADE les déclencheurs ralentissaient un peu les choses, car ces suppressions en cascade étaient terriblement lentes.

J'ai résolu le problème en créant des index sur les champs de clé étrangère sur les tables de référence, et je suis passé de quelques heures pour la suppression à quelques secondes.

34
ailnlv

Vous avez quelques options. La meilleure option consiste à exécuter une suppression par lots afin que les déclencheurs ne soient pas activés. Désactivez les déclencheurs avant de les supprimer, puis réactivez-les. Cela vous fait gagner beaucoup de temps. Par exemple:

ALTER TABLE tablename DISABLE TRIGGER ALL; 
DELETE ...; 
ALTER TABLE tablename ENABLE TRIGGER ALL;

Une clé majeure ici est que vous souhaitez minimiser la profondeur des sous-requêtes. Dans ce cas, vous souhaiterez peut-être configurer des tables temporaires pour stocker les informations pertinentes afin d'éviter les sous-requêtes approfondies lors de votre suppression.

31
Chris Travers

La méthode la plus simple pour résoudre le problème consiste à interroger le timing détaillé à partir de PostgreSQL: EXPLAIN . Pour cela, vous devez trouver au moins une seule requête qui se termine mais prend plus de temps que prévu. Disons que cette ligne ressemblerait à

delete from mydata where id='897b4dde-6a0d-4159-91e6-88e84519e6b6';

Au lieu d'exécuter vraiment cette commande, vous pouvez le faire

begin;
explain (analyze,buffers,timing) delete from mydata where id='897b4dde-6a0d-4159-91e6-88e84519e6b6';
rollback;

Le rollback à la fin permet d'exécuter cela sans vraiment modifier la base de données mais vous obtenez toujours le timing détaillé de ce qui a pris combien. Après avoir exécuté cela, vous pouvez trouver dans la sortie qu'un déclencheur provoque d'énormes retards:

...
Trigger for constraint XYZ123: time=12311.292 calls=1
...

Le time est en ms (milliseconde) donc la vérification de cette contraint a pris environ 12,3 secondes. Vous devez ajouter un nouveau INDEX sur les colonnes requises afin que ce déclencheur puisse être calculé efficacement. Pour les références de clé étrangère, la colonne qui fait référence à une autre table doit être indexée (c'est-à-dire la colonne source, pas la colonne cible). PostgreSQL ne crée pas automatiquement de tels index pour vous et DELETE est la seule requête courante où vous avez vraiment vraiment besoin de cet index. Par conséquent, vous pouvez avoir accumulé des années de données jusqu'à ce que vous rencontriez le cas où DELETE est trop lent en raison de l'absence d'index.

Une fois que vous avez corrigé les performances de cette contrainte (ou une autre chose qui a pris trop de temps), répétez la commande dans le bloc begin/rollback pour pouvoir comparer le nouveau temps d'exécution au précédent. Continuez jusqu'à ce que vous soyez satisfait du temps de réponse de suppression d'une seule ligne (j'ai eu une requête pour passer de 25,6 secondes à 15 ms simplement en ajoutant différents index). Ensuite, vous pouvez procéder à votre suppression complète sans aucun piratage.

(Notez que EXPLAIN a besoin d'une requête qui peut se terminer avec succès. J'ai eu une fois un problème où PostgreSQL a mis trop de temps à comprendre qu'une suppression allait violer une contrainte de clé étrangère et dans ce cas EXPLAIN ne peut pas être utilisé car il n'émettra pas de synchronisation pour les requêtes ayant échoué. Je ne connais aucun moyen facile de déboguer les problèmes de performances dans un tel cas.)

14
Mikko Rantalainen

La désactivation des déclencheurs peut constituer une menace pour l'intégrité de la base de données et ne peut pas être recommandée; cependant, si vous êtes sûr que votre opération est à l'épreuve des défaillances de contraintes, vous pouvez désactiver les déclencheurs, comme suit:

SET session_replication_role = replica;

Exécutez le DELETE ici.

Pour restaurer les déclencheurs, exécutez:

SET session_replication_role = DEFAULT;

Source ici.

9
Pinimo

Si vous avez des déclencheurs ON DELETE CASCADE, ils sont là, espérons-le, pour une raison et ne doivent donc pas être désactivés. Une autre astuce (ajoutez toujours vos indices) qui fonctionne pour moi est de créer une fonction de suppression qui supprime manuellement les données en commençant par les tables à la fin de la cascade, et fonctionne vers la table principale. (C'est la même chose que si vous aviez un déclencheur ON DELETE RESTRICT)

CREATE TABLE tablea (
    tablea_uid integer
);

CREATE TABLE tableb (
    tableb_uid integer,
    tablea_rid integer REFERENCES tablea(tablea_uid)
);

CREATE TABLE tablec (
    tablec_uid integer,
    tableb_rid integer REFERENCES tableb(tableb_uid)
);

Dans ce cas, supprimez les données dans tablec puis tableb puis tablea

CREATE OR REPLACE FUNCTION delete_in_order()
 RETURNS void AS $$

    DELETE FROM tablec;
    DELETE FROM tableb;
    DELETE FROM tablea;

$$ LANGUAGE SQL;
1
blindguy