Pourquoi Postgres UPDATE a pris 39 heures?

17

J'ai une table Postgres avec environ 2,1 millions de lignes. J'ai exécuté la mise à jour ci-dessous:

WITH stops AS (
    SELECT id,
           rank() OVER (ORDER BY offense_timestamp,
                     defendant_dl,
                     offense_street_number,
                     offense_street_name) AS stop
    FROM   consistent.master
    WHERE  citing_jurisdiction=1
)

UPDATE consistent.master
SET arrest_id=stops.stop
FROM stops
WHERE master.id = stops.id;

Cette requête a pris 39 heures pour s'exécuter. J'exécute cela sur un processeur pour ordinateur portable i7 Q720 à 4 cœurs (physique), beaucoup de RAM, rien d'autre ne fonctionne la grande majorité du temps. Pas de contraintes d'espace disque dur. Le tableau avait récemment été aspiré, analysé et réindexé.

Pendant toute la durée de l'exécution de la requête, au moins après la fin de l'initialisation WITH, l'utilisation du processeur était généralement faible et le disque dur était utilisé à 100%. Le disque dur était utilisé si durement que toute autre application fonctionnait beaucoup plus lentement que la normale.

Le paramètre d'alimentation de l'ordinateur portable était sur Haute performance (Windows 7 x64).

Voici l'EXPLIQUER:

Update on master  (cost=822243.22..1021456.89 rows=2060910 width=312)
  CTE stops
    ->  WindowAgg  (cost=529826.95..581349.70 rows=2060910 width=33)
          ->  Sort  (cost=529826.95..534979.23 rows=2060910 width=33)
                Sort Key: consistent.master.offense_timestamp, consistent.master.defendant_dl, consistent.master.offense_street_number, consistent.master.offense_street_name
                ->  Seq Scan on master  (cost=0.00..144630.06 rows=2060910 width=33)
                      Filter: (citing_jurisdiction = 1)
  ->  Hash Join  (cost=240893.51..440107.19 rows=2060910 width=312)
        Hash Cond: (stops.id = consistent.master.id)
        ->  CTE Scan on stops  (cost=0.00..41218.20 rows=2060910 width=48)
        ->  Hash  (cost=139413.45..139413.45 rows=2086645 width=268)
              ->  Seq Scan on master  (cost=0.00..139413.45 rows=2086645 width=268)

citing_jurisdiction=1exclut seulement quelques dizaines de milliers de lignes. Même avec cette WHEREclause, j'opère toujours sur plus de 2 millions de lignes.

Le disque dur est entièrement chiffré avec TrueCrypt 7.1a. Que les choses de Ralentit un peu, mais pas assez pour causer une requête à prendre que de nombreuses heures.

La WITHpartie ne prend que 3 minutes environ pour fonctionner.

Le arrest_idchamp n'avait pas d'index pour la clé étrangère. Il y a 8 index et 2 clés étrangères sur cette table. Tous les autres champs de la requête sont indexés.

Le arrest_idchamp n'avait pas de contraintes sauf NOT NULL.

Le tableau comprend 32 colonnes au total.

arrest_idest de type variable (20) . Je me rends compte rank()produit une valeur numérique, mais je dois utiliser la variation de caractère (20) parce que j'ai d'autres lignes où citing_jurisdiction<>1qui utilisent des données non numériques pour ce champ.

Le arrest_idchamp était vide pour toutes les lignes avec citing_jurisdiction=1.

Il s'agit d'un ordinateur portable personnel haut de gamme (il y a 1 an). Je suis le seul utilisateur. Aucune autre requête ou opération n'était en cours d'exécution. Le verrouillage semble peu probable.

Il n'y a aucun déclencheur nulle part dans cette table ou ailleurs dans la base de données.

Les autres opérations sur cette base de données ne prennent jamais un temps abornmal. Avec une indexation appropriée, les SELECTrequêtes sont généralement assez rapides.

Aren Cambre
la source
C'est Seq Scanun peu effrayant ...
rogerdpack

Réponses:

18

J'ai eu quelque chose de similaire récemment avec un tableau de 3,5 millions de lignes. Ma mise à jour ne se terminerait jamais. Après beaucoup d'expériences et de frustration, j'ai finalement trouvé le coupable. Il s'est avéré qu'il s'agissait des index de la table en cours de mise à jour.

La solution consistait à supprimer tous les index de la table en cours de mise à jour avant d'exécuter l'instruction de mise à jour. Une fois que j'ai fait cela, la mise à jour s'est terminée en quelques minutes. Une fois la mise à jour terminée, j'ai recréé les index et j'ai repris mes activités. Cela ne vous aidera probablement pas à ce stade, mais il se peut que quelqu'un d'autre cherche des réponses.

