MySQL ha servido bien a muchas empresas, pero llega un punto donde sus limitaciones empiezan a pesar — el manejo de JSON es más limitado que JSONB de PostgreSQL, las extensiones son escasas, la replicación tiene más restricciones y el cumplimiento del estándar SQL no es tan estricto. Si tu aplicación está creciendo y necesitas más potencia, flexibilidad y un camino claro de escalamiento, PostgreSQL es el siguiente paso natural.
En esta guía vas a migrar una base de datos MySQL completa a PostgreSQL usando pgloader — desde la planificación hasta la validación de que no se perdió ni un registro.
Antes de empezar: ¿realmente necesitas migrar?
No toda base de datos MySQL necesita migrar. Evalúa si te aplica alguno de estos escenarios:
- Tu aplicación necesita JSONB para consultas complejas sobre datos semi-estructurados
- Quieres extensiones como PostGIS (geoespacial), pg_trgm (búsqueda fuzzy) o pgvector (embeddings IA)
- La replicación streaming de PostgreSQL se adapta mejor a tus necesidades de alta disponibilidad
- Quieres eliminar costos de licencia (MySQL Enterprise) o restricciones del modelo Oracle
- Tu equipo de desarrollo prefiere PostgreSQL y la aplicación no tiene dependencias duras con MySQL
Si tu aplicación funciona bien con MySQL y no necesitas nada de lo anterior, optimizar MySQL con nuestro servicio de bases de datos puede ser más práctico que migrar.
Plan de migración
Una migración exitosa tiene 5 fases. No saltes ninguna:
| Fase | Descripción | Duración típica |
|---|---|---|
| 1. Análisis | Inventario de tablas, tipos, funciones, triggers y queries específicas de MySQL | 2-3 días |
| 2. Migración de esquema y datos | Conversión con pgloader + ajustes manuales | 1-2 días |
| 3. Ajuste de aplicación | Reescribir queries incompatibles, ORM config, funciones | 3-10 días |
| 4. Pruebas | Regresión completa, validación de datos, rendimiento | 3-5 días |
| 5. Corte | Ventana de mantenimiento, migración final, switch | 1 día |
Paso 1: Análisis de compatibilidad
Antes de tocar pgloader, analiza qué va a necesitar ajustes.
Diferencias de tipos de datos
| MySQL | PostgreSQL | Notas |
|---|---|---|
TINYINT(1) | BOOLEAN | pgloader lo convierte automáticamente |
INT AUTO_INCREMENT | SERIAL o GENERATED ALWAYS AS IDENTITY | Conversión automática |
DATETIME | TIMESTAMP | Compatible |
DOUBLE | DOUBLE PRECISION | Compatible |
ENUM('a','b','c') | TEXT + CHECK constraint | Requiere atención |
SET('x','y') | TEXT[] (array) | Requiere conversión manual |
BLOB / LONGBLOB | BYTEA | Conversión automática |
JSON | JSONB | Recomendado cambiar a JSONB |
Queries que necesitan ajuste
-- MySQL: comillas invertidas para identificadores
SELECT `order`.`id` FROM `order`;
-- PostgreSQL: comillas dobles (o sin comillas si no es palabra reservada)
SELECT "order"."id" FROM "order";
-- MySQL: LIMIT con OFFSET
SELECT * FROM productos LIMIT 10, 20;
-- PostgreSQL
SELECT * FROM productos LIMIT 20 OFFSET 10;
-- MySQL: GROUP_CONCAT
SELECT GROUP_CONCAT(nombre SEPARATOR ', ') FROM tags;
-- PostgreSQL
SELECT STRING_AGG(nombre, ', ') FROM tags;
-- MySQL: IF()
SELECT IF(stock > 0, 'Disponible', 'Agotado') FROM productos;
-- PostgreSQL
SELECT CASE WHEN stock > 0 THEN 'Disponible' ELSE 'Agotado' END FROM productos;
-- MySQL: IFNULL
SELECT IFNULL(telefono, 'Sin teléfono') FROM clientes;
-- PostgreSQL
SELECT COALESCE(telefono, 'Sin teléfono') FROM clientes;
-- MySQL: NOW() retorna DATETIME
-- PostgreSQL: NOW() retorna TIMESTAMP WITH TIME ZONE (más correcto)
Busca estos patrones en tu código antes de migrar. Si usas un ORM como SQLAlchemy, Prisma o TypeORM, la mayoría de estos ajustes se manejan cambiando el driver de conexión.
Paso 2: Instalar pgloader
pgloader es la herramienta estándar para migrar de MySQL a PostgreSQL. Lee directamente de MySQL, convierte tipos automáticamente y carga en PostgreSQL:
sudo apt install pgloader -y
pgloader --version
Paso 3: Preparar PostgreSQL destino
Crea la base de datos de destino vacía:
sudo -u postgres psql
CREATE USER app WITH PASSWORD 'contraseña_segura';
CREATE DATABASE miapp_pg OWNER app;
\q
Paso 4: Migrar con pgloader
Crea el archivo de comandos de pgloader:
cat > migracion.load <<'EOF'
LOAD DATABASE
FROM mysql://root:mysql_password@localhost:3306/miapp_mysql
INTO postgresql://app:contraseña_segura@localhost:5432/miapp_pg
WITH include drop,
create tables,
create indexes,
reset sequences,
downcase identifiers,
batch rows = 1000,
prefetch rows = 10000
SET maintenance_work_mem TO '512MB',
work_mem TO '128MB'
-- Conversiones personalizadas
CAST type tinyint to boolean using tinyint-to-boolean,
type int with extra auto_increment to serial,
type bigint with extra auto_increment to bigserial
-- Excluir tablas que no necesitas migrar (sesiones, cache, etc.)
EXCLUDING TABLE NAMES MATCHING 'sessions', 'cache', 'migrations'
BEFORE LOAD DO
$$ CREATE SCHEMA IF NOT EXISTS public; $$;
EOF
Ejecuta la migración:
pgloader migracion.load
pgloader muestra el progreso tabla por tabla con conteo de filas, errores y tiempo. Una migración típica de 10 GB toma 5-15 minutos.
Paso 5: Validar la migración
Conteo de registros
Compara el número de registros en cada tabla entre MySQL y PostgreSQL:
# Script de validación
cat > validar.py <<'PYTHON'
import mysql.connector
import psycopg2
# Conexiones
my = mysql.connector.connect(host='localhost', user='root', password='mysql_password', database='miapp_mysql')
pg = psycopg2.connect(host='localhost', user='app', password='contraseña_segura', dbname='miapp_pg')
my_cur = my.cursor()
pg_cur = pg.cursor()
# Obtener tablas de MySQL
my_cur.execute("SHOW TABLES")
tables = [t[0] for t in my_cur.fetchall()]
print(f"{'Tabla':<30} {'MySQL':>10} {'PostgreSQL':>12} {'OK':>5}")
print("-" * 60)
errors = 0
for table in tables:
try:
my_cur.execute(f"SELECT COUNT(*) FROM `{table}`")
my_count = my_cur.fetchone()[0]
pg_cur.execute(f"SELECT COUNT(*) FROM {table.lower()}")
pg_count = pg_cur.fetchone()[0]
ok = "✓" if my_count == pg_count else "✗"
if my_count != pg_count:
errors += 1
print(f"{table:<30} {my_count:>10} {pg_count:>12} {ok:>5}")
except Exception as e:
print(f"{table:<30} {'ERROR':>10} {str(e)[:20]:>12}")
errors += 1
print(f"\nResultado: {len(tables) - errors}/{len(tables)} tablas coinciden")
my.close()
pg.close()
PYTHON
python3 validar.py
Verificar secuencias
pgloader resetea las secuencias automáticamente, pero verifica:
-- En PostgreSQL: verificar que las secuencias están al valor correcto
SELECT schemaname, sequencename, last_value
FROM pg_sequences
WHERE schemaname = 'public';
Verificar constraints e índices
-- Constraints
SELECT table_name, constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_schema = 'public'
ORDER BY table_name;
-- Índices
SELECT tablename, indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public'
ORDER BY tablename;
Paso 6: Ajustar la aplicación
Cambiar el driver de conexión
Si tu aplicación usa un ORM, el cambio suele ser solo la cadena de conexión:
# SQLAlchemy — antes (MySQL)
DATABASE_URL = "mysql+pymysql://root:password@localhost/miapp"
# SQLAlchemy — después (PostgreSQL)
DATABASE_URL = "postgresql+psycopg2://app:password@localhost/miapp_pg"
// Prisma — schema.prisma
datasource db {
// Antes
// provider = "mysql"
// Después
provider = "postgresql"
url = env("DATABASE_URL")
}
Ajustar queries raw
Si tienes queries SQL escritas directamente (no via ORM), revisa los patrones que listamos en el Paso 1 y reescríbelos. Los más comunes:
- Comillas invertidas → comillas dobles o sin comillas
GROUP_CONCAT→STRING_AGGIFNULL→COALESCELIMIT offset, count→LIMIT count OFFSET offsetNOW()devuelve timestamp con timezone en PostgreSQL
Paso 7: Pruebas de regresión
Antes de cortar a producción, ejecuta:
- Suite de tests — si tienes tests automatizados, ejecútalos contra PostgreSQL
- Pruebas manuales — recorre los flujos principales de la aplicación (crear registro, editar, buscar, reportes)
- Pruebas de rendimiento — compara tiempos de consultas críticas entre MySQL y PostgreSQL. PostgreSQL puede ser más rápido o más lento dependiendo de las queries — optimiza con
EXPLAIN ANALYZE - Prueba de backup — verifica que puedes respaldar y restaurar la base de datos PostgreSQL
Paso 8: Corte a producción
Cuando las pruebas pasan, programa la ventana de mantenimiento:
# 1. Poner la aplicación en mantenimiento
# 2. Exportar datos frescos de MySQL de producción
mysqldump --single-transaction -u root -p miapp > produccion_final.sql
# 3. Migrar con pgloader (usando los datos de producción)
pgloader migracion.load
# 4. Validar conteos
python3 validar.py
# 5. Cambiar la cadena de conexión en la aplicación
# 6. Reiniciar la aplicación apuntando a PostgreSQL
# 7. Verificar que todo funciona
# 8. Quitar modo mantenimiento
Mantén MySQL disponible
No apagues MySQL inmediatamente después del corte. Mantenlo en solo lectura durante 1-2 semanas como plan de rollback. Si algo falla en PostgreSQL que no detectaste en pruebas, puedes volver a MySQL rápidamente.
Siguientes pasos
Con la migración completada:
- Optimizar PostgreSQL — ajustar
shared_buffers,work_memy crear índices específicos para tus queries más frecuentes - Replicación — configurar alta disponibilidad con streaming replication
- Extensiones — aprovechar JSONB, pg_trgm para búsqueda fuzzy, PostGIS para datos geoespaciales
- Monitoreo — dashboards de rendimiento de PostgreSQL con postgres_exporter
- Bases de datos administradas — nuestro servicio DBA para que tu PostgreSQL siempre esté optimizado y protegido
Migración profesional
¿Necesitas migrar de MySQL a PostgreSQL sin riesgos?
Planificamos y ejecutamos la migración completa con validación de datos, ajuste de queries y acompañamiento post-migración.



