web-dev-qa-db-fra.com

Bulk/batch update/upsert dans PostgreSQL

J'écris une extension Django-ORM qui tente de mettre en cache des modèles et de différer leur enregistrement jusqu'à la fin de la transaction. Tout est presque terminé, mais je suis tombé sur une difficulté inattendue dans la syntaxe SQL.

Je ne suis pas un gros administrateur de bases de données, mais d'après ce que j'ai compris, les bases de données ne fonctionnent pas efficacement pour de nombreuses petites requêtes. Quelques grandes requêtes sont bien meilleures. Par exemple, il est préférable d’utiliser de gros lots d’insert (par exemple 100 lignes à la fois) au lieu de 100 one-liners.

D'après ce que je peux voir, SQL ne fournit aucune instruction permettant d'effectuer une mise à jour par lots sur une table. Le terme semble être confus alors, je vais expliquer ce que je veux dire par là. J'ai un tableau de données arbitraires, chaque entrée décrivant une seule ligne dans une table. J'aimerais mettre à jour certaines lignes de la table, chacune utilisant les données de l'entrée correspondante dans le tableau. L'idée est très similaire à un insert de lot. 

Par exemple: Ma table pourrait avoir deux colonnes "id" et "some_col". Le tableau décrivant les données pour une mise à jour par lot est composé de trois entrées: (1, 'first updated'), (2, 'second updated') et (3, 'third updated'). Avant la mise à jour, la table contient des lignes: (1, 'first'), (2, 'second'), (3, 'third').

Je suis tombé sur ce post:

Pourquoi les insertions/mises à jour par lots sont-elles plus rapides? Comment fonctionnent les mises à jour par lots?

ce qui semble faire ce que je veux, mais je ne peux pas vraiment comprendre la syntaxe à la fin.

Je pourrais également supprimer toutes les lignes nécessitant une mise à jour et les réinsérer à l'aide d'une insertion de lot. Toutefois, j'ai du mal à croire que cela fonctionnerait mieux.

Je travaille avec PostgreSQL 8.4, donc certaines procédures stockées sont également possibles ici. Cependant, étant donné que j’ai l’intention d’obtenir le projet en source libre, toute idée plus portable ou tout moyen de faire la même chose sur un SGBDR différent sont les bienvenus.

Question de suivi: Comment faire une instruction "insérer-ou-mettre à jour"/"upsert" par lot?

Résultats de test

J'ai effectué 100x fois 10 opérations d'insertion réparties sur 4 tables différentes (soit 1000 inserts au total). J'ai testé sur Django 1.3 avec un backend PostgreSQL 8.4.

Ce sont les résultats:

  • Toutes les opérations effectuées via Django ORM - chaque étape passe ~ 2,45 secondes,
  • Les mêmes opérations, mais effectuées sans Django ORM - chaque passage ~ 1,48 seconde,
  • N'insérez que des opérations, sans interroger la base de données pour les valeurs de séquence ~ 0.72 secondes,
  • Seulement les opérations d’insertion, exécutées par blocs de 10 (100 blocs au total) ~ 0,19 secondes,
  • N'insérez que des opérations, un gros bloc d'exécution ~ 0,13 secondes.
  • N'insérez que des opérations, environ 250 instructions par bloc, ~ 0.12 secondes.

Conclusion: exécutez autant d'opérations que possible dans un seul fichier connection.execute (). Django lui-même introduit une surcharge substantielle.

Clause de non-responsabilité: je n'ai introduit aucun index autre que les index de clé primaire par défaut. Les opérations d'insertion pourraient donc s'exécuter plus rapidement à cause de cela.

39
julkiewicz

J'ai utilisé 3 stratégies pour le travail transactionnel par lots:

  1. Générez des instructions SQL à la volée, concatérez-les avec des points-virgules, puis soumettez-les en une seule fois. J'ai fait jusqu'à 100 insertions de cette manière, et c'était assez efficace (réalisé contre Postgres).
  2. JDBC possède des fonctionnalités de traitement par lots intégrées, si elles sont configurées. Si vous générez des transactions, vous pouvez purger vos instructions JDBC afin qu’elles soient traitées en une seule fois. Cette tactique nécessite moins d'appels de base de données, car les instructions sont toutes exécutées en un seul lot.
  3. Hibernate prend également en charge le traitement par lots JDBC selon les lignes de l'exemple précédent, mais dans ce cas, vous exécutez une méthode flush() sur Hibernate Session, et non sur la connexion JDBC sous-jacente. Il accomplit la même chose que le traitement par lots JDBC.