Je garderais les index sur la table dont vous tirez les données. Celui-ci n'aura pas à mettre à jour les index et devrait vous aider à trouver les données que vous souhaitez mettre à jour. Il fonctionnait bien sur un ordinateur portable lent.

JC Avena
la source
3
Je vous propose la meilleure réponse. Depuis que j'ai posté cela, j'ai rencontré d'autres situations où les index sont le problème, même si la colonne en cours de mise à jour a déjà une valeur et n'a pas d'index (!). Il semble que Postgres ait un problème avec la façon dont il gère les index sur d'autres colonnes. Il n'y a aucune raison pour que ces autres index gonflent le temps de requête d'une mise à jour lorsque la seule modification d'une table consiste à mettre à jour une colonne non indexée et que vous n'augmentez pas l'espace alloué pour une ligne de cette colonne.
Aren Cambre
1
Merci! J'espère que cela aide les autres. Cela m'aurait épargné des heures de maux de tête pour quelque chose qui semblait très simple.
JC Avena
5
@ArenCambre - il y a une raison: PostgreSQL copie la ligne entière vers un emplacement différent et marque l'ancienne version comme supprimée. C'est ainsi que PostgreSQL implémente le contrôle de concurrence multi-version (MVCC).
Piotr Findeisen
Ma question est ... pourquoi est-ce le coupable? Voir aussi stackoverflow.com/a/35660593/32453
rogerdpack
15

Votre plus gros problème est de faire d'énormes quantités de travail en écriture et en recherche sur un disque dur d'ordinateur portable. Cela ne sera jamais rapide, peu importe ce que vous faites, surtout si c'est le type de lecteur 5400RPM plus lent livré avec de nombreux ordinateurs portables.

TrueCrypt ralentit les choses plus que "un peu" pour les écritures. Les lectures seront raisonnablement rapides, mais les écritures rendent RAID 5 plus rapide. L'exécution d'une base de données sur un volume TrueCrypt sera une torture pour les écritures, en particulier les écritures aléatoires.

Dans ce cas, je pense que vous perdriez votre temps à essayer d'optimiser la requête. Vous réécrivez la plupart des lignes de toute façon, et ça va être lent avec votre horrible situation d'écriture. Ce que je recommanderais, c'est de:

BEGIN;
SELECT ... INTO TEMPORARY TABLE master_tmp ;
TRUNCATE TABLE consistent.master;
-- Now DROP all constraints on consistent.master, then:
INSERT INTO consistent.master SELECT * FROM master_tmp;
-- ... and re-create any constraints.

Je soupçonne que ce sera plus rapide que de simplement supprimer et recréer les contraintes, car une MISE À JOUR aura des modèles d'écriture assez aléatoires qui tueront votre stockage. Deux insertions en masse, l'une dans une table non enregistrée et l'autre dans une table enregistrée WAL sans contraintes, seront probablement plus rapides.

Si vous avez des sauvegardes absolument à jour et que cela ne vous dérange pas de devoir restaurer votre base de données à partir de sauvegardes, vous pouvez également redémarrer PostgreSQL avec le fsync=offparamètre et full_page_writes=off temporairement pour cette opération en bloc. Tout problème inattendu comme une panne de courant ou un crash du système d'exploitation laissera votre base de données irrécupérable pendant fsync=off.

L'équivalent POSTGreSQL de «pas de journalisation» consiste à utiliser des tables non enregistrées. Ces tables non enregistrées sont tronquées si la base de données s'arrête de manière incorrecte alors qu'elles sont sales. L'utilisation de tables non enregistrées réduira au moins de moitié votre charge d'écriture et réduira le nombre de recherches, afin qu'elles puissent être BEAUCOUP plus rapidement.

Comme dans Oracle, il peut être judicieux de supprimer un index puis de le recréer après une mise à jour par lots importante. Le planificateur de PostgreSQL ne peut pas déterminer qu'une grosse mise à jour a lieu, suspendre les mises à jour d'index, puis reconstruire l'index à la fin; même si c'était le cas, il lui serait très difficile de déterminer à quel moment cela valait la peine, surtout à l'avance.

