pgLoader es una herramienta de código abierto que lee una base de datos MySQL y la carga en PostgreSQL en un solo paso: crea las tablas con tipos equivalentes, copia los datos, reconstruye índices y claves foráneas y ajusta las secuencias. En esta guía migrarás una base de datos MySQL a un servidor PostgreSQL con Ubuntu 24.04, revisarás los puntos que pgLoader no convierte por ti, verificarás los datos y prepararás el cambio de la aplicación con un plan de vuelta atrás.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS para PostgreSQL, por ejemplo un VPS de CubePath, con un usuario no root con privilegios sudo. pgLoader se ejecutará en este mismo servidor.
  • Una base de datos MySQL 8.0 accesible desde ese servidor por el puerto 3306. En los ejemplos se llama appdb y está en your_mysql_host.
  • Acceso de administrador a MySQL para crear un usuario de migración.
  • Espacio en disco en el servidor PostgreSQL de al menos el doble del tamaño de los datos de MySQL.

Paso 1: Hacer una copia de seguridad de MySQL

pgLoader solo lee de MySQL, pero antes de cualquier migración necesitas una copia de la que partir si algo sale mal. En el servidor MySQL, crea un volcado consistente sin bloquear las tablas InnoDB:

mysqldump --single-transaction --routines --triggers --events \
  -u root -p appdb > appdb_$(date +%F).sql

Comprueba que el volcado termina correctamente; la última línea debe ser un comentario Dump completed:

tail -n 1 appdb_*.sql
-- Dump completed on 2026-09-25 10:04:11

Paso 2: Analizar el esquema de MySQL

pgLoader convierte automáticamente tablas, columnas, índices y claves foráneas, pero no traduce procedimientos almacenados, funciones, triggers ni vistas. Localízalos antes de empezar para planificar su reescritura en PL/pgSQL:

mysql -u root -p -e "
SELECT ROUTINE_TYPE, ROUTINE_NAME FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = 'appdb';
SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = 'appdb';
SELECT TABLE_NAME FROM information_schema.VIEWS WHERE TABLE_SCHEMA = 'appdb';"

Busca también columnas con tipos que se comportan distinto en PostgreSQL:

mysql -u root -p -e "
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, EXTRA
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'appdb'
  AND (DATA_TYPE IN ('enum', 'set', 'year') OR COLUMN_TYPE LIKE '%unsigned%' OR EXTRA LIKE '%on update%')
ORDER BY TABLE_NAME;"

Lo que debes saber de cada caso:

  • TINYINT(1) se convierte en boolean, y AUTO_INCREMENT en serial o bigserial.
  • Los enteros UNSIGNED pasan al tipo entero inmediatamente mayor para que quepan todos los valores.
  • ENUM se convierte en un tipo enumerado de PostgreSQL. Las columnas SET no tienen equivalente directo: revisa cómo quedan tras la migración.
  • Las fechas 0000-00-00 no existen en PostgreSQL y se cargan como NULL.
  • ON UPDATE CURRENT_TIMESTAMP no existe en PostgreSQL; revisa esas columnas después de migrar y, si la aplicación depende de ello, recrea el comportamiento con un trigger.

Anota además el número de filas de las tablas principales para compararlo al final.

Paso 3: Preparar PostgreSQL

En el servidor de destino, instala PostgreSQL 16 desde los repositorios de Ubuntu:

sudo apt update
sudo apt install -y postgresql

Crea el rol de la aplicación y una base de datos vacía de su propiedad. Sustituye your_strong_password por una contraseña robusta:

sudo -u postgres psql -c "CREATE ROLE appuser WITH LOGIN PASSWORD 'your_strong_password';"
sudo -u postgres createdb --owner=appuser --encoding=UTF8 appdb

Comprueba que puedes conectarte con ese usuario:

psql "host=127.0.0.1 dbname=appdb user=appuser" -c "SELECT current_user;"
 current_user
--------------
 appuser
(1 row)

Paso 4: Instalar pgLoader

pgLoader está en los repositorios de Ubuntu 24.04:

sudo apt install -y pgloader

