Dalibo & Contributors
db_migrator
mysql:8.2
)gvenzl/oracle-free:23-slim
)Les exercices de cet atelier sont accompagnés d’une correction valable pour un système d’exploitation Rocky Linux 8. Pour émuler les bases Oracle ou MySQL, il est nécessaire d’installer la version communautaire de Docker.
Installation et démarrage de Docker
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf -y install docker-ce docker-ce-cli containerd.io
sudo systemctl start docker
sudo usermod -aG docker $(whoami)
Se reconnecter avec le compte pour bénéficier des droits sur l’instance Docker.
Création du conteneur de son choix
mkdir mysql
export R=https://github.com/ivanceras/sakila/raw/master/mysql-sakila-db
wget $R/sakila-schema.sql -O mysql/01-mysql-sakila-schema.sql
wget $R/mysql-sakila-insert-data.sql -O mysql/02-mysql-sakila-insert-data.sql
cat <<EOF > mysql.env
MYSQL_DATABASE=sakila
MYSQL_PASSWORD=sakila
MYSQL_ROOT_PASSWORD=root
MYSQL_USER=sakila
EOF
docker pull mysql:8.2
docker run --name mysql --env-file mysql.env \
--publish 3306:3306 --volume $PWD/mysql:/docker-entrypoint-initdb.d \
--detach mysql:8.2
Démarrer le conteneur Oracle
cat <<EOF > oracle.env
APP_USER_PASSWORD=sakila
APP_USER=sakila
ORACLE_DATABASE=sakila
ORACLE_PASSWORD=manager
EOF
docker pull gvenzl/oracle-free:23-slim
docker run --name oracle --env-file oracle.env \
--publish 1521:1521 \
--detach gvenzl/oracle-free:23-slim
Alimenter la base Sakila avec le jeu de données
mkdir oracle
export R=https://github.com/ivanceras/sakila/raw/master/oracle-sakila-db
wget $R/oracle-sakila-drop-objects.sql -O oracle/oracle-sakila-drop-objects.sql
wget $R/oracle-sakila-schema.sql -O oracle/oracle-sakila-schema.sql
wget $R/oracle-sakila-schema-pl-sql.sql -O oracle/oracle-sakila-schema-pl-sql.sql
wget $R/oracle-sakila-insert-data.sql -O oracle/oracle-sakila-insert-data.sql
source oracle.env
docker exec -i oracle sqlplus -S /nolog <<-EOF
CONNECT ${APP_USER}/${APP_USER_PASSWORD}@localhost:1521/${ORACLE_DATABASE}
$(cat oracle/oracle-sakila-drop-objects.sql)
$(cat oracle/oracle-sakila-schema.sql)
$(cat oracle/oracle-sakila-schema-pl-sql.sql)
$(cat oracle/oracle-sakila-insert-data.sql)
exit
EOF
Installation et démarrage de l’instance PostgreSQL
La version communautaire est directement installée sur la VM. La communauté propose un guide de téléchargement et d’installation à l’adresse suivante : https://www.postgresql.org/download/linux/.
export R=https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64
sudo dnf install -y $R/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf -qy module disable postgresql
sudo dnf install -y postgresql16-server
sudo /usr/pgsql-16/bin/postgresql-16-setup initdb
sudo systemctl enable postgresql-16
sudo systemctl start postgresql-16
sudo -iu postgres createuser --superuser $(whoami)
C’est lors de la révision de l’année 2003 que le standard ISO/IEC fut subdivisé en 9 parties issues du standard précédent, chacune d’entre elles ayant pour ambition de couvrir un aspect différent du langage. Parmi elles, la norme ISO/IEC 9075-9, Management of External Data, aussi appelée SQL/MED est créée de toute pièce.
Ce chapitre de la norme propose les concepts de datalink et de foreign-data wrapper, ainsi que les différentes syntaxes pour les manipuler. Ces éléments peuvent paraître familiers, il s’agit de la même terminologie qu’emploie PostgreSQL pour répondre à la norme. Ce standard impliquerait que les données d’un système soient dites externes, si elles sont disponibles et gérés par un autre système de base de donnée.
Une telle architecture répondant à ces contraintes permet l’émergence des systèmes de bases de données fédérées, responsables de la gestion d’un ensemble de données autonomes et hétérogènes. Sur le plan théorique, les utilisateurs et les applications ne se connectent plus qu’à un seul point d’accès et seraient capables de consulter et modifier les données éparpillées sur différents moteurs de bases de données.
Implémentations de la norme SQL/MED dans PostgreSQL
2009 (8.4) : Ajout de l’infrastructure pour SQL/MED
2011 (9.1) : Tables externes en lecture seule, contribution
file_fdw
2013 (9.3) : Tables externes en écriture, contribution
postgres_fdw
2016 (9.5) : Support de l’instruction
IMPORT FOREIGN SCHEMA
2018 (11) : Routage des écritures pour les tables partitionnées
2021 (14) : Optimisation des INSERT
avec l’option
batch_size
, support des exécutions asynchrones
2023 (16) : Prise en compte de l’option batch_size
pour les instructions COPY
Plus de détails : https://pgpedia.info/f/foreign-data-wrapper-fdw.html
CREATE EXTENSION postgres_fdw;
CREATE SERVER localhost FOREIGN DATA WRAPPER postgres_fdw
'/tmp', dbname 'pagila', port '5432');
OPTIONS (host
CREATE USER MAPPING FOR dalibo SERVER localhost
user 'dalibo', password ''); OPTIONS (
Les options sont définies par l’extension et peuvent varier de l’une à l’autre. L’exemple proposé correspond à la création d’un wrapper vers une instance PostgreSQL qui écoute sur le socket local afin de lire les données des tables contenues dans une autre base de données de l’instance.
Syntaxe : https://www.postgresql.org/docs/current/sql-createserver.html
CREATE FOREIGN TABLE public.films (
integer NOT NULL,
film_id varchar(255) NOT NULL,
title
description text
) SERVER localhost OPTIONS ('public',
schema_name 'films'
table_name
);
ALTER FOREIGN TABLE public.films
ALTER COLUMN description OPTIONS (
'desc'
column_name );
Dans le domaine des systèmes relationnels
Plus de détails : https://wiki.postgresql.org/wiki/Foreign_data_wrappers
IMPORT FOREIGN SCHEMA
INSERT
mysql_fdw
disponible depuis le dépôt RPM
classique
oracle_fdw
disponible depuis le dépôt RPM
« non-free »
Installation de l’extension mysql_fdw
sudo dnf install -y mysql_fdw_16
Installation de l’extension
oracle_fdw
export R=https://download.oracle.com/otn_software/linux/instantclient/2112000
sudo dnf install -y $R/oracle-instantclient-basic-21.12.0.0.0-1.el8.x86_64.rpm
sudo dnf install -y $R/oracle-instantclient-sqlplus-21.12.0.0.0-1.el8.x86_64.rpm
sudo dnf install -y $R/oracle-instantclient-devel-21.12.0.0.0-1.el8.x86_64.rpm
export R=https://download.postgresql.org/pub/repos/yum/reporpms/non-free/EL-8-x86_64
sudo dnf install -y $R/pgdg-redhat-nonfree-repo-latest.noarch.rpm
sudo dnf update -y
sudo dnf install -y oracle_fdw_16
Créer les composants pour l’instance MySQL Sakila
export PGDATABASE=sakila_mysql
createdb --owner $(whoami)
CREATE EXTENSION mysql_fdw;
CREATE SERVER sakila_mysql FOREIGN DATA WRAPPER mysql_fdw
'127.0.0.1', port '3306');
OPTIONS (host
CREATE USER MAPPING FOR public SERVER sakila_mysql
'sakila', password 'sakila'); OPTIONS (username
Créer les composants pour l’instance Oracle Sakila
export PGDATABASE=sakila_oracle
createdb --owner $(whoami)
CREATE EXTENSION oracle_fdw;
CREATE SERVER sakila_oracle FOREIGN DATA WRAPPER oracle_fdw
'//localhost:1521/sakila');
OPTIONS (dbserver
CREATE USER MAPPING FOR public SERVER sakila_oracle
user 'sakila', password 'sakila'); OPTIONS (
IMPORT FOREIGN SCHEMA
Import du schéma depuis MySQL
L’extension ne fait la distinction stricte entre une table et une vue
depuis l’instance MySQL distante. Pour cette raison, l’instruction
IMPORT FOREIGN SCHEMA
est enrichie de la liste
EXCEPT
pour exclure les noms des vues issues de la base
sakila
distante.
L’import des tables se déroule dans un schéma dédié nommé
mysql
.
CREATE SCHEMA fdw;
SET search_path = fdw,public;
FOREIGN SCHEMA "sakila"
IMPORT EXCEPT ("actor_info", "customer_list", "film_list",
"nicer_but_slower_film_list", "sales_by_film_category",
"sales_by_store", "staff_list")
FROM SERVER sakila_mysql INTO fdw;
L’import aboutit avec quelques messages d’erreur :
ENUM
a été rencontré sur la table
film
, une instruction CREATE TYPE
est
proposée ;SET
a été rencontré sur la table
film
et ne peut être transposé avec PostgreSQL, le wrapper
prend la décision de ne pas l’importer la table.NOTICE: error while generating the table definition
HINT: If you encounter an error, you may need to execute the following first:
... CREATE TYPE film_rating_t AS enum('G','PG','PG-13','R','NC-17'); ...
WARNING: skipping import for relation "film"
DETAIL: MySQL SET columns are not supported.
IMPORT FOREIGN SCHEMA
Pour la table film
, l’import est effectivement
impossible, il est nécessaire de la recréer de toute pièce avec des
colonnes de type text
et des contraintes CHECK
adaptées pour les types ENUM
et SET
pour la
table finale.
Voici le résultat de la commande
SHOW CREATE TABLE sakila.film
:
Table Create Table film CREATE TABLE `film` (
smallint unsigned NOT NULL AUTO_INCREMENT,
`film_id` varchar(255) NOT NULL,
`title`
`description` text,year DEFAULT NULL,
`release_year` NOT NULL,
`language_id` tinyint unsigned DEFAULT NULL,
`original_language_id` tinyint unsigned NOT NULL DEFAULT '3',
`rental_duration` tinyint unsigned decimal(4,2) NOT NULL DEFAULT '4.99',
`rental_rate` smallint unsigned DEFAULT NULL,
`length` decimal(5,2) NOT NULL DEFAULT '19.99',
`replacement_cost` 'G','PG','PG-13','R','NC-17') DEFAULT 'G',
`rating` enum(set('Trailers','Commentaries','Deleted Scenes','Behind the Scenes') DEFAULT NULL,
`special_features` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`last_update` PRIMARY KEY (`film_id`),
KEY `idx_title` (`title`),
KEY `idx_fk_language_id` (`language_id`),
KEY `idx_fk_original_language_id` (`original_language_id`),
CONSTRAINT `fk_film_language` FOREIGN KEY (`language_id`)
REFERENCES `language` (`language_id`) ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT `fk_film_language_original` FOREIGN KEY (`original_language_id`)
REFERENCES `language` (`language_id`) ON DELETE RESTRICT ON UPDATE CASCADE
=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb3 ) ENGINE
L’instruction de création de la table film
avec la bonne
correspondance du typage de colonnes serait la suivante :
CREATE FOREIGN TABLE fdw.film (
smallint NOT NULL,
film_id varchar(255),
title
description text,smallint,
release_year smallint NOT NULL,
language_id smallint,
original_language_id smallint NOT NULL,
rental_duration numeric(4,2) NOT NULL,
rental_rate length smallint,
numeric(5,2) NOT NULL,
replacement_cost
rating text,
special_features text,timestamp without time zone
last_update
)
SERVER sakila_mysql
OPTIONS ('sakila',
dbname 'film'
table_name );
Ainsi, les 16 tables externes sont présentes dans le schéma
fdw
:
sakila_mysql=# \d
List of relations
Schema | Name | Type | Owner
--------+---------------+---------------+----------
fdw | actor | foreign table | postgres
fdw | address | foreign table | postgres
fdw | category | foreign table | postgres
fdw | city | foreign table | postgres
fdw | country | foreign table | postgres
fdw | customer | foreign table | postgres
fdw | film | foreign table | postgres
fdw | film_actor | foreign table | postgres
fdw | film_category | foreign table | postgres
fdw | film_text | foreign table | postgres
fdw | inventory | foreign table | postgres
fdw | language | foreign table | postgres
fdw | payment | foreign table | postgres
fdw | rental | foreign table | postgres
fdw | staff | foreign table | postgres
fdw | store | foreign table | postgres
(16 rows)
Import du schéma depuis Oracle
Les vues doivent être exclues de l’import avec l’option
EXCEPT
de l’instruction
IMPORT FOREIGN SCHEMA
.
CREATE SCHEMA fdw;
SET search_path = fdw,public;
FOREIGN SCHEMA "SAKILA"
IMPORT EXCEPT ("actor_info", "customer_list", "film_list",
"sales_by_film_category", "sales_by_store", "staff_list")
FROM SERVER sakila_oracle INTO fdw;
Les 16 tables sont importées sans erreur dans le schéma
fdw
:
sakila_oracle=# \d
List of relations
Schema | Name | Type | Owner
--------+---------------+---------------+----------
fdw | actor | foreign table | postgres
fdw | address | foreign table | postgres
fdw | category | foreign table | postgres
fdw | city | foreign table | postgres
fdw | country | foreign table | postgres
fdw | customer | foreign table | postgres
fdw | film | foreign table | postgres
fdw | film_actor | foreign table | postgres
fdw | film_category | foreign table | postgres
fdw | film_text | foreign table | postgres
fdw | inventory | foreign table | postgres
fdw | language | foreign table | postgres
fdw | payment | foreign table | postgres
fdw | rental | foreign table | postgres
fdw | staff | foreign table | postgres
fdw | store | foreign table | postgres
(16 rows)
film.special_features
Transfert sans transformation
Création des tables permanentes
CREATE TABLE public.actor (LIKE fdw.actor);
CREATE TABLE public.address (LIKE fdw.address);
CREATE TABLE public.category (LIKE fdw.category);
CREATE TABLE public.city (LIKE fdw.city);
CREATE TABLE public.country (LIKE fdw.country);
CREATE TABLE public.customer (LIKE fdw.customer);
CREATE TABLE public.film(LIKE fdw.film);
CREATE TABLE public.film_actor (LIKE fdw.film_actor);
CREATE TABLE public.film_category (LIKE fdw.film_category);
CREATE TABLE public.film_text (LIKE fdw.film_text);
CREATE TABLE public.inventory (LIKE fdw.inventory);
CREATE TABLE public.language (LIKE fdw.language);
CREATE TABLE public.payment (LIKE fdw.payment);
CREATE TABLE public.rental (LIKE fdw.rental);
CREATE TABLE public.staff (LIKE fdw.staff);
CREATE TABLE public.store (LIKE fdw.store);
Insertion des lignes
INSERT INTO public.actor SELECT * FROM fdw.actor;
INSERT INTO public.address SELECT * FROM fdw.address;
INSERT INTO public.category SELECT * FROM fdw.category;
INSERT INTO public.city SELECT * FROM fdw.city;
INSERT INTO public.country SELECT * FROM fdw.country;
INSERT INTO public.customer SELECT * FROM fdw.customer;
INSERT INTO public.film SELECT * FROM fdw.film;
INSERT INTO public.film_actor SELECT * FROM fdw.film_actor;
INSERT INTO public.film_category SELECT * FROM fdw.film_category;
INSERT INTO public.film_text SELECT * FROM fdw.film_text;
INSERT INTO public.inventory SELECT * FROM fdw.inventory;
INSERT INTO public.language SELECT * FROM fdw.language;
INSERT INTO public.payment SELECT * FROM fdw.payment;
INSERT INTO public.rental SELECT * FROM fdw.rental;
INSERT INTO public.staff SELECT * FROM fdw.staff;
INSERT INTO public.store SELECT * FROM fdw.store;
Transformation des données à la volée
Cas particulier pour la colonne
film.special_features
SET
est similaire à un type ARRAY
pour PostgreSQLUne première proposition de portage du type text
vers
text[]
consiste à changer la colonne avec une fonction de
conversion des données insérées :
ALTER TABLE public.film
ALTER COLUMN special_features TYPE text[]
USING string_to_array(special_features, ',');
Dans le cas où la transformation de la table est trop coûteuse avec
un nombre de lignes trop important, il est possible de partir d’une
table public.film
vide avec la bonne définition de la
colonne special_features
puis d’importer les données avec
une transformation à la volée :
TRUNCATE public.film;
INSERT INTO public.film
SELECT film_id, title, description, release_year,
language_id, original_language_id, rental_duration,length, replacement_cost, rating,
rental_rate, ','), last_update
string_to_array(special_features, FROM fdw.film;
Le portage de la colonne special_features
s’accompagne
d’une contrainte CHECK
pour respecter le besoin initial que
proposait le type SET
.
ALTER TABLE public.film ADD CHECK (
<@ ARRAY['Behind the Scenes', 'Commentaries', 'Deleted Scenes', 'Trailers']
special_features );
INSERT ... SELECT
Dans le domaine des migrations, il est courant de choisir un outil polyvalent pour automatiser les grandes étapes du projet, tel que la conversion du modèle de données ou le transfert optimisée des lignes.
Les outils ci-après ont été sélectionnés pour leur licence libre, leur réputation ainsi que leur prise en compte de la technologie des Foreign Data Wrappers.
FDW_SERVER
à renseignerOra2Pg est l’un des outils de migration les plus avancés en matière de migration vers PostgreSQL. À l’origine, il a été conçu pour accompagner les équipes dans le portage d’Oracle vers PostgreSQL, puis il s’est enrichi avec le support de MySQL (version 16.0, octobre 2015) et le support de SQL Server (version 24.0, juillet 2023).
La version 22.0 s’est doté d’un nouveau mode de transfert avec la
directive FDW_SERVER
. Dès lors que cette dernière est
valorisée, Ora2Pg crée automatiquement les tables externes en respectant
la configuration de l’utilisateur et déclenche le transfert des lignes
avec des instructions INSERT ... SELECT
. Plusieurs tables
peuvent être exportées en parallèle, à l’image des autres modes de
transferts historiques.
Notes de sortie : https://github.com/darold/ora2pg/releases/tag/v22.0
L’auteur, dans un article, a réalisé un benckmark entre les deux modes de transfert et annonce des gains significatifs sur les temps de copie des données.
Source : https://www.migops.com/blog/ora2pg-now-supports-oracle_fdw-to-increase-the-data-migration-speed/
ora_migrator
(et oracle_fdw
)mysql_migrator
(et mysql_fdw
)mssql_migrator
(et tds_fdw
)L’outil db_migrator
est une proposition de Laurenz Albe,
le principal contributeur de l’extension oracle_fdw
.
L’outil se veut être un framework bas niveau pour simplifier
les migrations. L’architecture logicielle permet l’ajout de
plugins pour supporter facilement de nouveaux systèmes de bases
de données comme point de départ.
Les plugins mysql_migrator
et
mssql_migrator
ont été conçues dans un cadre de recherches
et ont permis de faire évoluer db_migrator
significativement, notamment avec l’ajout du partitionnement et la
sortie de la version 1.0 en février 2023.
Notes de sortie : https://github.com/cybertec-postgresql/db_migrator/blob/master/CHANGELOG.md
db_migrator
et de ses
pluginsdb_migrate_tables
CREATE EXTENSION
Téléchargement
Les extensions ne sont pas encore empaquetées, il est nécessaire de télécharger leur source pour les déployer au niveau de l’installation de l’instance.
export P=https://codeload.github.com
export R=zip/refs/heads
wget $P/cybertec-postgresql/db_migrator/$R/master -O db_migrator.zip
wget $P/cybertec-postgresql/ora_migrator/$R/master -O ora_migrator.zip
wget $P/fljdin/mysql_migrator/$R/main -O mysql_migrator.zip
unzip -o \*.zip
Déploiement
Puisque les extensions n’ont pas besoin d’être compilées, une simple
installation grâce aux copies des fichiers control
et
sql
est suffisante. Dans le cas d’un déploiement plus
complexe, il aurait été nécessaire d’installer les en-têtes de
développement de PostgreSQL ainsi que le composant
make
.
export PATH=/usr/pgsql-16/bin:$PATH
export EXT=$(pg_config --sharedir)/extension
sudo install -v -c -m 644 */{*.control,*--*.sql} $EXT
Installation
Enfin, il reste à installer le bon plugin dans la base de données de son choix.
export PGDATABASE=sakila_mysql
CREATE EXTENSION mysql_migrator CASCADE;
Comme l’indique la documentation du plugin, le compte
SAKILA
qui est employé pour consulter le catalogue doit
disposer des droits de lecture sur les tables du dictionnaire. La
commande suivante permet de mettre en place les privilèges :
source oracle.env
docker exec -i oracle sqlplus -S /nolog <<-EOF
CONNECT system/${ORACLE_PASSWORD}@localhost:1521/${ORACLE_DATABASE}
GRANT SELECT ANY DICTIONARY TO SAKILA;
exit
EOF
export PGDATABASE=sakila_oracle
CREATE EXTENSION ora_migrator CASCADE;
db_migrate_prepare
plugin
: nom du plugin à utiliserserver
: le nom du serveur à créer au préalablestaging_schema
: emplacement des tables externes
connectées au catalogue distantpgstage_schema
: emplacement du catalogue normalisé (ou
snapshot)only_schemas
: liste des schémas distants à
exporteroptions
(optionnel) : propre à chaque pluginLa méthode db_migrate_prepare
s’appuie sur les règles
définies par le plugin pour créer de nouvelles tables externes dans un
schéma temporaire afin de consulter les vues et tables du catalogue
distant.
Les résultats obtenus sont normalisés pour être stockés dans une
série de tables à l’intérieur d’un autre schéma, appelé
pgstage
ou pgsql_stage
. Le paramètre
only_schemas
permet de limiter le nombre de schémas à
traiter dans le snapshot.
La méthode db_migrate_refresh
est disponible pour mettre
à jour partiellement le snapshot déjà présent dans le schéma
pgstage
, pour peu qu’aucune table ou objet n’ait été
renommé ou supprimé.
Récupération du catalogue
SELECT db_migrate_prepare(
=> 'mysql_migrator',
plugin => 'sakila_mysql',
server => '{sakila}'
only_schemas );
SELECT db_migrate_prepare(
=> 'ora_migrator',
plugin => 'sakila_oracle',
server => '{SAKILA}'
only_schemas );
Consultation des objets créés
sakila_mysql=# \det fdw_stage.*
List of foreign tables
Schema | Table | Server
-----------+-------------------------+--------------
fdw_stage | CHECK_CONSTRAINTS | sakila_mysql
fdw_stage | COLUMNS | sakila_mysql
fdw_stage | COLUMN_PRIVILEGES | sakila_mysql
fdw_stage | KEY_COLUMN_USAGE | sakila_mysql
fdw_stage | PARAMETERS | sakila_mysql
fdw_stage | PARTITIONS | sakila_mysql
fdw_stage | REFERENTIAL_CONSTRAINTS | sakila_mysql
fdw_stage | ROUTINES | sakila_mysql
fdw_stage | SCHEMATA | sakila_mysql
fdw_stage | STATISTICS | sakila_mysql
fdw_stage | TABLES | sakila_mysql
fdw_stage | TABLE_CONSTRAINTS | sakila_mysql
fdw_stage | TABLE_PRIVILEGES | sakila_mysql
fdw_stage | TRIGGERS | sakila_mysql
fdw_stage | VIEWS | sakila_mysql
fdw_stage | innodb_index_stats | sakila_mysql
sakila_oracle=# \det fdw_stage.*
List of foreign tables
Schema | Table | Server
-----------+----------------------+---------------
fdw_stage | checks | sakila_oracle
fdw_stage | column_privs | sakila_oracle
fdw_stage | columns | sakila_oracle
fdw_stage | foreign_keys | sakila_oracle
fdw_stage | func_src | sakila_oracle
fdw_stage | index_exp | sakila_oracle
fdw_stage | keys | sakila_oracle
fdw_stage | pack_src | sakila_oracle
fdw_stage | partition_columns | sakila_oracle
fdw_stage | schemas | sakila_oracle
fdw_stage | segments | sakila_oracle
fdw_stage | sequences | sakila_oracle
fdw_stage | subpartition_columns | sakila_oracle
fdw_stage | table_privs | sakila_oracle
fdw_stage | tables | sakila_oracle
fdw_stage | trig | sakila_oracle
fdw_stage | views | sakila_oracle
pgsql_stage
sakila_mysql=# \dt pgsql_stage.*
db_migrate_mkforeign
IMPORT FOREIGN SCHEMA
Création du schéma et des tables externes
L’ensemble des étapes qui suivent s’appuient sur les tables du
snapshot. La première consiste à créer les schémas présents
dans la table pgsql_stage.schemas
ainsi que les séquences
issues de la table pgsql_stage.sequences
. Les tables
externes sont ensuite créées sur la base des éléments collectés et
présents dans les tables pgsql_stage.tables
et
pgsql_stage.columns
.
Il est possible d’ajuster les noms de schémas, des séquences, de
tables ou des colonnes en réaliser une série d’instructions
UPDATE
si besoin.
SELECT db_migrate_mkforeign(
=> 'mysql_migrator',
plugin => 'sakila_mysql'
server );
SELECT db_migrate_mkforeign(
=> 'ora_migrator',
plugin => 'sakila_oracle'
server );
Consultation des objets créés
sakila_mysql=# \det sakila.*
List of foreign tables
Schema | Table | Server
--------+---------------+--------------
sakila | actor | sakila_mysql
sakila | address | sakila_mysql
sakila | category | sakila_mysql
sakila | city | sakila_mysql
sakila | country | sakila_mysql
sakila | customer | sakila_mysql
sakila | film | sakila_mysql
sakila | film_actor | sakila_mysql
sakila | film_category | sakila_mysql
sakila | film_text | sakila_mysql
sakila | inventory | sakila_mysql
sakila | language | sakila_mysql
sakila | payment | sakila_mysql
sakila | rental | sakila_mysql
sakila | staff | sakila_mysql
sakila | store | sakila_mysql
sakila_mysql=# \ds sakila.*
List of relations
Schema | Name | Type | Owner
--------+---------------+----------+----------
sakila | actor_seq | sequence | postgres
sakila | address_seq | sequence | postgres
sakila | category_seq | sequence | postgres
sakila | city_seq | sequence | postgres
sakila | country_seq | sequence | postgres
sakila | customer_seq | sequence | postgres
sakila | film_seq | sequence | postgres
sakila | film_text_seq | sequence | postgres
sakila | inventory_seq | sequence | postgres
sakila | language_seq | sequence | postgres
sakila | payment_seq | sequence | postgres
sakila | rental_seq | sequence | postgres
sakila | staff_seq | sequence | postgres
sakila | store_seq | sequence | postgres
db_migrate_tables
materialize_foreign_table
INSERT
Migration automatique des données
La première méthode db_migrate_tables
se charge de
récupérer toutes les tables à migrer, celles dont le champ
migrate
est actif dans le snapshot
(pgsql_stage.tables
). Ensuite, chaque table est traitée une
à une au sein d’une transaction qui lui est propre à l’aide d’une autre
méthode nommée materialize_foreign_table
:
with_data
est
activeEn cas d’anomalie dans l’un de ces étapes, c’est l’entièreté des
opérations qui sont annulées à l’aide d’un ROLLBACK
.
SELECT db_migrate_tables(
=> 'mysql_migrator',
plugin => true
with_data );
SELECT db_migrate_tables(
=> 'ora_migrator',
plugin => true
with_data );
Migration fine des données
Dans certaines situations, il peut être nécessaire de filtrer certaines lignes lors de la migration, par exemple pour anticiper la copie des lignes mortes et les exclure lors de l’opération de bascule.
L’exemple précédant le démontrait : la méthode
materialize_foreign_table
ne permet pas de filtrer les
lignes lors de l’instruction INSERT
. Il est alors
nécessaire d’utiliser une autre méthode de bas niveau pour construire la
table externe à traiter dans un schéma à part, par exemple
sakila_fdw
.
CREATE SCHEMA sakila_fdw;
-- pour la base MySQL
SELECT regexp_replace(statement,
'TABLE ([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)',
'TABLE \1_fdw.\2')
FROM construct_foreign_tables_statements(
=> 'mysql_migrator',
plugin => 'sakila_mysql')
server WHERE schema_name = 'sakila' \gexec
-- pour la base Oracle
SELECT regexp_replace(statement,
'TABLE ([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)',
'TABLE \1_fdw.\2')
FROM construct_foreign_tables_statements(
=> 'ora_migrator',
plugin => 'sakila_oracle')
server WHERE schema_name = 'sakila' \gexec
Il est alors possible de rédiger ses propres instructions
INSERT
, par exemple pour la table rental
que
nous vidons au préalable :
TRUNCATE TABLE sakila.rental;
INSERT INTO sakila.rental SELECT * FROM sakila_fdw.rental WHERE rental_date < '2005-08-01';
INSERT INTO sakila.rental SELECT * FROM sakila_fdw.rental WHERE rental_date BETWEEN '2005-08-01' AND '2006-01-01';
Cette méthode permet également de contrôler finement les étapes, et d’ajouter un contrôle intermédiaire avant la suppression des tables externes :
CREATE TABLE public.migration_report (
schema_name name,
table_name name,
total_rows bigint,
imported_rows bigint,char(1)
completed GENERATED ALWAYS AS (
CASE WHEN total_rows = imported_rows
THEN '🔵' ELSE '🔴'
END
) STORED,PRIMARY KEY (schema_name, table_name)
);
DO $$DECLARE
record;
rec
total bigint;
imported bigint;BEGIN
FOR rec IN
SELECT schema, table_name
FROM pgsql_stage.tables WHERE migrate
LOOP
EXECUTE format('SELECT count(*) FROM %s_fdw.%s', rec.schema, rec.table_name) INTO total;
EXECUTE format('SELECT count(*) FROM %s.%s', rec.schema, rec.table_name) INTO imported;
INSERT INTO public.migration_report
VALUES (rec.schema, rec.table_name, total, imported);
END LOOP;
END; $$;
Résultat de la copie des données :
SELECT * FROM public.migration_report
ORDER BY schema_name, table_name;
schema_name | table_name | total_rows | imported_rows | completed
-------------+---------------+------------+---------------+-----------
sakila | actor | 200 | 200 | 🔵
sakila | address | 603 | 603 | 🔵
sakila | category | 16 | 16 | 🔵
sakila | city | 600 | 600 | 🔵
sakila | country | 109 | 109 | 🔵
sakila | customer | 599 | 599 | 🔵
sakila | film | 1000 | 1000 | 🔵
sakila | film_actor | 5462 | 5462 | 🔵
sakila | film_category | 1000 | 1000 | 🔵
sakila | film_text | 1000 | 1000 | 🔵
sakila | inventory | 4581 | 4581 | 🔵
sakila | language | 6 | 6 | 🔵
sakila | payment | 16049 | 16049 | 🔵
sakila | rental | 16044 | 15862 | 🔴
sakila | staff | 2 | 2 | 🔵
sakila | store | 2 | 2 | 🔵
La table rental
requiert une attention particulière ; en
effet, les données supérieures à 2016 ont été volontairement ignorées.
La commande suivante permet de rattraper les lignes manquantes et de
terminer la migration.
INSERT INTO sakila.rental SELECT * FROM sakila_fdw.rental WHERE rental_date > '2006-01-01';
À l’issue de la migration, le schéma temporaire des tables externes peut être supprimé.
DROP SCHEMA sakila_fdw CASCADE;
db_migrate_functions
db_migrate_triggers
db_migrate_views
Cette étape permet de récupérer la définition des objets complexes depuis l’instance distante, mais ne procède pas à leur conversion. Il n’est pas assuré que ces objets soient compatibles avec PostgreSQL selon la manière dont ils ont été écrits.
Le plugin mysql_migrator
ne permet pas de récupérer le
corps des fonctions et des procédures.
L’export des triggers avec db_migrator
est actuellement
en erreur et doit faire l’objet d’un patch correctif. Pour une cause
similaire, l’export des vues avec ora_migrator
est en
erreur.
Exemple d’une vue avec MySQL
Pour commencer, il est nécessaire de désactiver la migration de
toutes les vues afin que la méthode db_migrate_views
puisse
les ignorer. Leurs créations nécessitent une revue manuelle avec des
ajustements pour rendre les requêtes SQL compatibles avec
PostgreSQL.
UPDATE pgsql_stage.views SET migrate = false;
L’obtention de la définition originale se réalise en consultant la
table pgsql_stage.views
. Ici, nous concentrons nos efforts
sur une seule vue, nommée « customer_list » :
SELECT orig_def FROM pgsql_stage.views
WHERE view_name = 'customer_list' \g (tuples_only)
select `cu`.`customer_id` AS `ID`,concat(`cu`.`first_name`,' ',`cu`.`last_name`)
AS `name`,`a`.`address` AS `address`,`a`.`postal_code` AS `zip code`,`a`.`phone`
AS `phone`,`sakila`.`city`.`city` AS `city`,`sakila`.`country`.`country` AS
if(`cu`.`active`,'active','') AS `notes`,`cu`.`store_id` AS `SID` from
`country`,join `sakila`.`address` `a` on((`cu`.`address_id` =
(((`sakila`.`customer` `cu` join `sakila`.`city` on((`a`.`city_id` =
`a`.`address_id`))) join `sakila`.`country`
`sakila`.`city`.`city_id`))) on((`sakila`.`city`.`country_id` = `sakila`.`country`.`country_id`)))
La définition provient directement du catalogue MySQL, tel que réécrit par le moteur lui-même. Pour rendre cette requête compatible avec PostgreSQL, il est nécessaire de la réécrire avec les quelques astuces suivantes :
postal_code
doit être échappé
par des guillemets puisqu’il contient un caractère d’espacement ;IF
doit être remplacée par une instruction
CASE
.UPDATE pgsql_stage.views SET migrate = true,
= $$
definition select cu.customer_id AS ID,
concat(cu.first_name,' ',cu.last_name) AS name,
AS address,
a.address AS "zip code",
a.postal_code AS phone,
a.phone AS city,
sakila.city.city AS country,
sakila.country.country CASE WHEN cu.active > 0 THEN 'active' ELSE '' END) AS notes,
(AS SID
cu.store_id from (((sakila.customer cu
join sakila.address a on((cu.address_id = a.address_id)))
join sakila.city on((a.city_id = sakila.city.city_id)))
join sakila.country on((sakila.city.country_id = sakila.country.country_id)))
$$WHERE view_name = 'customer_list';
Enfin, la méthode db_migrate_views
peut être
invoquée.
SELECT db_migrate_views(plugin => 'mysql_migrator');
db_migrate_indexes
construct_indexes_statements
db_migrate_constraints
construct_key_constraints_statements
construct_fkey_constraints_statements
construct_check_constraints_statements
construct_defaults_statements
La dernière étape d’une migration consiste à recréer les contraintes et les index. Ces deux opérations nécessitent que les données soient importées au préalable pour économiser des opérations disques.
L’extension db_migrator
propose deux méthodes distinctes
pour les créer. Il est recommandé de créer les index avant les
contraintes, pour optimiser les contrôles d’intégrité, notamment sur les
colonnes externes lors de l’ajout de contraintes étrangères.
Création des index
SELECT db_migrate_indexes(plugin => 'mysql_migrator');
SELECT db_migrate_indexes(plugin => 'ora_migrator');
Création des contraintes
La méthode db_migrate_constraints
est responsable de
recréer les contraintes issues du modèle distant, à savoir : clés
primaires, clés étrangères, contraintes d’unicité et contraintes de type
CHECK
. Elle embarque également une dernière étape qui
correspond à l’ajout des valeurs par défaut des colonnes de tables.
SELECT db_migrate_constraints(plugin => 'mysql_migrator');
SELECT db_migrate_constraints(plugin => 'ora_migrator');
Exécution parallélisée
Dans la vie réelle, l’étape finale de création des index et des
contraintes sur des données volumineuses ne peut être réalisée avec
cette approche séquentielle. Les instructions CREATE INDEX
et ALTER TABLE
doivent pouvoir être exécutées en parallèle,
à l’aide de plusieurs processus.
Les méthodes de bas-niveau fournies par db_migrator
permettent d’obtenir ces instructions. Grâce à elles, il devient
possible d’utiliser des outils tiers pour ouvrir plusieurs processus et
y répartir les opérations à travers plusieurs connexions.
construct_indexes_statements
construct_key_constraints_statements
construct_fkey_constraints_statements
construct_check_constraints_statements
construct_defaults_statements
La première étape consiste à exporter les instructions dans des fichiers distincts. Ici, nous séparons les instructions dans trois fichiers (index et clés uniques, contraintes étrangères et valeurs par défaut des colonnes de tables).
-- changer la variable pour utiliser le bon plugin
set plugin ora_migrator
\
-- l'export se réalise avec la méta-commande \g et ses options
-- les clés primaires et contraintes d'unicité sont similaires à la création d'index
SELECT statement FROM construct_indexes_statements(plugin => :'plugin')
UNION
SELECT statement FROM construct_key_constraints_statements(plugin => :'plugin')
=unaligned tuples_only) indexes.sql
\g (format
-- le reste des contraintes est exporté dans un même fichier
-- chaque instruction doit être exportée sans saut de ligne avec REPLACE
SELECT statement FROM construct_fkey_constraints_statements(plugin => :'plugin')
UNION
SELECT replace(statement, E'\n', '')
FROM construct_check_constraints_statements(plugin => :'plugin')
=unaligned tuples_only) constraints.sql
\g (format
-- ajout d'un point-virgule à chaque instruction pour que le fichier
-- soit correctement traité par l'option -f de psql
SELECT CONCAT(statement, ';') FROM construct_defaults_statements(plugin => :'plugin')
=unaligned tuples_only) defaults.sql \g (format
Dans le cas où les index et contraintes sont déjà présents dans le
schéma sakila
de la base cible, il est possible de
substituer les instructions de création par des instructions de
suppression pour nettoyer le schéma.
drop_index='s/.* INDEX ([^ ]+).*/DROP INDEX \1;/'
drop_constraint='s/(.*) ADD CONSTRAINT ([^ ]+).*/\1 DROP CONSTRAINT \2 CASCADE;/'
export PGOPTIONS="-c search_path=sakila,public"
perl -pe "$drop_index,$drop_constraint" indexes.sql constraints.sql | psql
La commande xargs
permet ensuite de récupérer les
instructions lignes à lignes depuis les fichiers et de les faire
exécuter par le premier processus disponible. Par exemple, pour répartir
les instructions de création d’index sur 4 processus, la commande
xargs
prend la forme suivante :
xargs -P 4 -a indexes.sql -d '\n' -I % sh -c 'psql -c "%"'
De la même manière, il devient aisé de créer les contraintes en parallèle :
xargs -P 4 -a constraints.sql -d '\n' -I % sh -c 'psql -c "%"'
La remise en place des valeurs par défaut des colonnes de tables n’est pas coûteuse et les instructions du fichier peuvent être exécutées dans la même connexion :
psql -f defaults.sql
db_migrate_finish
Dès que les contrôles de fin de migration sont positifs, l’ensemble
des objets créés par l’extension db_migrator
peuvent être
supprimés à l’aide de la méthode db_migrate_finish
.
SELECT db_migrate_finish();
Enfin, les extensions peuvent être supprimées de la base de données.
DROP EXTENSION db_migrator CASCADE;
DROP EXTENSION mysql_fdw;
DROP EXTENSION oracle_fdw;
URL des projets :
Idées de plugins à créer :
Conférences des années passées
REX sur une migration d’Oracle à PostgreSQL - par Cédric Champmartin, Université de Lorraine (2023)
Migration vers PostgreSQL : mener de gros volumes de données à bon port - par Philippe Beaudoin, DALIBO (2022)
La validation de migration facilitée avec Ora2Pg - par Gilles Darold, MigOps Inc (2021)
Migrez vos bases de données vers PostgreSQL et retrouvez une vraie liberté - Fabrice Viault, Cheops technologies (2019)
Démarche de transition à PostgreSQL et outillage open source - Anthony Nowocien, Société générale (2019)
Série d’articles sur les techniques de migration
Parlons un peu des données externes (juillet 2021)
Migrer vers PostgreSQL (décembre 2021)
En route vers la liberté avec db_migrator (août 2023)
Les modes de transfert dans une migration (octobre 2023)
Contenus pédagogiques maintenus par Dalibo
Les contenus sont mis à disposition sous licence Creative Commons (CC BY-NC-SA).