web-dev-qa-db-fra.com

Comment fonctionne COPY et pourquoi est-il tellement plus rapide que INSERT?

Aujourd'hui, j'ai passé ma journée à améliorer les performances de mon script Python qui pousse les données dans ma base de données Postgres. J'insérais auparavant des enregistrements en tant que tels:

query = "INSERT INTO my_table (a,b,c ... ) VALUES (%s, %s, %s ...)";
for d in data:
    cursor.execute(query, d)

J'ai ensuite réécrit mon script pour qu'il crée un fichier en mémoire qui est utilisé pour la commande COPY de Postgres, qui me permet de copier les données d'un fichier vers ma table:

f = StringIO(my_tsv_string)
cursor.copy_expert("COPY my_table FROM STDIN WITH CSV DELIMITER AS E'\t' ENCODING 'utf-8' QUOTE E'\b' NULL ''", f)

La méthode COPY était incroyablement plus rapide .

METHOD      | TIME (secs)   | # RECORDS
=======================================
COPY_FROM   | 92.998    | 48339
INSERT      | 1011.931  | 48377

Mais je ne trouve aucune information expliquant pourquoi? En quoi cela fonctionne-t-il différemment d'un multiligne INSERT de telle sorte qu'il le rend tellement plus rapide?

Voir aussi ce benchmark :

# original
0.008857011795043945: query_builder_insert
0.0029380321502685547: copy_from_insert

#  10 records
0.00867605209350586: query_builder_insert
0.003248929977416992: copy_from_insert

# 10k records
0.041108131408691406: query_builder_insert
0.010066032409667969: copy_from_insert

# 1M records
3.464181900024414: query_builder_insert
0.47070908546447754: copy_from_insert

# 10M records
38.96936798095703: query_builder_insert
5.955034017562866: copy_from_insert
20
turnip