Verifica la instalación:

pgloader --version

La salida muestra la versión de pgLoader y la de SBCL con la que está compilado.

Paso 5: Crear un usuario de lectura en MySQL

Crea en MySQL un usuario con permisos de solo lectura sobre la base de datos, limitado a la IP del servidor PostgreSQL (your_server_ip):

CREATE USER 'pgloader'@'your_server_ip' IDENTIFIED WITH mysql_native_password BY 'your_mysql_password';
GRANT SELECT, SHOW VIEW ON appdb.* TO 'pgloader'@'your_server_ip';

Se usa mysql_native_password porque las versiones de pgLoader empaquetadas no admiten el método caching_sha2_password, que es el predeterminado en MySQL 8. Si tu servidor es MySQL 8.4, este plugin viene desactivado y debes habilitarlo con mysql_native_password=ON en la sección [mysqld] de su configuración.

Desde el servidor PostgreSQL, comprueba que llegas a MySQL. Si no tienes el cliente, instálalo con sudo apt install -y mysql-client:

mysql -h your_mysql_host -u pgloader -p -e "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'appdb';"

Paso 6: Escribir el archivo de carga

Un archivo de comandos permite repetir la migración con las mismas opciones tantas veces como necesites. Créalo en tu directorio personal:

nano ~/appdb.load
LOAD DATABASE
  FROM mysql://pgloader:your_mysql_password@your_mysql_host/appdb
  INTO postgresql://appuser:[email protected]/appdb

WITH include drop, create tables, create indexes, reset sequences,
     foreign keys, workers = 4, concurrency = 1

SET PostgreSQL PARAMETERS
    maintenance_work_mem to '512MB',
    work_mem to '64MB'

ALTER SCHEMA 'appdb' RENAME TO 'public';

Qué hace cada parte:

  • FROM e INTO: cadenas de conexión de origen y destino. Si una contraseña contiene caracteres como @, : o /, codifícalos en formato URL (por ejemplo @ como %40).
  • include drop, create tables: borra y recrea las tablas en cada ejecución, así puedes repetir la migración de prueba desde cero.
  • reset sequences: ajusta las secuencias al valor máximo de cada columna autoincremental.
  • ALTER SCHEMA ... RENAME TO 'public': pgLoader crea por defecto un esquema con el nombre de la base de datos MySQL. Con esta línea las tablas quedan en public, que es donde la mayoría de aplicaciones las buscan.

Para excluir tablas que no quieras migrar, como cachés o sesiones, añade antes de ALTER SCHEMA una línea EXCLUDING TABLE NAMES MATCHING 'sessions', ~/^cache_/.

El archivo contiene contraseñas, así que restringe sus permisos:

chmod 600 ~/appdb.load

Paso 7: Ejecutar la migración

Lanza pgLoader con el archivo de carga y guarda la salida en un registro:

pgloader ~/appdb.load 2>&1 | tee ~/pgloader.log

Al terminar, pgLoader muestra un resumen por tabla. Revisa la columna errors, que debe estar a 0 en todas las filas:

             table name     errors       rows      bytes      total time
-----------------------  ---------  ---------  ---------  --------------
        public.clientes          0     210944    38.2 MB          4.812s
         public.pedidos          0    4812330   702.4 MB         58.301s
          public.lineas          0    1920410   210.9 MB         21.442s
-----------------------  ---------  ---------  ---------  --------------
      Total import time          ✓    6943684   951.5 MB       1m32.120s

Si alguna tabla muestra errores, busca el detalle en ~/pgloader.log, corrige la causa y vuelve a ejecutar: gracias a include drop el resultado siempre parte de cero.

Paso 8: Verificar los datos

Compara el número exacto de filas de las tablas importantes en ambos motores. En MySQL:

mysql -h your_mysql_host -u pgloader -p -e "SELECT COUNT(*) FROM appdb.pedidos;"

En PostgreSQL:

psql "host=127.0.0.1 dbname=appdb user=appuser" -c "SELECT count(*) FROM pedidos;"

Revisa que los tipos se han convertido como esperabas:

