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

đ 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 pgloaderSur macOS, la formule Homebrew installe la mĂȘme lignĂ©e :
brew install pgloaderLa 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 --versionSi 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 --versionEt voilĂ la migration complĂšte, dans sa forme la plus courte :
createdb pagila
pgloader mysql://root@localhost/sakila postgresql:///pagilaCâ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 dropsupprime cĂŽtĂ© PostgreSQL les tables portant le mĂȘme nom que celles de MySQL, enCASCADE. 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 sequencesrecale chaque sĂ©quence sur lemax()de sa colonne aprĂšs le chargement. Sans ça, votre premierINSERTapplicatif tombe sur un doublon de clĂ© primaire.workersetconcurrencypilotent le parallĂ©lisme ;rows per rangedĂ©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)devientboolean(fonctiontinyint-to-boolean), etbit(1)aussi ;- les entiers
unsignedsont promus au type supĂ©rieur âint unsigneddevientbigint,tinyint unsigneddevientsmallint; - les colonnes
auto_incrementdeviennentserialoubigserialselon la prĂ©cision ; datetimeettimestampdeviennenttimestamptz, et les valeurs0000-00-00 00:00:00sont converties enNULLparzero-dates-to-nullâ PostgreSQL nâa pas dâannĂ©e zĂ©ro ;yeardevientinteger, les typesblobdeviennentbytea, et les colonnes texte passent parremove-null-characters(le fameux\0que Postgres refuse dans untext) ;- chaque colonne
ENUMproduit unCREATE TYPEdĂ©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 typemodLes 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.