Comment accélérer les performances d'insertion dans PostgreSQL

215

Je teste les performances d'insertion de Postgres. J'ai une table avec une colonne avec un nombre comme type de données. Il y a également un index. J'ai rempli la base de données en utilisant cette requête:

insert into aNumber (id) values (564),(43536),(34560) ...

J'ai inséré 4 millions de lignes très rapidement 10 000 à la fois avec la requête ci-dessus. Une fois que la base de données a atteint 6 millions de lignes, les performances ont considérablement chuté à 1 million de lignes toutes les 15 minutes. Existe-t-il une astuce pour augmenter les performances d'insertion? J'ai besoin de performances d'insertion optimales sur ce projet.

Utilisation de Windows 7 Pro sur une machine avec 5 Go de RAM.

Luke101
la source
5
Il convient également de mentionner votre version Pg dans les questions. Dans ce cas, cela ne fait pas beaucoup de différence, mais cela le fait pour beaucoup de questions.
Craig Ringer
1
supprimez les index sur la table et déclenche le cas échéant et exécutez le script d'insertion. Une fois le chargement en masse terminé, vous pouvez recréer les index.
Sandeep

Réponses:

481

Voir remplir une base de données dans le manuel PostgreSQL, l' article excellent comme d'habitude de depesz sur le sujet, et cette question SO .

(Notez que cette réponse concerne le chargement en masse de données dans une base de données existante ou pour en créer une nouvelle. Si vous êtes intéressé par les performances de restauration de base de données pg_restoreou l' psqlexécution de la pg_dumpsortie, une grande partie de cela ne s'applique pas depuis pg_dumpet fait pg_restoredéjà des choses comme la création déclenche et indexe une fois le schéma + la restauration des données terminé) .

Il y a beaucoup à faire. La solution idéale serait d'importer dans une UNLOGGEDtable sans index, puis de la remplacer par journalisée et d'ajouter les index. Malheureusement, dans PostgreSQL 9.4, il n'y a pas de prise en charge pour changer les tables de UNLOGGEDen journalisé. 9.5 ajoute ALTER TABLE ... SET LOGGEDpour vous permettre de le faire.

Si vous pouvez mettre votre base de données hors ligne pour l'importation en masse, utilisez pg_bulkload.

Autrement:

  • Désactivez tous les déclencheurs sur la table

  • Supprimez les index avant de démarrer l'importation, puis recréez-les par la suite. (Il faut beaucoup moins de temps pour construire un index en un seul passage que pour lui ajouter progressivement les mêmes données, et l'index résultant est beaucoup plus compact).

  • Si vous effectuez l'importation dans une seule transaction, il est sûr de supprimer les contraintes de clé étrangère, d'effectuer l'importation et de recréer les contraintes avant de valider. Ne le faites pas si l'importation est répartie sur plusieurs transactions, car vous pourriez introduire des données non valides.

  • Si possible, utilisez COPYau lieu de INSERTs

  • Si vous ne pouvez pas utiliser, COPYenvisagez d'utiliser des valeurs multiples INSERTsi possible. Vous semblez déjà le faire. N'essayez pas de lister trop de valeurs en une seule VALUEScependant; ces valeurs doivent rester en mémoire plusieurs fois, alors restez à quelques centaines par instruction.

  • Regroupez vos encarts dans des transactions explicites, en faisant des centaines de milliers ou des millions d'inserts par transaction. Il n'y a pas de limite pratique AFAIK, mais le traitement par lots vous permettra de récupérer d'une erreur en marquant le début de chaque lot dans vos données d'entrée. Encore une fois, vous semblez déjà le faire.

  • Utilisez synchronous_commit=offet un énorme commit_delaypour réduire les coûts de fsync (). Cependant, cela n'aidera pas beaucoup si vous avez regroupé votre travail en grosses transactions.

  • INSERTou COPYen parallèle à partir de plusieurs connexions. Le nombre dépend du sous-système de disque de votre matériel; en règle générale, vous voulez une connexion par disque dur physique si vous utilisez un stockage directement connecté.

  • Définissez une checkpoint_segmentsvaleur élevée et activez log_checkpoints. Regardez les journaux de PostgreSQL et assurez-vous qu'il ne se plaint pas que les points de contrôle se produisent trop fréquemment.

  • Si et seulement si cela ne vous dérange pas de perdre la totalité de votre cluster PostgreSQL (votre base de données et toutes les autres sur le même cluster) à une corruption catastrophique si le système se bloque pendant l'importation, vous pouvez arrêter Pg, définir fsync=off, démarrer Pg, faire votre importation, puis (vitalement) arrêtez Pg et reprenez fsync=on. Voir configuration WAL . Ne faites pas cela s'il y a déjà des données dont vous vous souciez dans une base de données sur votre installation PostgreSQL. Si vous définissez, fsync=offvous pouvez également définir full_page_writes=off; encore une fois, n'oubliez pas de le réactiver après votre importation pour éviter la corruption de la base de données et la perte de données. Voir les paramètres non durables dans le manuel Pg.

