web-dev-qa-db-fra.com

Comment faire des mises à jour non bloquantes importantes dans PostgreSQL?

Je veux faire une grande mise à jour sur une table dans PostgreSQL, mais je n'ai pas besoin que l'intégrité transactionnelle soit maintenue sur toute l'opération, car je sais que la colonne que je modifie ne sera pas écrite ou lue pendant la mise à jour. Je veux savoir s'il existe un moyen simple dans la console psql pour rendre ces types d'opérations plus rapides.

Par exemple, disons que j'ai une table appelée "commandes" avec 35 millions de lignes, et je veux faire ceci:

UPDATE orders SET status = null;

Pour éviter d'être détourné vers une discussion hors sujet, supposons que toutes les valeurs de statut pour les 35 millions de colonnes sont actuellement définies sur la même valeur (non nulle), rendant ainsi un index inutile.

Le problème avec cette instruction est qu'elle prend beaucoup de temps pour entrer en vigueur (uniquement à cause du verrouillage) et toutes les lignes modifiées sont verrouillées jusqu'à la fin de la mise à jour. Cette mise à jour peut prendre 5 heures, alors que quelque chose comme

UPDATE orders SET status = null WHERE (order_id > 0 and order_id < 1000000);

peut prendre 1 minute. Plus de 35 millions de lignes, faire ce qui précède et le diviser en morceaux de 35 ne prendrait que 35 minutes et me ferait gagner 4 heures et 25 minutes.

Je pourrais le décomposer encore plus avec un script (en utilisant le pseudocode ici):

for (i = 0 to 3500) {
  db_operation ("UPDATE orders SET status = null
                 WHERE (order_id >" + (i*1000)"
             + " AND order_id <" + ((i+1)*1000) " +  ")");
}

Cette opération peut se terminer en quelques minutes au lieu de 35.

Cela revient donc à ce que je demande vraiment. Je ne veux pas écrire un script flippant pour interrompre les opérations à chaque fois que je veux faire une grosse mise à jour unique comme celle-ci. Existe-t-il un moyen d'accomplir ce que je veux entièrement dans SQL?

61
S D

Colonne/ligne

... Je n'ai pas besoin que l'intégrité transactionnelle soit maintenue pendant toute l'opération, car je sais que la colonne que je modifie ne sera pas écrite ou lue pendant la mise à jour.

Tout UPDATE dans modèle MVCC de PostgreSQL écrit une nouvelle version de toute la ligne. Si les transactions simultanées changent la colonne any de la même ligne, des problèmes de simultanéité chronophages surviennent. Détails dans le manuel. Connaître la même chose colonne ne sera pas touché par les transactions simultanées évite certaines complications possibles, mais pas d'autres .

Indice

Pour éviter d'être détourné vers une discussion hors sujet, supposons que toutes les valeurs de statut pour les 35 millions de colonnes sont actuellement définies sur la même valeur (non nulle), rendant ainsi un index inutile.

Lors de la mise à jour du table entière (ou des parties principales de celui-ci) Postgres = n'utilise jamais d'index. Une analyse séquentielle est plus rapide lorsque toutes ou la plupart des lignes doivent être lues. Au contraire: la maintenance de l'index signifie un coût supplémentaire pour le UPDATE.

Performance

Par exemple, disons que j'ai une table appelée "commandes" avec 35 millions de lignes, et je veux faire ceci:

UPDATE orders SET status = null;

Je comprends que vous visez une solution plus générale (voir ci-dessous). Mais pour répondre la vraie question demandé: Cela peut être traité dans quelques millisecondes, quelle que soit la taille de la table:

ALTER TABLE orders DROP column status
                 , ADD  column status text;

Par documentation:

Lorsqu'une colonne est ajoutée avec ADD COLUMN, toutes les lignes existantes du tableau sont initialisées avec la valeur par défaut de la colonne (NULL si aucune clause DEFAULT n'est spécifiée). S'il n'y a pas de clause DEFAULT, il s'agit simplement d'un changement de métadonnées ...

Et:

Le DROP COLUMN form ne supprime pas physiquement la colonne, mais la rend simplement invisible pour les opérations SQL. Les opérations d'insertion et de mise à jour suivantes dans la table stockeront une valeur nulle pour la colonne. Ainsi, la suppression d'une colonne est rapide, mais elle ne réduira pas immédiatement la taille sur disque de votre table, car l'espace occupé par la colonne supprimée n'est pas récupéré. L'espace sera récupéré au fil du temps à mesure que les lignes existantes sont mises à jour. (Ces instructions ne s'appliquent pas lors de la suppression de la colonne système oid; cela se fait avec une réécriture immédiate.)

Assurez-vous de ne pas avoir d'objets en fonction de la colonne (contraintes de clé étrangère, index, vues, ...). Vous devrez les supprimer/recréer. Sauf que, de minuscules opérations sur la table du catalogue système pg_attribute fait le travail. Nécessite un verrou exclusif sur la table, ce qui peut être un problème pour une charge simultanée élevée. Comme cela ne prend que quelques millisecondes, tout devrait bien se passer.