Il y a un certain nombre de facteurs à l'œuvre ici:

  • Latence du réseau et retards aller-retour
  • Frais généraux par instruction dans PostgreSQL
  • Changements de contexte et retards du planificateur
  • COMMIT coûts, si pour les personnes faisant un commit par insert (vous ne l'êtes pas)
  • COPY- optimisations spécifiques pour le chargement en masse

La latence du réseau

Si le serveur est distant, vous pourriez "payer" un "prix" à temps fixe par relevé de, disons, 50 ms (1/20e de seconde). Ou bien plus pour certaines bases de données hébergées dans le cloud. Étant donné que l'insertion suivante ne peut pas commencer avant que la dernière ne se termine avec succès, cela signifie que votre maximum taux d'insertions est de 1000/round-trip-latency-in-ms lignes par seconde. À une latence de 50 ms ("temps de ping"), c'est 20 lignes/seconde. Même sur un serveur local, ce délai n'est pas nul. Alors que COPY remplit juste les fenêtres d'envoi et de réception TCP, et diffuse les lignes aussi rapidement que la base de données peut les écrire et que le réseau peut les transférer. Il n'est pas affecté par la latence beaucoup, et pourrait insérer des milliers de lignes par seconde sur la même liaison réseau.

Coûts par instruction dans PostgreSQL

Il y a aussi des coûts pour analyser, planifier et exécuter une instruction dans PostgreSQL. Il doit prendre des verrous, ouvrir des fichiers de relations, rechercher des index, etc. COPY essaie de faire tout cela une fois, au début, puis se concentre uniquement sur le chargement des lignes aussi vite que possible.

Coûts de changement de tâche/contexte

Il y a des coûts de temps supplémentaires dus au fait que le système d'exploitation doit basculer entre postgres en attente d'une ligne pendant que votre application se prépare et l'envoie, puis votre application attend la réponse de postgres pendant que postgres traite la ligne. Chaque fois que vous passez de l'un à l'autre, vous perdez un peu de temps. Plus de temps est potentiellement perdu à suspendre et à reprendre divers états de noyau de bas niveau lorsque les processus entrent et sortent des états d'attente.

Manquer les optimisations COPY

En plus de tout cela, COPY a quelques optimisations qu'il peut utiliser pour certains types de charges. S'il n'y a pas de clé générée et que les valeurs par défaut sont des constantes par exemple, il peut les pré-calculer et contourner complètement l'exécuteur, chargeant rapidement les données dans la table à un niveau inférieur qui ignore entièrement une partie du travail normal de PostgreSQL. Si vous CREATE TABLE ou TRUNCATE dans la même transaction que vous COPY, il peut faire encore plus d'astuces pour accélérer le chargement en contournant la comptabilité normale des transactions nécessaire dans une base de données multi-clients.

Malgré cela, COPY de PostgreSQL pourrait encore faire beaucoup plus pour accélérer les choses, des choses qu'il ne sait pas encore faire. Il pourrait ignorer automatiquement les mises à jour d'index puis reconstruire les index si vous modifiez plus d'une certaine proportion de la table. Il pourrait effectuer des mises à jour d'index par lots. Beaucoup plus.

Coûts d'engagement

Une dernière chose à considérer est d'engager les coûts. Ce n'est probablement pas un problème pour vous car psycopg2 par défaut, l'ouverture d'une transaction et la non-validation jusqu'à ce que vous le lui disiez. Sauf si vous lui avez dit d'utiliser l'autocommit. Mais pour de nombreux pilotes de base de données, la validation automatique est la valeur par défaut. Dans de tels cas, vous feriez un commit pour chaque INSERT. Cela signifie une vidange de disque, où le serveur s'assure qu'il écrit toutes les données en mémoire sur le disque et indique aux disques d'écrire leurs propres caches dans un stockage persistant. Cela peut prendre un temps long et varie beaucoup en fonction du matériel. Mon ordinateur portable NVMe BTRFS basé sur SSD ne peut effectuer que 200 fsyncs/seconde, contre 300 000 écritures non synchronisées/seconde. Il ne chargera donc que 200 lignes/seconde! Certains serveurs ne peuvent faire que 50 fsyncs/seconde. Certains peuvent en faire 20 000. Donc, si vous devez valider régulièrement, essayez de charger et de valider par lots, de faire des insertions sur plusieurs lignes, etc. Parce que COPY ne fait qu'une seule validation à la fin, les coûts de validation sont négligeables. Mais cela signifie également que COPY ne peut pas récupérer des erreurs en cours de route dans les données; il annule toute la charge en vrac.

16
Craig Ringer

La copie utilise le chargement en bloc, ce qui signifie qu'elle insère plusieurs lignes à chaque fois, tandis que l'insertion simple effectue une insertion à la fois, mais vous pouvez insérer plusieurs lignes avec l'insertion en suivant la syntaxe:

insert into table_name (column1, .., columnn) values (val1, ..valn), ..., (val1, ..valn)

pour plus d'informations sur l'utilisation de la charge en bloc, reportez-vous par exemple à Le moyen le plus rapide pour charger 1m de lignes en postgresql par Daniel Westermann .

la question du nombre de lignes que vous devez insérer à la fois dépend de la longueur de la ligne, une bonne règle est d'insérer 100 lignes par instruction d'insertion.

4
rachid el kedmiri

Effectuez des INSERT dans une transaction pour accélérer.

Test en bash sans transaction:

>  time ( for((i=0;i<100000;i++)); do echo 'INSERT INTO testtable (value) VALUES ('$i');'; done ) | psql root | uniq -c
 100000 INSERT 0 1

real    0m15.257s
user    0m2.344s
sys     0m2.102s

Et avec transaction:

> time ( echo 'BEGIN;' && for((i=0;i<100000;i++)); do echo 'INSERT INTO testtable (value) VALUES ('$i');'; done && echo 'COMMIT;' ) | psql root | uniq -c
      1 BEGIN
 100000 INSERT 0 1
      1 COMMIT

real    0m7.933s
user    0m2.549s
sys     0m2.118s
2
OBi