Vous devriez également envisager de régler votre système:

  • Utilisez autant que possible des SSD de bonne qualité pour le stockage. De bons disques SSD avec des caches de réécriture fiables et protégés par l'alimentation accélèrent considérablement les taux de validation. Ils sont moins bénéfiques lorsque vous suivez les conseils ci-dessus - ce qui réduit les vidages de disque / le nombre de fsync()s - mais peuvent toujours être d'une grande aide. N'utilisez pas de SSD bon marché sans protection appropriée contre les pannes de courant, sauf si vous ne vous souciez pas de conserver vos données.

  • Si vous utilisez RAID 5 ou RAID 6 pour le stockage directement connecté, arrêtez maintenant. Sauvegardez vos données, restructurez votre matrice RAID en RAID 10 et réessayez. RAID 5/6 est sans espoir pour les performances d'écriture en masse - bien qu'un bon contrôleur RAID avec un grand cache puisse aider.

  • Si vous avez la possibilité d'utiliser un contrôleur RAID matériel avec un grand cache d'écriture différée alimenté par batterie, cela peut vraiment améliorer les performances d'écriture pour les charges de travail avec beaucoup de validations. Cela n'aide pas beaucoup si vous utilisez une validation asynchrone avec un commit_delay ou si vous effectuez moins de grosses transactions pendant le chargement en bloc.

  • Si possible, stockez WAL ( pg_xlog) sur un disque / baie de disques distinct. Il est inutile d'utiliser un système de fichiers distinct sur le même disque. Les gens choisissent souvent d'utiliser une paire RAID1 pour WAL. Encore une fois, cela a plus d'effet sur les systèmes avec des taux de validation élevés, et cela a peu d'effet si vous utilisez une table non enregistrée comme cible de chargement des données.

Vous pouvez également être intéressé par Optimiser PostgreSQL pour des tests rapides .

Craig Ringer
la source
1
Seriez-vous d'accord pour dire que la pénalité d'écriture de RAID 5/6 est quelque peu atténuée si des SSD de bonne qualité sont utilisés? Évidemment, il y a toujours une pénalité, mais je pense que la différence est beaucoup moins douloureuse qu'avec les disques durs.
1
Je n'ai pas testé ça. Je dirais que c'est probablement moins mauvais - les effets d'amplification d'écriture désagréables et (pour les petites écritures) le besoin d'un cycle de lecture-modification-écriture existent toujours, mais la pénalité sévère pour une recherche excessive ne devrait pas être un problème.
Craig Ringer
Pouvons-nous simplement désactiver les index au lieu de les supprimer, par exemple, en définissant indisvalid( postgresql.org/docs/8.3/static/catalog-pg-index.html ) sur false, puis en chargeant les données et en mettant les index en ligne par REINDEX?
Vladislav Rastrusny
1
@CraigRinger J'ai testé RAID-5 vs RAID-10 avec des SSD sur un Perc H730. RAID-5 est en fait plus rapide. Il convient également de noter que l'insertion / les transactions en combinaison avec de grands bytea semblent être plus rapides que la copie. Globalement, de bons conseils.
atlaste
2
Quelqu'un voit-il des améliorations de vitesse majeures avec UNLOGGED? Un test rapide montre une amélioration de 10 à 20%.
serg
15

Une utilisation COPY table TO ... WITH BINARYconforme à la documentation est " un peu plus rapide que les formats texte et CSV ". Ne faites cela que si vous avez des millions de lignes à insérer et si vous êtes à l'aise avec les données binaires.

Voici un exemple de recette en Python, utilisant psycopg2 avec une entrée binaire .

Mike T
la source
1
Le mode binaire peut être un gain de temps important sur certaines entrées, telles que les horodatages, où les analyser n'est pas trivial. Pour de nombreux types de données, il n'offre pas beaucoup d'avantages ou peut même être légèrement plus lent en raison de l'augmentation de la bande passante (par exemple, les petits entiers). Bon point de le soulever.
Craig Ringer
11