Si vous avez une colonne par défaut que vous souhaitez conserver, ajoutez-la de nouveau dans une commande distincte. Le faire dans la même commande l'appliquerait immédiatement à toutes les lignes, annulant l'effet. Vous pouvez ensuite mettre à jour les colonnes existantes dans lots . Suivez le lien de documentation et lisez les Notes dans le manuel.

Solution générale

dblink a été mentionné dans une autre réponse. Il permet d'accéder aux bases de données Postgres "distantes" dans des connexions implicites séparées. La base de données "distante" peut être la base actuelle, réalisant ainsi "transactions autonomes": ce que la fonction écrit dans la base de données "distante" est validé et ne peut pas être annulé.

Cela permet d'exécuter une seule fonction qui met à jour une grande table en parties plus petites et chaque partie est validée séparément. Évite de créer des frais généraux de transaction pour un très grand nombre de lignes et, plus important encore, libère les verrous après chaque partie. Cela permet aux opérations simultanées de se poursuivre sans trop de retard et rend les blocages moins probables.

Si vous ne disposez pas d'un accès simultané, cela n'est guère utile - sauf pour éviter ROLLBACK après une exception. Considérez également SAVEPOINT pour ce cas.

Avertissement

Tout d'abord, de nombreuses petites transactions sont en fait plus chères. Ceci n'a de sens que pour les grandes tables. Le sweet spot dépend de nombreux facteurs.

Si vous n'êtes pas sûr de ce que vous faites: une seule transaction est la méthode sûre. Pour que cela fonctionne correctement, les opérations simultanées sur la table doivent fonctionner. Par exemple: concurrent écrit peut déplacer une ligne vers une partition qui est censée avoir déjà été traitée. Ou les lectures simultanées peuvent voir des états intermédiaires incohérents. Vous avez été prévenu.

Instructions étape par étape

Le module supplémentaire dblink doit être installé en premier:

La configuration de la connexion avec dblink dépend beaucoup de la configuration de votre cluster DB et des politiques de sécurité en place. Cela peut être délicat. Réponse ultérieure associée avec plus comment se connecter avec dblink :

Créez un FOREIGN SERVER et a USER MAPPING comme indiqué ici pour simplifier et rationaliser la connexion (sauf si vous en avez déjà une).
En supposant un serial PRIMARY KEY avec ou sans lacunes.

CREATE OR REPLACE FUNCTION f_update_in_steps()
  RETURNS void AS
$func$
DECLARE
   _step int;   -- size of step
   _cur  int;   -- current ID (starting with minimum)
   _max  int;   -- maximum ID
BEGIN
   SELECT INTO _cur, _max  min(order_id), max(order_id) FROM orders;
                                        -- 100 slices (steps) hard coded
   _step := ((_max - _cur) / 100) + 1;  -- rounded, possibly a bit too small
                                        -- +1 to avoid endless loop for 0
   PERFORM dblink_connect('myserver');  -- your foreign server as instructed above

   FOR i IN 0..200 LOOP                 -- 200 >> 100 to make sure we exceed _max
      PERFORM dblink_exec(
       $$UPDATE public.orders
         SET    status = 'foo'
         WHERE  order_id >= $$ || _cur || $$
         AND    order_id <  $$ || _cur + _step || $$
         AND    status IS DISTINCT FROM 'foo'$$);  -- avoid empty update

      _cur := _cur + _step;

      EXIT WHEN _cur > _max;            -- stop when done (never loop till 200)
   END LOOP;

   PERFORM dblink_disconnect();
END
$func$  LANGUAGE plpgsql;

Appel:

SELECT f_update_in_steps();

Vous pouvez paramétrer n'importe quelle partie selon vos besoins: le nom de la table, le nom de la colonne, la valeur, ... assurez-vous simplement de nettoyer les identifiants pour éviter l'injection SQL:

À propos de la mise à jour vide:

37

Vous devez déléguer cette colonne à une autre table comme celle-ci:

create table order_status (
  order_id int not null references orders(order_id) primary key,
  status int not null
);

Ensuite, votre opération de définition de status = NULL sera instantanée:

truncate order_status;
4
Tometzky

Tout d'abord - êtes-vous sûr de devoir mettre à jour toutes les lignes?

Peut-être que certaines des lignes ont déjà status NULL?

Si oui, alors:

UPDATE orders SET status = null WHERE status is not null;

Quant au partitionnement du changement - ce n'est pas possible en SQL pur. Toutes les mises à jour sont en une seule transaction.

Une façon possible de le faire en "SQL pur" serait d'installer dblink, de se connecter à la même base de données en utilisant dblink, puis d'émettre de nombreuses mises à jour via dblink, mais il semble exagéré pour une tâche aussi simple.

Habituellement, l'ajout d'un where correct résout le problème. Si ce n'est pas le cas, partitionnez-le simplement manuellement. Écrire un script, c'est trop - vous pouvez généralement le faire dans une simple ligne:

Perl -e '
    for (my $i = 0; $i <= 3500000; $i += 1000) {
        printf "UPDATE orders SET status = null WHERE status is not null
                and order_id between %u and %u;\n",
        $i, $i+999
    }
