←Tous les articles
Mar. 11 août 2026·8 min de lecture

🐘 pgloader : migrer une base MySQL vers PostgreSQL sans y passer le week-end

Logos MySQL et PostgreSQL

📚 Introduction

La migration MySQL → PostgreSQL revient rĂ©guliĂšrement : une application vieillissante, un besoin de types plus riches, un hĂ©bergeur qui pousse Postgres, ou simplement l’envie d’arrĂȘter de contourner les bizarreries de MySQL. Et Ă  chaque fois, la mĂȘme tentation : mysqldump, quelques sed bien sentis, et on verra bien.

On ne verra rien de bon. Le dialecte SQL de mysqldump n’est pas celui de PostgreSQL : les backticks, AUTO_INCREMENT, ENGINE=InnoDB, tinyint(1) en guise de boolĂ©en, les dates 0000-00-00, les ENUM dĂ©clarĂ©s inline
 Chacun de ces points demande une transformation, et il y en a une bonne trentaine. Trois jours de bricolage plus tard, on a une base Ă  moitiĂ© convertie et zĂ©ro confiance dedans.

pgloader rĂšgle le problĂšme d’un coup. Il se connecte Ă  MySQL, lit le catalogue, en dĂ©duit un schĂ©ma PostgreSQL Ă©quivalent, streame les donnĂ©es via COPY, puis reconstruit index et clĂ©s Ă©trangĂšres en parallĂšle. Le projet est distribuĂ© sous la licence PostgreSQL et existe depuis assez longtemps pour avoir croisĂ© toutes les horreurs que votre base contient.

🔎 Qu’est-ce que pgloader, et pourquoi pas un simple dump ?

pgloader est un chargeur de donnĂ©es pour PostgreSQL bĂąti autour de COPY. Son avantage sur un \copy brut, c’est son comportement transactionnel : lĂ  oĂč PostgreSQL annule tout le chargement d’une table dĂšs la premiĂšre ligne invalide, pgloader met les lignes fautives de cĂŽtĂ© dans une paire de fichiers reject.dat / reject.log et continue Ă  charger les bonnes.

Sur une migration de base, cette diffĂ©rence est dĂ©cisive. Une table de 12 millions de lignes avec trois dates pourries ne fait pas Ă©chouer la migration : elle produit un rapport de trois lignes Ă  traiter Ă  la main. Le reste du travail — conversion des types, crĂ©ation des index, rĂ©initialisation des sĂ©quences — est fait par l’outil, pas par vous.

🚀 Installer et lancer une premiùre migration

Deux lignĂ©es coexistent aujourd’hui. La v3, Ă©crite en Common Lisp, est la derniĂšre branche stable (3.6.10) et celle que reprennent les gestionnaires de paquets. Sur Debian et Ubuntu :

apt-get install pgloader

Sur macOS, la formule Homebrew installe la mĂȘme lignĂ©e :

brew install pgloader

La v4 est une réécriture complĂšte en Clojure, distribuĂ©e sous forme d’un JAR auto-contenu qui ne demande qu’un Java 21 ou supĂ©rieur. Elle se veut un remplaçant direct de la v3 : mĂȘme syntaxe de fichier .load, mĂȘmes options en ligne de commande.

curl -L -o pgloader.jar \
  https://github.com/dimitri/pgloader/releases/download/v4-dev/pgloader.jar
java -jar pgloader.jar --version

Si vous prĂ©fĂ©rez ne rien installer, l’image Docker est construite par la CI du projet :

docker pull ghcr.io/dimitri/pgloader:latest
docker run --rm -it ghcr.io/dimitri/pgloader:latest pgloader --version

Et voilĂ  la migration complĂšte, dans sa forme la plus courte :

createdb pagila
pgloader mysql://root@localhost/sakila postgresql:///pagila

C’est tout. Tables, types, index, clĂ©s Ă©trangĂšres, commentaires et donnĂ©es : pgloader fait le tour. Avant de lancer pour de vrai, --dry-run vĂ©rifie les deux connexions sans rien charger — ça Ă©vite de dĂ©couvrir un mot de passe erronĂ© aprĂšs vingt minutes de COPY.

đŸ§± Le fichier .load, lĂ  oĂč tout se joue

La ligne de commande suffit pour un essai. DÚs que la base a une histoire, on passe à un fichier de commandes versionné dans le dépÎt :

LOAD DATABASE
     FROM      mysql://root:pass@db:3306/sakila?useSSL=false
     INTO postgresql://pguser@pghost/pagila
 
 WITH include drop, create tables, create indexes, reset sequences,
      workers = 8, concurrency = 1,
      multiple readers per thread, rows per range = 50000
 
  SET PostgreSQL PARAMETERS
      maintenance_work_mem to '128MB',
      work_mem to '12MB'
 
  SET MySQL PARAMETERS
      net_read_timeout  = '120',
      net_write_timeout = '120'
 
 CAST type tinyint when (= precision 1) to boolean drop typemod,
      type varchar to varchar keep typemod,
      type year to integer
 
 EXCLUDING TABLE NAMES MATCHING ~/^tmp_/, 'sessions'
 
 BEFORE LOAD DO
   $$ create schema if not exists pagila; $$;