En plus de l'excellent article de Craig Ringer et du blog de Depesz, si vous souhaitez accélérer vos insertions via l' interface ODBC ( psqlodbc ) en utilisant des insertions d'instructions préparées dans une transaction, il y a quelques choses supplémentaires que vous devez faire pour y parvenir travailler vite:

  1. Définissez le niveau de restauration des erreurs sur "Transaction" en spécifiant Protocol=-1dans la chaîne de connexion. Par défaut, psqlodbc utilise le niveau "Statement", qui crée un SAVEPOINT pour chaque instruction plutôt qu'une transaction entière, ce qui ralentit les insertions.
  2. Utilisez des instructions préparées côté serveur en spécifiant UseServerSidePrepare=1dans la chaîne de connexion. Sans cette option, le client envoie l'intégralité de l'instruction d'insertion avec chaque ligne insérée.
  3. Désactivez la validation automatique sur chaque instruction à l'aide de SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, reinterpret_cast<SQLPOINTER>(SQL_AUTOCOMMIT_OFF), 0);
  4. Une fois toutes les lignes insérées, validez la transaction à l'aide de SQLEndTran(SQL_HANDLE_DBC, conn, SQL_COMMIT);. Il n'est pas nécessaire d'ouvrir explicitement une transaction.

Malheureusement, psqlodbc "implémente" SQLBulkOperationsen émettant une série d'instructions d'insertion non préparées, de sorte que pour obtenir l'insertion la plus rapide, il faut coder manuellement les étapes ci-dessus.

Maxim Egorushkin
la source
Grande taille de tampon de socket, A8=30000000dans la chaîne de connexion doit également être utilisée pour accélérer les insertions.
Andrus
9

J'ai passé environ 6 heures sur le même problème aujourd'hui. Les insertions vont à une vitesse `` régulière '' (moins de 3 secondes par 100 K) jusqu'à 5MI (sur un total de 30MI) lignes, puis les performances diminuent considérablement (jusqu'à 1 minute par 100K).

Je n'énumérerai pas toutes les choses qui n'ont pas fonctionné et coupées directement à la viande.

J'ai laissé tomber une clé primaire sur la table cible (qui était un GUID) et mes 30MI ou lignes ont heureusement coulé vers leur destination à une vitesse constante de moins de 3sec pour 100K.

Dennis
la source
6

Si vous êtes heureux d'insérer des colonnes avec des UUID (ce qui n'est pas exactement votre cas) et d'ajouter à la réponse @Dennis (je ne peux pas encore commenter), sachez que l'utilisation de gen_random_uuid () (nécessite PG 9.4 et le module pgcrypto) est (un beaucoup) plus vite que uuid_generate_v4 ()

=# explain analyze select uuid_generate_v4(),* from generate_series(1,10000);
                                                        QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=11.674..10304.959 rows=10000 loops=1)
 Planning time: 0.157 ms
 Execution time: 13353.098 ms
(3 filas)

contre


=# explain analyze select gen_random_uuid(),* from generate_series(1,10000);
                                                        QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
 Function Scan on generate_series  (cost=0.00..12.50 rows=1000 width=4) (actual time=252.274..418.137 rows=10000 loops=1)
 Planning time: 0.064 ms
 Execution time: 503.818 ms
(3 filas)

C'est aussi la manière officielle suggérée de le faire

Remarque

Si vous n'avez besoin que d'UUID générés aléatoirement (version 4), envisagez plutôt d'utiliser la fonction gen_random_uuid () du module pgcrypto.

Ce temps d'insertion a chuté de ~ 2 heures à ~ 10 minutes pour 3,7 millions de lignes.

Francisco Reynoso
la source
1

Pour des performances d'insertion optimales, désactivez l'index si c'est une option pour vous. En dehors de cela, un meilleur matériel (disque, mémoire) est également utile

Icare
la source
-1

J'ai également rencontré ce problème de performances d'insertion. Ma solution consiste à générer des routines de fin pour terminer le travail d'insertion. Dans l'intervalle, SetMaxOpenConnsil convient de donner un numéro correct, sinon trop d'erreurs de connexion ouvertes seraient alertées.

db, _ := sql.open() 
db.SetMaxOpenConns(SOME CONFIG INTEGER NUMBER) 
var wg sync.WaitGroup
for _, query := range queries {
    wg.Add(1)
    go func(msg string) {
        defer wg.Done()
        _, err := db.Exec(msg)
        if err != nil {
            fmt.Println(err)
        }
    }(query)
}
wg.Wait()

La vitesse de chargement est beaucoup plus rapide pour mon projet. Cet extrait de code donne juste une idée de son fonctionnement. Les lecteurs devraient pouvoir le modifier facilement.

Patrick
la source
Tu peux dire ça. Mais cela réduit le temps de fonctionnement de quelques heures à plusieurs minutes pour des millions de lignes pour mon cas. :)
Patrick