La meilleure façon de remplir une nouvelle colonne dans un grand tableau?

33

Nous avons une table de 2,2 Go dans Postgres avec 7 801 611 lignes. Nous y ajoutons une colonne uuid / guid et je me demande quel est le meilleur moyen de remplir cette colonne (car nous voulons lui ajouter une NOT NULLcontrainte).

Si je comprends bien Postgres, une mise à jour est techniquement une suppression et une insertion. Il s'agit donc en fait de reconstruire l'intégralité du tableau de 2,2 Go. De plus, nous avons un esclave qui court donc nous ne voulons pas que cela prenne du retard.

Existe-t-il un meilleur moyen que d’écrire un script qui le remplit progressivement au fil du temps?

Collin Peters
la source
2
Avez-vous déjà lancé une action ALTER TABLE .. ADD COLUMN ...ou faut-il y répondre également?
Ypercubeᵀᴹ
N'a pas encore exécuté de modifications de table, juste en phase de planification. Je l'ai déjà fait auparavant en ajoutant la colonne, en la remplissant, puis en ajoutant la contrainte ou l'index. Cependant, cette table est nettement plus grande et je suis inquiet pour la charge, le verrouillage, la réplication, etc.
Collin Peters

Réponses:

45

Cela dépend beaucoup des détails de vos besoins.

Si vous disposez de suffisamment d'espace libre (au moins 110% de pg_size_pretty((pg_total_relation_size(tbl))) sur le disque et que vous pouvez vous permettre un verrou de partage pendant un certain temps et un verrou exclusif pendant très peu de temps , créez une nouvelle table incluant la uuidcolonne à l'aide de CREATE TABLE AS. Pourquoi?

Le code ci-dessous utilise une fonction du uuid-ossmodule supplémentaire .

  • Verrouillez la table contre les modifications simultanées du SHAREmode (autorisant toujours les lectures simultanées). Les tentatives d'écriture sur la table attendent et finissent par échouer. Voir ci-dessous.

  • Copiez l'intégralité de la table tout en remplissant la nouvelle colonne à la volée - en ordonnant éventuellement les lignes tout en respectant les instructions.
    Si vous envisagez de réorganiser les lignes, veillez à définir la valeur la work_memplus élevée possible (uniquement pour votre session, pas globalement).

  • Ajoutez ensuite des contraintes, des clés étrangères, des index, des déclencheurs, etc. à la nouvelle table. Lors de la mise à jour de grandes parties d'une table, il est beaucoup plus rapide de créer des index à partir de rien que d'ajouter des lignes de manière itérative.

  • Lorsque la nouvelle table est prête, supprimez l'ancienne et renommez la nouvelle pour la remplacer par une nouvelle version. Seule cette dernière étape acquiert un verrou exclusif sur l’ancienne table pour le reste de la transaction - ce qui devrait être très court maintenant.
    Vous devez également supprimer tout objet en fonction du type de table (vues, fonctions utilisant le type de table dans la signature, ...), puis les recréer.

  • Faites tout cela en une seule transaction pour éviter les états incomplets.

BEGIN;
LOCK TABLE tbl IN SHARE MODE;

SET LOCAL work_mem = '???? MB';  -- just for this transaction

CREATE TABLE tbl_new AS 
SELECT uuid_generate_v1() AS tbl_uuid, <list of all columns in order>
FROM   tbl
ORDER  BY ??;  -- optionally order rows favorably while being at it.

ALTER TABLE tbl_new
   ALTER COLUMN tbl_uuid SET NOT NULL
 , ALTER COLUMN tbl_uuid SET DEFAULT uuid_generate_v1()
 , ADD CONSTRAINT tbl_uuid_uni UNIQUE(tbl_uuid);

-- more constraints, indices, triggers?

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME tbl;

-- recreate views etc. if any
COMMIT;

Cela devrait être le plus rapide. Toute autre méthode de mise à jour en place doit également réécrire la table entière, mais de manière plus coûteuse. Vous ne pouvez emprunter cette voie que si vous ne disposez pas de suffisamment d'espace libre sur le disque ou si vous ne pouvez vous permettre de verrouiller la table entière ou de générer des erreurs pour les tentatives d'écriture simultanées.

Qu'advient-il des écritures simultanées?