Quelques repÚres sur la clause WITH, dont le défaut pour MySQL est no truncate, create tables, include drop, create indexes, reset sequences, foreign keys, downcase identifiers, uniquify index names :

  • include drop supprime cĂŽtĂ© PostgreSQL les tables portant le mĂȘme nom que celles de MySQL, en CASCADE. C’est ce qui rend la commande rejouable en boucle pendant la mise au point — et c’est aussi le meilleur moyen de vider une base par erreur.
  • reset sequences recale chaque sĂ©quence sur le max() de sa colonne aprĂšs le chargement. Sans ça, votre premier INSERT applicatif tombe sur un doublon de clĂ© primaire.
  • workers et concurrency pilotent le parallĂ©lisme ; rows per range dĂ©coupe les grosses tables en tranches lues en parallĂšle.

CÎté v4, les URI acceptent aussi la forme JDBC (jdbc:mysql://
) et les paramÚtres du driver Connector/J passent tels quels : useSSL=false et allowPublicKeyRetrieval=true rÚglent 90 % des refus de connexion sur MySQL 8 en local.

🔍 Les rùgles de cast, le vrai sujet

C’est lĂ  que pgloader gagne ses trois jours. Un jeu de rĂšgles par dĂ©faut couvre dĂ©jĂ  l’essentiel des piĂšges MySQL :

  • tinyint(1) devient boolean (fonction tinyint-to-boolean), et bit(1) aussi ;
  • les entiers unsigned sont promus au type supĂ©rieur — int unsigned devient bigint, tinyint unsigned devient smallint ;
  • les colonnes auto_increment deviennent serial ou bigserial selon la prĂ©cision ;
  • datetime et timestamp deviennent timestamptz, et les valeurs 0000-00-00 00:00:00 sont converties en NULL par zero-dates-to-null — PostgreSQL n’a pas d’annĂ©e zĂ©ro ;
  • year devient integer, les types blob deviennent bytea, et les colonnes texte passent par remove-null-characters (le fameux \0 que Postgres refuse dans un text) ;
  • chaque colonne ENUM produit un CREATE TYPE dĂ©diĂ©, nommĂ© d’aprĂšs la table et la colonne, avec les mĂȘmes labels dans le mĂȘme ordre.

Le point Ă  surveiller : beaucoup de rĂšgles par dĂ©faut appliquent drop typemod, donc un varchar(40) MySQL arrive en varchar sans longueur. Si vous tenez Ă  vos contraintes de longueur, il faut l’écrire :

CAST type varchar to varchar keep typemod,
     type char    to char    keep typemod

Les gardes permettent d’affiner : when unsigned, when (= precision 1), when default '0', with extra auto_increment. Et quand une seule table utilise tinyint comme boolĂ©en alors que les autres s’en servent comme entier, on cible la colonne plutĂŽt que le type : CAST column commande.paye to boolean.

🎯 Filtrer, renommer, rĂ©organiser

Une migration est rarement un copier-coller. pgloader fait la rĂ©organisation au passage, sur sa reprĂ©sentation en mĂ©moire du schĂ©ma — aucun ALTER TABLE n’est envoyĂ© Ă  PostgreSQL :

 INCLUDING ONLY TABLE NAMES MATCHING ~/^facture/, 'client'
 EXCLUDING TABLE NAMES MATCHING ~/^cache_/
 DECODING TABLE NAMES MATCHING ~/legacy/ AS utf8
 
 MATERIALIZE VIEWS film_list, staff_list
 
 ALTER TABLE NAMES MATCHING ~/_list$/ SET SCHEMA 'mv'
 ALTER TABLE NAMES MATCHING 'film' RENAME TO 'films'
 ALTER SCHEMA 'sakila' RENAME TO 'pagila'

MATERIALIZE VIEWS mĂ©rite une mention : les vues MySQL ne sont pas migrĂ©es, mais cette clause importe le rĂ©sultat d’une vue comme une table ordinaire. C’est la porte de sortie quand une vue sert de couche de compatibilitĂ©, et c’est aussi un moyen Ă©lĂ©gant de transformer les donnĂ©es au vol : on Ă©crit une requĂȘte MySQL qui produit la forme voulue, et pgloader charge son rĂ©sultat.

DECODING 
 AS utf8 rĂ©pond au grand classique de la table latin1 qui contient en rĂ©alitĂ© de l’UTF-8 — ou l’inverse. On peut cumuler autant de rĂšgles que de tables mal encodĂ©es, chacune avec son encodage.