psql "host=127.0.0.1 dbname=appdb user=appuser" -c "\d pedidos"

Comprueba también que las secuencias continúan desde el último identificador, insertando una fila de prueba en una tabla con clave autoincremental dentro de una transacción que después deshaces:

psql "host=127.0.0.1 dbname=appdb user=appuser" -c "BEGIN; INSERT INTO clientes (nombre) VALUES ('prueba') RETURNING id; ROLLBACK;"

El id devuelto debe ser mayor que el máximo existente. Ajusta la columna nombre a una de tus tablas.

Paso 9: Adaptar las consultas de la aplicación

Aunque los datos estén bien, la aplicación fallará si usa sintaxis propia de MySQL. Las diferencias más habituales:

MySQLPostgreSQL
IFNULL(a, b)COALESCE(a, b)
GROUP_CONCAT(x SEPARATOR ',')string_agg(x, ',')
LIMIT 10, 20LIMIT 20 OFFSET 10
`columna`"columna"
INSERT ... ON DUPLICATE KEY UPDATEINSERT ... ON CONFLICT (id) DO UPDATE SET ...
NOW() - INTERVAL 1 DAYnow() - interval '1 day'
Comparación de texto sin distinguir mayúsculasDistingue mayúsculas: usa ILIKE o lower()

Localiza estos patrones en el código con grep, por ejemplo desde la raíz del proyecto:

grep -rnE "IFNULL|GROUP_CONCAT|ON DUPLICATE KEY|LIMIT [0-9]+ *, *[0-9]+" --include="*.php" --include="*.py" --include="*.js" .

Si usas un ORM, cambiar el driver y la cadena de conexión suele bastar para la mayoría de consultas; céntrate en las consultas SQL escritas a mano. Después ejecuta la batería de pruebas de la aplicación contra la base de datos PostgreSQL.

Paso 10: Planificar el cambio y la vuelta atrás

Cuando las pruebas pasen, planifica el corte definitivo:

  1. Pon la aplicación en modo mantenimiento para que no haya escrituras en MySQL.
  2. Ejecuta de nuevo pgloader ~/appdb.load para cargar los datos finales.
  3. Repite las comprobaciones del paso 8.
  4. Cambia la cadena de conexión de la aplicación a PostgreSQL y sal del modo mantenimiento.

MySQL no se modifica en ningún momento, así que la vuelta atrás consiste en restaurar la cadena de conexión anterior. Esta opción deja de ser segura en cuanto la aplicación escribe datos nuevos en PostgreSQL, por lo que conviene decidir de antemano cuánto tiempo mantener MySQL como alternativa.

Solución de problemas

MySQL Error: Authentication plugin 'caching_sha2_password' is not supported. El usuario de MySQL usa el método de autenticación por defecto. Cámbialo con ALTER USER 'pgloader'@'your_server_ip' IDENTIFIED WITH mysql_native_password BY 'your_mysql_password';.

Heap exhausted, game over. pgLoader se queda sin memoria con tablas de filas muy grandes. Reduce el trabajo en paralelo y el número de filas en memoria añadiendo prefetch rows = 10000 a la cláusula WITH y bajando workers a 2.

Las tablas aparecen en un esquema llamado appdb. Falta la línea ALTER SCHEMA 'appdb' RENAME TO 'public'; o el nombre no coincide exactamente con el de la base de datos MySQL.

Caracteres extraños en textos con acentos. Alguna tabla de MySQL usa latin1 con datos que en realidad son UTF-8. Comprueba el juego de caracteres de cada tabla con SHOW CREATE TABLE y corrígelo en MySQL antes de repetir la migración.

Conclusión

Has migrado una base de datos MySQL a PostgreSQL con pgLoader, revisado los tipos y objetos que requieren atención manual, verificado los datos y preparado un corte con vuelta atrás. Como siguientes pasos, reescribe en PL/pgSQL los procedimientos y triggers que localizaste en el paso 2, configura copias de seguridad de PostgreSQL con pg_dump y revisa las consultas más lentas con la extensión pg_stat_statements.