Une autre transaction (dans d'autres sessions) essayant de INSERT/ UPDATE/ DELETEdans la même table après que votre transaction soit SHAREverrouillée attendra jusqu'à ce que le verrou soit libéré ou qu'un délai expire, selon la première éventualité. Ils échoueront dans les deux cas, car la table à laquelle ils essayaient d'écrire a été supprimée.

La nouvelle table a un nouvel OID de table, mais les transactions simultanées ont déjà résolu le nom de la table en OID de la table précédente . Lorsque le verrou est enfin relâché, ils essaient de verrouiller la table eux-mêmes avant d'écrire et de constater qu'elle a disparu. Postgres répondra:

ERROR: could not open relation with OID 123456

Où se 123456trouve l'OID de l'ancienne table. Vous devez intercepter cette exception et réessayer les requêtes dans le code de votre application pour l'éviter.

Si vous ne pouvez pas vous le permettre, vous devez conserver votre table d'origine.

Deux alternatives en gardant la table existante

  1. Mettre à jour en place (éventuellement en exécutant la mise à jour sur de petits segments à la fois) avant d'ajouter la NOT NULLcontrainte. Ajouter une nouvelle colonne avec des valeurs NULL et sans NOT NULLcontrainte est bon marché.
    Depuis Postgres 9.2, vous pouvez également créer une CHECKcontrainte avecNOT VALID :

    La contrainte sera toujours appliquée aux insertions ou mises à jour ultérieures

    Cela vous permet de mettre à jour les lignes peu à peu - dans plusieurs transactions distinctes . Cela évite de conserver les verrous de ligne trop longtemps et permet également de réutiliser des lignes mortes. (Vous devrez exécuter VACUUMmanuellement s'il n'y a pas assez de temps entre autovacuum et les autres.) Enfin, ajoutez la NOT NULLcontrainte et supprimez-la NOT VALID CHECK:

    ALTER TABLE tbl ADD CONSTRAINT tbl_no_null CHECK (tbl_uuid IS NOT NULL) NOT VALID;
    
    -- update rows in multiple batches in separate transactions
    -- possibly run VACUUM between transactions
    
    ALTER TABLE tbl ALTER COLUMN tbl_uuid SET NOT NULL;
    ALTER TABLE tbl ALTER DROP CONSTRAINT tbl_no_null;

    Réponse associée discuter NOT VALIDplus en détail:

  2. Préparez le nouvel état dans une table temporaire , TRUNCATEl'original et rechargez à partir de la table temporaire. Tout en une transaction . Vous devez toujours SHAREverrouiller votre ordinateur avant de préparer la nouvelle table pour éviter de perdre des écritures simultanées.

    Détails dans ces réponses connexes sur SO:

Erwin Brandstetter
la source
Réponse fantastique! Exactement l'info que je cherchais. Deux questions 1. Avez-vous une idée d'un moyen simple de tester la durée d'une telle action? 2. Si cela prend 5 minutes, qu'advient-il des actions qui tentent de mettre à jour une ligne de cette table pendant ces 5 minutes?
Collin Peters
@CollinPeters: 1. La majeure partie du temps serait consacrée à la copie de la grande table - et éventuellement à la recréation d'indices et de contraintes (cela dépend). Abandonner et renommer n’est pas cher. Pour tester, vous pouvez exécuter votre script SQL préparé sans LOCKet sans le DROP. Je ne pouvais que pousser des suppositions sauvages et inutiles. En ce qui concerne le point 2, veuillez examiner l'addendum à ma réponse.
Erwin Brandstetter
@ ErwinBrandstetter Continuez à recréer les vues, donc si j’en ai une douzaine qui utilise toujours l’ancienne table (oid) après le changement de nom de table. Existe-t-il un moyen d'effectuer un remplacement en profondeur plutôt que de réexécuter l'intégralité de l'actualisation / création de la vue?
CodeFarmer
@ CodeFarmer: Si vous renommez simplement une table, les vues continuent à fonctionner avec la table renommée. Pour que les vues utilisent la nouvelle table à la place, vous devez les recréer en fonction de la nouvelle table. (Permet également de supprimer l’ancienne table.) Aucun moyen (pratique) de la contourner.
Erwin Brandstetter
14

Je n'ai pas de "meilleure" réponse, mais j'ai une "moins mauvaise" réponse qui pourrait vous permettre de faire les choses assez rapidement.

Ma table avait des lignes de 2MM et les performances de la mise à jour étaient médiocres lorsque j'ai tenté d'ajouter une colonne d'horodatage secondaire à la première.

ALTER TABLE mytable ADD new_timestamp TIMESTAMP ;
UPDATE mytable SET new_timestamp = old_timestamp ;
ALTER TABLE mytable ALTER new_timestamp SET NOT NULL ;

Après 40 minutes d’attente, j’ai essayé ceci sur un petit lot pour avoir une idée du temps que cela pouvait prendre - la prévision était d’environ 8 heures.

La réponse acceptée est définitivement meilleure - mais cette table est fortement utilisée dans ma base de données. Il y a quelques dizaines de tables avec FKEY dessus; Je voulais éviter de changer de touches étrangères sur autant de tables. Et puis il y a des points de vue.

Un peu de recherche de documents, d'études de cas et de StackOverflow, et j'ai eu le "A-Ha!" moment. Le drain ne se trouvait pas sur la mise à jour principale, mais sur toutes les opérations INDEX. Ma table comportait 12 index, quelques-uns pour les contraintes uniques, quelques-uns pour accélérer le planificateur de requêtes et quelques-uns pour la recherche en texte intégral.

Chaque ligne mise à jour ne fonctionnait pas uniquement sur un élément DELETE / INSERT, mais aussi sur la surcharge liée à la modification de chaque index et à la vérification des contraintes.

Ma solution consistait à supprimer tous les index et contraintes, à mettre à jour la table, puis à rajouter tous les index / contraintes.

Il a fallu environ 3 minutes pour écrire une transaction SQL ayant les conséquences suivantes:

  • COMMENCER;
  • abandonné les index / constaints
  • table de mise à jour
  • rajouter des index / contraintes
  • COMMETTRE;

Le script a pris 7 minutes pour s'exécuter.

La réponse acceptée est définitivement meilleure et plus appropriée ... et élimine pratiquement le besoin de temps d'arrêt. Dans mon cas, toutefois, il aurait fallu beaucoup plus de "développeurs" pour utiliser cette solution et nous avions une fenêtre d'indisponibilité planifiée de 30 minutes pour y parvenir. Notre solution en tenait compte dans 10.

Jonathan Vanasco
la source