'

J'ai encapsulé des lignes ici pour plus de lisibilité, généralement c'est une seule ligne. La sortie de la commande ci-dessus peut être directement envoyée à psql:

Perl -e '...' | psql -U ... -d ...

Ou tout d'abord à déposer puis à psql (au cas où vous auriez besoin du fichier plus tard):

Perl -e '...' > updates.partitioned.sql
psql -U ... -d ... -f updates.partitioned.sql
3
user80168

J'utiliserais CTAS:

begin;
create table T as select col1, col2, ..., <new value>, colN from orders;
drop table orders;
alter table T rename to orders;
commit;
3
mys

Êtes-vous sûr que c'est à cause du verrouillage? Je ne pense pas et il y a beaucoup d'autres raisons possibles. Pour le savoir, vous pouvez toujours essayer de ne faire que le verrouillage. Essayez ceci: COMMENCEZ; CHOISISSEZ MAINTENANT (); SÉLECTIONNEZ * DE LA COMMANDE POUR LA MISE À JOUR; CHOISISSEZ MAINTENANT (); RETOUR EN ARRIERE;

Pour comprendre ce qui se passe réellement, vous devez d'abord exécuter un EXPLAIN (EXPLAIN UPDATE ordonne le statut SET ...) et/ou EXPLAIN ANALYZE. Vous découvrirez peut-être que vous n'avez pas assez de mémoire pour effectuer efficacement la MISE À JOUR. Si c'est le cas, SET work_mem TO 'xxxMB'; pourrait être une solution simple.

En outre, réduisez le journal PostgreSQL pour voir si certains problèmes liés aux performances se produisent.

2
Martin Torhage

Postgres utilise MVCC (contrôle de concurrence multi-version), évitant ainsi tout verrouillage si vous êtes le seul écrivain; un nombre illimité de lecteurs simultanés peut fonctionner sur la table, et il n'y aura pas de verrouillage.

Donc, si cela prend vraiment 5 heures, cela doit être pour une raison différente (par exemple, si vous faites avez des écritures simultanées, contrairement à votre affirmation que vous n'en avez pas).

2
Martin v. Löwis

Je ne suis en aucun cas un DBA, mais une conception de base de données où vous auriez fréquemment à mettre à jour 35 millions de lignes pourrait avoir… des problèmes.

Un simple WHERE status IS NOT NULL pourrait accélérer un peu les choses (à condition que vous ayez un index sur le statut) - ne connaissant pas le cas d'utilisation réel, je suppose que si cela est exécuté fréquemment, une grande partie des 35 millions de lignes pourraient déjà avoir un statut nul .

Cependant, vous pouvez faire des boucles dans la requête via instruction LOOP . Je vais juste préparer un petit exemple:

CREATE OR REPLACE FUNCTION nullstatus(count INTEGER) RETURNS integer AS $$
DECLARE
    i INTEGER := 0;
BEGIN
    FOR i IN 0..(count/1000 + 1) LOOP
        UPDATE orders SET status = null WHERE (order_id > (i*1000) and order_id <((i+1)*1000));
        RAISE NOTICE 'Count: % and i: %', count,i;
    END LOOP;
    RETURN 1;
END;
$$ LANGUAGE plpgsql;

Il peut ensuite être exécuté en faisant quelque chose qui ressemble à:

SELECT nullstatus(35000000);

Vous souhaiterez peut-être sélectionner le nombre de lignes, mais sachez que le nombre exact de lignes peut prendre beaucoup de temps. Le wiki PostgreSQL contient un article sur comptage lent et comment l'éviter .

De plus, la partie RAISE NOTICE est juste là pour garder une trace de la longueur du script. Si vous ne surveillez pas les avis ou ne vous en souciez pas, il serait préférable de les laisser de côté.

2
mikl

Quelques options qui n'ont pas été mentionnées:

Utilisez l'astuce nouveau tablea . Probablement, ce que vous auriez à faire dans votre cas est d'écrire des déclencheurs pour le gérer afin que les modifications apportées à la table d'origine soient également propagées dans votre copie de table, quelque chose comme ça ... ( percona est un exemple de quelque chose qui le fait de manière déclenchée). Une autre option pourrait être la "créer une nouvelle colonne puis remplacer l'ancienne par elle" astuce , pour éviter les verrous (on ne sait pas si cela aide à la vitesse).

Calculez éventuellement l'ID max, puis générez "toutes les requêtes dont vous avez besoin" et transmettez-les en une seule requête comme update X set Y = NULL where ID < 10000 and ID >= 0; update X set Y = NULL where ID < 20000 and ID > 10000; ... alors il pourrait ne pas faire autant de verrouillage, et être toujours entièrement SQL, bien que vous ayez une logique supplémentaire pour le faire :(

1
rogerdpack

PostgreSQL version 11 gère cela automatiquement pour vous avec la fonction Fast ALTER TABLE ADD COLUMN avec une valeur par défaut non NULL . Veuillez effectuer la mise à niveau vers la version 11 si possible.

Une explication est fournie dans ce article de blog .

0
axiopisty