web-dev-qa-db-fra.com

Optimisation des performances de mise à jour en masse dans PostgreSQL

Utilisation de PG 9.1 sur Ubuntu 12.04.

Il nous faut actuellement jusqu'à 24h pour exécuter un grand ensemble d'instructions UPDATE sur une base de données, qui sont de la forme:

UPDATE table
SET field1 = constant1, field2 = constant2, ...
WHERE id = constid

(Nous remplaçons simplement les champs des objets identifiés par ID.) Les valeurs proviennent d'une source de données externe (pas déjà dans la base de données dans une table).

Les tableaux ont chacun une poignée d'indices et aucune contrainte de clé étrangère. Aucun COMMIT n'est effectué jusqu'à la fin.

Il faut 2h pour importer un pg_dump de la base de données entière. Cela semble être une référence que nous devrions raisonnablement cibler.

À moins de produire un programme personnalisé qui reconstruit en quelque sorte un ensemble de données pour que PostgreSQL soit réimporté, y a-t-il quelque chose que nous puissions faire pour rapprocher les performances de mise à jour en masse de celles de l'importation? (C'est un domaine que nous pensons que les arbres de fusion à structure logarithmique gèrent bien, mais nous nous demandons s'il y a quelque chose que nous pouvons faire dans PostgreSQL.)

Quelques idées:

  • supprimer tous les indices non-ID et reconstruire ensuite?
  • augmentation de checkpoint_segments, mais cela aide-t-il réellement à un débit soutenu à long terme?
  • en utilisant les techniques mentionnées ici ? (Charger les nouvelles données sous forme de tableau, puis "fusionner" les anciennes données où l'ID ne se trouve pas dans les nouvelles données)

Fondamentalement, il y a beaucoup de choses à essayer et nous ne savons pas quels sont les plus efficaces ou si nous oublions d'autres choses. Nous allons passer les prochains jours à expérimenter, mais nous avons pensé que nous poserions également la question ici.

J'ai une charge simultanée sur la table, mais elle est en lecture seule.

40
Yang

Hypothèses

Comme il manque des informations dans le Q, je suppose:

  • Vos données proviennent d'un fichier sur le serveur de base de données.
  • Les données sont formatées exactement comme la sortie COPY, avec un uniqueid par ligne pour correspondre à la table cible.
    Sinon, formatez-le correctement d'abord ou utilisez les options COPY pour gérer le format.
  • Vous mettez à jour chaque ligne de la table cible ou la plupart d'entre elles.
  • Vous pouvez vous permettre de supprimer et de recréer la table cible.
    Cela signifie non accès simultané. Sinon, considérez cette réponse connexe:
  • Il n'y a aucun objet dépendant du tout, à l'exception des indices.

Solution

Je vous suggère de suivre une approche similaire à celle décrite dans le lien depuis votre troisième puce . Avec des optimisations majeures.

Pour créer la table temporaire, il existe un moyen plus simple et plus rapide:

CREATE TEMP TABLE tmp_tbl AS SELECT * FROM tbl LIMIT 0;

Un seul gros UPDATE d'une table temporaire à l'intérieur la base de données sera plus rapide que les mises à jour individuelles de l'extérieur de la base de données de plusieurs ordres de grandeur.

Dans modèle MVCC de PostgreSQL , un UPDATE signifie créer une nouvelle version de ligne et marquer l'ancienne comme supprimée. C'est à peu près aussi cher qu'un INSERT et un DELETE combinés. De plus, il vous laisse beaucoup de tuples morts. Comme vous mettez à jour la table entière de toute façon, il serait globalement plus rapide de simplement créer une nouvelle table et de supprimer l'ancienne.

Si vous en avez assez RAM disponible, définissez temp_buffers (uniquement pour cette session!) suffisamment haut pour contenir la table temporaire dans RAM - avant de faire quoi que ce soit d'autre.

Pour obtenir une estimation de la quantité RAM est nécessaire, exécutez un test avec un petit échantillon et utilisez fonctions de taille d'objet db :

SELECT pg_size_pretty(pg_relation_size('tmp_tbl'));  -- complete size of table
SELECT pg_column_size(t) FROM tmp_tbl t LIMIT 10;  -- size of sample rows

Script complet

SET temp_buffers = '1GB';        -- example value

CREATE TEMP TABLE tmp_tbl AS SELECT * FROM tbl LIMIT 0;

COPY tmp_tbl FROM '/absolute/path/to/file';

CREATE TABLE tbl_new AS
SELECT t.col1, t.col2, u.field1, u.field2
FROM   tbl     t
JOIN   tmp_tbl u USING (id);

-- Create indexes like in original table
ALTER TABLE tbl_new ADD PRIMARY KEY ...;
CREATE INDEX ... ON tbl_new (...);
CREATE INDEX ... ON tbl_new (...);

-- exclusive lock on tbl for a very brief time window!
DROP TABLE tbl;
ALTER TABLE tbl_new RENAME TO tbl;

DROP TABLE tmp_tbl; -- will also be dropped at end of session automatically

Charge simultanée

Les opérations simultanées sur la table (que j'ai exclues dans les hypothèses au début) vont attendre, une fois que la table est verrouillée vers la fin et échouer dès que la transaction est validée, car le nom de la table est résolu à son OID immédiatement, mais la nouvelle table a un OID différent. La table reste cohérente, mais les opérations simultanées peuvent obtenir une exception et doivent être répétées. Détails dans cette réponse connexe:

MISE À JOUR de l'itinéraire

Si vous (devez) suivre la route UPDATE, supprimez tout index qui n'est pas nécessaire pendant la mise à jour et recréez-le ensuite. Il est beaucoup moins cher de créer un index en une seule pièce que de le mettre à jour pour chaque ligne individuelle. Cela peut également permettre mises à jour CHAUDES .

J'ai décrit une procédure similaire en utilisant UPDATE dans cette réponse étroitement liée à SO .

48
Erwin Brandstetter

Si les données peuvent être mises à disposition dans un fichier structuré, vous pouvez les lire avec un wrapper de données étranger et effectuer une fusion sur la table cible.

2
David Aldridge