Par ailleurs, Hibernate prend également en charge une stratégie de traitement par lots lors de la récupération des collections. Si vous annotez une collection avec @BatchSize, lors de la récupération des associations, Hibernate utilisera IN au lieu de =, ce qui entraînera moins d'instructions SELECT pour charger les collections.

12
atrain

Insert en vrac

Vous pouvez modifier l'insertion en bloc de trois colonnes par Ketema:

INSERT INTO "table" (col1, col2, col3)
  VALUES (11, 12, 13) , (21, 22, 23) , (31, 32, 33);

Il devient:

INSERT INTO "table" (col1, col2, col3)
  VALUES (unnest(array[11,21,31]), 
          unnest(array[12,22,32]), 
          unnest(array[13,23,33]))

Remplacer les valeurs par des espaces réservés:

INSERT INTO "table" (col1, col2, col3)
  VALUES (unnest(?), unnest(?), unnest(?))

Vous devez passer des tableaux ou des listes comme arguments de cette requête. Cela signifie que vous pouvez faire de gros inserts en vrac sans faire de concaténation de ficelle (et de tous ses inconvénients et dangers: injection SQL et citer l'enfer).

Mise à jour en masse

PostgreSQL a ajouté l’extension FROM à UPDATE. Vous pouvez l'utiliser de cette façon:

update "table" 
  set value = data_table.new_value
  from 
    (select unnest(?) as key, unnest(?) as new_value) as data_table
  where "table".key = data_table.key;

Il manque une bonne explication dans le manuel, mais il existe un exemple dans la liste de diffusion postgresql-admin . J'ai essayé de développer là-dessus:

create table tmp
(
  id serial not null primary key,
  name text,
  age integer
);

insert into tmp (name,age) 
values ('keith', 43),('leslie', 40),('bexley', 19),('casey', 6);

update tmp set age = data_table.age
from
(select unnest(array['keith', 'leslie', 'bexley', 'casey']) as name, 
        unnest(array[44, 50, 10, 12]) as age) as data_table
where tmp.name = data_table.name;

Il y a aussi autreposts sur StackExchange expliquant UPDATE...FROM.. en utilisant une clause VALUES au lieu d'une sous-requête. Ils pourraient être plus faciles à lire, mais sont limités à un nombre fixe de lignes.

43
hagello

Les inserts en vrac peuvent être réalisés comme tels:

INSERT INTO "table" ( col1, col2, col3)
  VALUES ( 1, 2, 3 ) , ( 3, 4, 5 ) , ( 6, 7, 8 );

Va insérer 3 rangées.

Les mises à jour multiples sont définies par le standard SQL mais ne sont pas implémentées dans PostgreSQL.

Citation: 

"Selon le standard, la syntaxe de liste de colonnes doit permettre d'affecter une liste De colonnes à partir d'une seule expression de valeur de ligne, telle que

UPDATE accounts SET (nom du dernier contact, nom du premier contact) = (SELECT nom, prénom, prénom FROM vendeurs WHERE salesmen.id = accounts.sales_id); "

Référence: http://www.postgresql.org/docs/9.0/static/sql-update.html

12
Ketema

il est assez rapide de peupler json en recordset (postgresql 9.3+)

big_list_of_tuples = [
    (1, "123.45"),
    ...
    (100000, "678.90"),
]

connection.execute("""
    UPDATE mytable
    SET myvalue = Q.myvalue
    FROM (
        SELECT (value->>0)::integer AS id, (value->>1)::decimal AS myvalue 
        FROM json_array_elements(%s)
    ) Q
    WHERE mytable.id = Q.id
    """, 
    [json.dumps(big_list_of_tuples)]
)
2
nogus

Désactivez autocommit et faites juste un commit à la fin. En langage SQL simple, cela signifie qu’il faut lancer BEGIN au début et COMMIT à la fin. Vous auriez besoin de créer une fonction pour effectuer une ascension réelle.

0
aliasmrchips