Craig Ringer
la source
Cette réponse est exacte sur la grande quantité d'écritures et la terrible performance du cryptage et du lecteur d'ordinateur portable lent. Je note également que la présence de 8 indices produit de nombreuses écritures et défaites d' application extra HOT en bloc des mises à jour de ligne, afin de supprimer des index et en utilisant un faible facteur de remplissage sur la table peut empêcher une tonne de migration de ligne
dbenhur
1
Bon appel pour augmenter les chances de HOT avec un facteur de remplissage - bien qu'avec TrueCrypt forçant les cycles de lecture-réécriture de blocs dans des blocs énormes, je ne suis pas sûr que cela aidera beaucoup; la migration des lignes peut même être plus rapide car la croissance de la table fait au moins des blocs d'écritures linéaires.
Craig Ringer
2,5 ans plus tard, je fais quelque chose de similaire mais sur une table plus grande. Juste pour être sûr, est-ce une bonne idée de supprimer tous les index, même si la seule colonne que je mets à jour n'est pas indexée?
Aren Cambre
1
@ArenCambre Dans ce cas ... eh bien, c'est compliqué. Si la plupart de vos mises à jour sont éligibles, HOTil est préférable de laisser les index en place. Sinon, vous voudrez probablement supprimer et recréer. La colonne n'est pas indexée, mais pour pouvoir effectuer une mise à jour HOT, il doit également y avoir de l'espace libre sur la même page, cela dépend donc un peu de la quantité d'espace mort dans la table. Si c'est surtout en écriture, je dirais de supprimer tous les index. S'il s'agit de lots mis à jour, il pourrait y avoir des trous et vous pourriez être OK. Des outils comme pageinspectet pg_freespacemappeuvent aider à déterminer cela.
Craig Ringer
Merci. Dans ce cas, il s'agit d'une colonne booléenne qui avait déjà une entrée dans chaque ligne. Je modifiais l'entrée sur certaines lignes. Je viens de confirmer: la mise à jour n'a pris que 2 heures après la suppression de tous les index. Auparavant, j'ai dû arrêter la mise à jour après 18 heures car cela prenait trop de temps. Ceci malgré le fait que la colonne qui était en cours de mise à jour n'était définitivement pas indexée.
Aren Cambre
2

Quelqu'un donnera une meilleure réponse pour Postgres, mais voici quelques observations d'un point de vue Oracle qui peuvent s'appliquer (et les commentaires sont trop longs pour le champ de commentaire).

Ma première préoccupation serait d'essayer de mettre à jour 2 millions de lignes en une seule transaction. Dans Oracle, vous écririez une image avant chaque bloc mis à jour afin que les autres sessions aient toujours une lecture cohérente sans lire vos blocs modifiés et vous avez la possibilité de revenir en arrière. C'est un long retour en arrière qui se construit. Il est généralement préférable de faire les transactions en petits morceaux. Dites 1 000 enregistrements à la fois.

Si vous avez des index sur la table et que la table sera considérée comme hors service pendant la maintenance, il est souvent préférable de supprimer les index avant une grosse opération, puis de la recréer à nouveau par la suite. Moins cher, puis essayant constamment de maintenir les index avec chaque enregistrement mis à jour.

Oracle autorise les astuces "no logging" sur les instructions pour arrêter la journalisation. Il accélère beaucoup les déclarations, mais laisse votre base de données dans une situation "irrécupérable". Vous souhaitez donc effectuer une sauvegarde avant, puis une nouvelle sauvegarde immédiatement après. Je ne sais pas si Postgres a des options similaires.

Glenn
la source
PostgreSQL n'a pas de problèmes avec un long rollback, n'existe pas. ROLBACK est très rapide dans PostgreSQL, quelle que soit la taille de votre transaction. Oracle! = PostgreSQL
Frank Heikens
@FrankHeikens Merci, c'est intéressant. Je vais devoir lire comment fonctionne le journalisation sur Postgres. Afin de faire fonctionner tout le concept de transactions, deux versions différentes des données doivent être conservées pendant une transaction, l'image avant et l'image après et c'est le mécanisme auquel je fais référence. D'une manière ou d'une autre, je suppose qu'il existe un seuil au-delà duquel les ressources nécessaires pour maintenir la transaction seront trop chères.
Glenn
2
@Glenn postgres conserve les versions d'une ligne dans le tableau lui-même - voir ici pour une explication. Le compromis est que vous obtenez des tuples `` morts '' qui sont nettoyés de manière asynchrone avec ce qui est appelé `` vide '' dans postgres (Oracle n'a pas besoin de vide car il n'a jamais de lignes `` mortes '' dans le tableau lui-même)
Jack dit essayez topanswers.xyz
Vous êtes les bienvenus, et plutôt tardivement: bienvenue sur le site :-)
Jack dit d'essayer topanswers.xyz
@Glenn Le document canonique pour le contrôle d'accès concurrentiel à la version en ligne de PostgreSQL est postgresql.org/docs/current/static/mvcc-intro.html et il vaut bien la peine d'être lu. Voir aussi wiki.postgresql.org/wiki/MVCC . Notez que MVCC avec des lignes mortes et VACUUMn'est que la moitié de la réponse; PostgreSQL utilise également un soi-disant "journal d'avance" (en fait un journal) pour fournir des validations atomiques et se protéger contre les écritures partielles, etc. Voir postgresql.org/docs/current/static/wal-intro.html
Craig Ringer