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 quelle est la meilleure façon de remplir cette colonne (car nous voulons ajouter un NOT NULL
contrainte).
Si je comprends bien Postgres, une mise à jour est techniquement une suppression et une insertion, c'est donc essentiellement la reconstruction de l'ensemble du tableau de 2,2 Go. De plus, nous avons un esclave qui court donc nous ne voulons pas que cela traîne derrière.
Y a-t-il un meilleur moyen que d'écrire un script qui le remplit lentement au fil du temps?
Cela dépend beaucoup des détails de vos besoins.
Si vous avez suffisamment d'espace libre (au moins 110% de - pg_size_pretty((pg_total_relation_size(tbl))
) sur le disque et peut se permettre un verrou de partage pendant un certain temps et un verrou exclusif pour un temps très court , puis créez une nouvelle table incluant le uuid
colonne utilisant CREATE TABLE AS
. Pourquoi?
Le code ci-dessous utilise une fonction de la fonction supplémentaire uuid-oss
module .
Verrouille la table contre les modifications simultanées en mode SHARE
(autorisant toujours les lectures simultanées). Les tentatives d'écriture dans la table vont attendre et éventuellement échouer. Voir ci-dessous.
Copiez la table entière tout en remplissant la nouvelle colonne à la volée - en ordonnant éventuellement les lignes favorablement tout en y étant.
Si vous allez réorganiser les lignes, assurez-vous de définir work_mem
aussi haut que vous pouvez vous le permettre (juste pour votre session, pas globalement).
Alors ajoutez des contraintes, des clés étrangères, des indices, des déclencheurs, etc. à la nouvelle table. Lors de la mise à jour de grandes portions d'une table, il est beaucoup plus rapide de créer des index à partir de zéro 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 en faire un remplacement direct. Seule cette dernière étape acquiert un verrou exclusif sur l'ancienne table pour le reste de la transaction - qui devrait être très court maintenant.
Cela nécessite également de supprimer tout objet en fonction du type de table (vues, fonctions utilisant le type de table dans la signature, ...) et de les recréer ensuite.
Faites tout en une seule transaction pour éviter des é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, juste de manière plus coûteuse. Vous ne choisiriez cette voie que si vous n'avez pas suffisamment d'espace libre sur le disque ou si vous ne pouvez pas vous permettre de verrouiller la table entière ou de générer des erreurs pour les tentatives d'écriture simultanées.
Une autre transaction (dans d'autres sessions) essayant de INSERT
/UPDATE
/DELETE
dans le même tableau après que votre transaction a pris le verrou SHARE
, attendra jusqu'à le verrou est libéré ou un délai d'attente intervient, selon la première éventualité. Ils échoueront dans les deux cas, car la table sur laquelle ils essayaient d'écrire a été supprimée sous eux.
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 du tableau précédent. Lorsque le verrou est enfin libéré, ils essaient de verrouiller la table eux-mêmes avant d'y écrire et constatent qu'elle a disparu. Postgres répondra:
ERROR: could not open relation with OID 123456
Où 123456
est le OID de l'ancienne table. Vous devez intercepter cette exception et réessayer les requêtes dans votre code d'application pour l'éviter.
Si vous ne pouvez pas vous le permettre, vous devez garder votre table d'origine.
Mettre à jour sur place (éventuellement en exécutant la mise à jour sur de petits segments à la fois) avant d'ajouter le NOT NULL
contrainte. Ajout d'une nouvelle colonne avec des valeurs NULL et sans NOT NULL
la contrainte est bon marché.
Depuis Postgres 9.2 , vous pouvez également créer un CHECK
contrainte avec NOT VALID
:
La contrainte sera toujours appliquée contre les insertions ou mises à jour ultérieures
Cela vous permet de mettre à jour les lignes peu à peu - dans plusieurs transactions distinctes . Cela évite de garder les verrous de ligne trop longtemps et permet également de réutiliser les lignes mortes. (Vous devrez exécuter VACUUM
manuellement s'il n'y a pas assez de temps entre les deux pour que le vide automatique démarre.) Enfin, ajoutez le NOT NULL
contrainte et supprimez le NOT VALID CHECK
contrainte:
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 connexe discutant NOT VALID
plus en détail:
Préparez le nouvel état dans une table temporaire , TRUNCATE
l'original et la recharge de la table temporaire. Tout en une transaction . Vous devez toujours prendre un SHARE
verrou avant préparation de la nouvelle table pour éviter de perdre des écritures simultanées .
Détails dans ces réponses connexes sur SO:
Je n'ai pas de "meilleure" réponse, mais j'ai une "moins mauvaise" réponse qui pourrait vous permettre de faire avancer les choses assez rapidement.
Ma table avait 2MM de lignes et les performances de mise à jour étaient en train de chugger lorsque j'ai essayé d'ajouter une colonne d'horodatage secondaire qui était par défaut 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 avoir suspendu pendant 40 minutes, j'ai essayé cela sur un petit lot pour avoir une idée du temps que cela pourrait prendre - la prévision était d'environ 8 heures.
La réponse acceptée est certainement meilleure - mais ce tableau est largement utilisé dans ma base de données. Il y a quelques dizaines de tables que FKEY dessus; Je voulais éviter de basculer les clés étrangères sur autant de tables. Et puis il y a des vues.
Un peu de recherche de documents, d'études de cas et de StackOverflow, et j'ai eu le "A-Ha!" moment. Le drain n'était pas sur le noyau UPDATE, mais sur toutes les opérations INDEX. Ma table comportait 12 index - quelques-uns pour des 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 qui a été MISE À JOUR ne fonctionnait pas seulement sur un SUPPRIMER/INSÉRER, mais aussi la surcharge de modification de chaque index et de vérification des contraintes.
Ma solution a été de supprimer tous les index et contraintes, de mettre à jour la table, puis de réintégrer tous les index/contraintes.
Il a fallu environ 3 minutes pour écrire une transaction SQL qui a fait ce qui suit:
Le script a pris 7 minutes pour s'exécuter.
La réponse acceptée est certainement meilleure et plus appropriée ... et élimine pratiquement le besoin de temps d'arrêt. Dans mon cas cependant, il aurait fallu beaucoup plus de travail de "développeur" pour utiliser cette solution et nous avions une fenêtre de 30 minutes de temps d'arrêt planifié dans laquelle cela pourrait être accompli. Notre solution l'a corrigé en 10.