⚠ Quelques prĂ©cautions

Les vues et les triggers ne sont pas migrĂ©s. La documentation est explicite : porter les vues supposerait un parseur complet du dialecte MySQL et un moteur de réécriture. Les triggers non plus. PrĂ©voyez de les réécrire Ă  la main, et de les tester : c’est souvent lĂ  que se cache la logique mĂ©tier qu’on avait oubliĂ©e.

downcase identifiers est actif par dĂ©faut. Vos tables CommandeClient deviennent commandeclient. C’est le bon comportement dans un monde PostgreSQL — mais si votre ORM gĂ©nĂšre des requĂȘtes avec les noms d’origine entre guillemets, ça casse. DĂ©cidez tĂŽt entre downcase identifiers et quote identifiers, et adaptez le code applicatif en consĂ©quence.

include drop fait un DROP 
 CASCADE. La cascade touche tous les objets qui référencent les tables ciblées, y compris ceux qui ne viennent pas de MySQL. Sur une base cible partagée, préférez include no drop et recréez une base vierge à chaque essai.

Lisez le rĂ©sumĂ© de fin de course. pgloader affiche un tableau par table avec le nombre de lignes lues, importĂ©es et rejetĂ©es. Une colonne « rejetĂ©es » non nulle renvoie vers reject.dat et reject.log, Ă©crits sous /tmp/pgloader par dĂ©faut — rĂ©pertoire rĂ©glable avec --root-dir. Ne considĂ©rez jamais une migration terminĂ©e sans avoir regardĂ© ce tableau. En phase de mise au point, --on-error-stop s’arrĂȘte Ă  la premiĂšre erreur au lieu de continuer.

RĂ©pĂ©tez la migration, ne l’improvisez pas. L’intĂ©rĂȘt d’un fichier .load versionnĂ©, c’est de pouvoir le rejouer Ă  l’identique — sur un dump de production, dans un job de CI, la veille de la bascule. Attention en revanche Ă  ce que pgloader n’est pas : il rejoue une migration complĂšte, il ne fait pas de rĂ©plication continue. Pour une bascule Ă  froid, comptez la fenĂȘtre de coupure sur la durĂ©e du dernier run.

🎉 Conclusion

pgloader transforme une migration MySQL → PostgreSQL de projet Ă  part entiĂšre en tĂąche d’aprĂšs-midi. La premiĂšre commande donne un rĂ©sultat exploitable en quelques minutes ; le reste du travail consiste Ă  Ă©crire un fichier .load de trente lignes qui encode vos dĂ©cisions — les tinyint qui sont vraiment des boolĂ©ens, les tables Ă  laisser derriĂšre, les schĂ©mas Ă  rĂ©organiser.

Mon conseil : commencez par la commande Ă  une ligne sur une copie de la base, regardez le rĂ©sumĂ©, et n’ajoutez des rĂšgles de CAST que pour ce qui vous a dĂ©plu. Les dĂ©fauts sont bons, et chaque rĂšgle Ă©crite est une rĂšgle Ă  maintenir.

Sur le choix de version : la v3 packagĂ©e par Debian et Homebrew reste la valeur sĂ»re en production. La v4 en Clojure est autrement plus confortable Ă  dĂ©ployer — un JAR, un -Xmx pour la mĂ©moire, et fini les problĂšmes de heap SBCL sur les grosses bases — mais elle est encore diffusĂ©e comme prĂ©-version. Testez-la sur votre base rĂ©elle avant de l’inscrire dans le plan de bascule.

🔗 Liens utiles

/faq

Questions fréquentes

Qu'est-ce que pgloader ?

+

pgloader est un outil open source qui charge des données dans PostgreSQL via la commande COPY. Pour une source MySQL, il convertit aussi le schéma à la volée : tables, types, index, clés étrangÚres et commentaires.

Comment migrer une base MySQL vers PostgreSQL avec pgloader ?

+

Dans le cas le plus simple, une seule commande suffit : pgloader mysql://user@host/base postgresql://user@host/base. pgloader crée les tables, convertit les types, copie les données puis reconstruit index et clés étrangÚres.

Comment pgloader gĂšre-t-il les dates 0000-00-00 de MySQL ?

+

Une rÚgle de cast par défaut transforme les valeurs 0000-00-00 et 0000-00-00 00:00:00 en NULL, via la fonction zero-dates-to-null, et retire au passage le DEFAULT correspondant. PostgreSQL n'a pas d'année zéro dans son calendrier.

pgloader migre-t-il les vues et les triggers MySQL ?

+

Non. La documentation liste les vues et les triggers comme non migrés. On peut toutefois utiliser MATERIALIZE VIEWS pour importer le résultat d'une vue comme une table ordinaire, et il faut réécrire les triggers à la main cÎté PostgreSQL.