MySQL y PostgreSQL son las dos bases de datos relacionales de código abierto más usadas. Las dos son fiables, soportan transacciones ACID y funcionan bien con cualquier framework moderno, pero difieren en el modelo de conexiones, en lo estrictas que son con los datos, en funciones SQL avanzadas y en cómo se replican. En esta guía verás esas diferencias con ejemplos que puedes ejecutar en Ubuntu 24.04 y un criterio práctico para elegir.

Resumen rápido

AspectoMySQL (InnoDB)PostgreSQL
Versión en Ubuntu 24.048.0 (paquete mysql-server)16 (paquete postgresql)
Modelo de conexionesUn hilo por conexiónUn proceso por conexión
Aislamiento por defectoREPEATABLE READREAD COMMITTED
Comparación de texto por defectoInsensible a mayúsculas (utf8mb4_0900_ai_ci)Sensible a mayúsculas
DDL dentro de transaccionesNo, cada ALTER hace commit implícitoSí, se puede deshacer con ROLLBACK
JSONTipo JSON, índices sobre columnas generadasJSON y JSONB con índices GIN
Vistas materializadasNoSí
RETURNINGNoSí
ExtensionesPlugins limitadosPostGIS, pg_trgm, TimescaleDB, pgvector y muchas más
ReplicaciónBinlog asíncrona o semisíncrona, Group ReplicationStreaming física y replicación lógica
MantenimientoMínimoVACUUM (automático con autovacuum)

Oracle dio por terminado el soporte de MySQL 8.0 en abril de 2026 y la rama LTS actual es la 8.4. Las diferencias de esta guía aplican igual a ambas.

MariaDB es un fork de MySQL con alta compatibilidad; Ubuntu 24.04 incluye MariaDB 10.11. Casi todo lo que se dice aquí de MySQL aplica también a MariaDB, aunque MariaDB sí soporta RETURNING en INSERT y DELETE.

Requisitos previos

Para probar los ejemplos necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM si instalas los dos motores a la vez.
  • Un usuario no root con privilegios sudo.

Instala ambos desde los repositorios de Ubuntu:

sudo apt update
sudo apt install mysql-server postgresql

Comprueba las versiones:

mysql --version
psql --version
mysql  Ver 8.0.43-0ubuntu0.24.04.1 for Linux on x86_64 ((Ubuntu))
psql (PostgreSQL) 16.10 (Ubuntu 16.10-0ubuntu0.24.04.1)

Los números de parche serán distintos en tu servidor. Crea una base de datos de pruebas en cada motor:

sudo mysql -e "CREATE DATABASE pruebas;"
sudo -u postgres createdb pruebas

Abre una consola en cada uno con sudo mysql pruebas y sudo -u postgres psql pruebas.

Arquitectura y conexiones

MySQL crea un hilo por conexión dentro de un único proceso mysqld. Aguanta cientos o miles de conexiones con un coste de memoria moderado, lo que encaja con aplicaciones PHP que abren una conexión por petición.

PostgreSQL crea un proceso del sistema operativo por conexión. Cada proceso es más pesado, y por encima de unos pocos cientos de conexiones activas el rendimiento cae. La solución estándar es poner un pool de conexiones delante, como PgBouncer:

sudo apt install pgbouncer

Comprueba el límite configurado en cada motor:

sudo mysql -e "SHOW VARIABLES LIKE 'max_connections';"
sudo -u postgres psql -c "SHOW max_connections;"
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 151   |
+-----------------+-------+
 max_connections
-----------------
 100
(1 row)

Si tu aplicación usa un pool propio (la mayoría de frameworks en Java, Go, Python o Node.js lo hacen), esta diferencia importa mucho menos.

Diferencias de comportamiento que afectan a tu aplicación

Mayúsculas y minúsculas en comparaciones

Con la collation por defecto de MySQL 8.0, 'Ana' = 'ana' es verdadero. En PostgreSQL es falso. Pruébalo en cada consola:

SELECT 'Ana' = 'ana';

MySQL devuelve 1 y PostgreSQL devuelve f. Esto afecta a restricciones UNIQUE sobre emails o nombres de usuario: en PostgreSQL se suele resolver con un índice sobre lower(email) o con la extensión citext.

DDL transaccional

En PostgreSQL una migración fallida se puede deshacer entera:

BEGIN;
CREATE TABLE clientes (id serial PRIMARY KEY, email text NOT NULL);
ALTER TABLE clientes ADD COLUMN pais char(2);
ROLLBACK;

Tras el ROLLBACK la tabla no existe. En MySQL cada CREATE o ALTER hace commit implícito, así que una migración que falla a mitad deja la base de datos en un estado intermedio que hay que arreglar a mano.

Insertar o actualizar (upsert)

La sintaxis es distinta. En MySQL:

CREATE TABLE contadores (clave varchar(50) PRIMARY KEY, valor int NOT NULL);
INSERT INTO contadores (clave, valor) VALUES ('visitas', 1)
  ON DUPLICATE KEY UPDATE valor = valor + 1;

En PostgreSQL, que además devuelve la fila con RETURNING:

CREATE TABLE contadores (clave varchar(50) PRIMARY KEY, valor int NOT NULL);
INSERT INTO contadores (clave, valor) VALUES ('visitas', 1)
  ON CONFLICT (clave) DO UPDATE SET valor = contadores.valor + 1
  RETURNING valor;

PostgreSQL 15 y posteriores también soportan MERGE del estándar SQL.

SQL avanzado

Desde MySQL 8.0 la distancia se ha reducido mucho. Ambos soportan:

  • Expresiones de tabla comunes (WITH), incluidas las recursivas.
  • Funciones de ventana (ROW_NUMBER(), RANK(), LAG(), SUM() OVER (...)).
  • Restricciones CHECK (MySQL desde 8.0.16).

Esta consulta funciona igual en los dos:

WITH ventas AS (
  SELECT 'norte' AS zona, 120 AS importe UNION ALL
  SELECT 'norte', 80 UNION ALL
  SELECT 'sur', 200
)
SELECT zona, importe,
       SUM(importe) OVER (PARTITION BY zona) AS total_zona
FROM ventas;

Lo que solo tiene PostgreSQL:

  • Vistas materializadas (CREATE MATERIALIZED VIEW y REFRESH MATERIALIZED VIEW CONCURRENTLY).
  • Índices parciales (CREATE INDEX ... WHERE activo) e índices de expresión sobre cualquier función.
  • Tipos nativos de arrays, rangos (tstzrange), inet/cidr y tipos definidos por el usuario.
  • Restricciones de exclusión, por ejemplo para impedir reservas que se solapen en el tiempo.
  • Un ecosistema de extensiones que se activan con CREATE EXTENSION.

JSON

Los dos almacenan JSON validado, pero PostgreSQL lo indexa de forma más flexible. En PostgreSQL con JSONB y un índice GIN puedes buscar por cualquier clave:

CREATE TABLE eventos (id bigserial PRIMARY KEY, datos jsonb NOT NULL);
CREATE INDEX eventos_datos_idx ON eventos USING gin (datos);
INSERT INTO eventos (datos) VALUES ('{"tipo": "login", "usuario": 42}');
SELECT id FROM eventos WHERE datos @> '{"tipo": "login"}';

En MySQL no se indexa la columna JSON directamente: se crea una columna generada con la ruta concreta y se indexa esa columna:

CREATE TABLE eventos (
  id bigint AUTO_INCREMENT PRIMARY KEY,
  datos json NOT NULL,
  tipo varchar(20) AS (JSON_UNQUOTE(JSON_EXTRACT(datos, '$.tipo'))) STORED,
  INDEX (tipo)
);
INSERT INTO eventos (datos) VALUES ('{"tipo": "login", "usuario": 42}');
SELECT id FROM eventos WHERE tipo = 'login';

Funciona bien cuando sabes de antemano qué claves vas a consultar. Si las consultas sobre JSON son variadas, PostgreSQL es claramente mejor.

Replicación y alta disponibilidad

MySQL replica mediante el binary log. Una réplica se configura con CHANGE REPLICATION SOURCE TO y START REPLICA, y puede ser asíncrona o semisíncrona. Para alta disponibilidad con failover automático existe Group Replication, que junto con MySQL Router y MySQL Shell forma InnoDB Cluster.

PostgreSQL ofrece replicación física por streaming (una copia exacta del servidor, creada con pg_basebackup) y replicación lógica por publicaciones y suscripciones, útil para replicar solo algunas tablas o migrar entre versiones mayores. El failover automático no viene incluido: se usa una herramienta externa como Patroni.

Ambos soportan réplicas de lectura de forma fiable. La diferencia está en las herramientas: MySQL trae más piezas oficiales y PostgreSQL depende más del ecosistema.

Mantenimiento: VACUUM en PostgreSQL

PostgreSQL implementa MVCC guardando versiones antiguas de las filas en la propia tabla. VACUUM recupera ese espacio y el proceso autovacuum lo ejecuta automáticamente. En tablas con muchas actualizaciones conviene vigilarlo:

SELECT relname, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 5;

Si n_dead_tup crece sin parar y last_autovacuum está vacío o es muy antiguo, hay que ajustar autovacuum para esa tabla. InnoDB gestiona las versiones antiguas en el undo log y no requiere esta atención.

Ajuste inicial

El parámetro más importante de cada motor es la memoria de caché.

En MySQL, crea /etc/mysql/mysql.conf.d/zz-tuning.cnf. En un servidor dedicado se suele dar entre el 50 y el 70 % de la RAM al buffer pool; con 4 GB:

[mysqld]
innodb_buffer_pool_size = 2G

En PostgreSQL 16, edita /etc/postgresql/16/main/postgresql.conf. La recomendación habitual es un 25 % de la RAM para shared_buffers:

shared_buffers = 1GB
effective_cache_size = 3GB

Reinicia cada servicio y comprueba que el valor se aplicó:

sudo systemctl restart mysql postgresql
sudo mysql -e "SELECT @@innodb_buffer_pool_size/1024/1024/1024 AS gb;"
sudo -u postgres psql -c "SHOW shared_buffers;"

Cuál elegir

Elige MySQL cuando:

  • Usas software que lo trae como opción principal: WordPress, Magento, PrestaShop, Nextcloud o muchos paneles de hosting.
  • La carga es sobre todo de lecturas simples por clave primaria y muchas conexiones cortas.
  • Tu equipo ya lo opera y conoce sus herramientas de replicación.

Elige PostgreSQL cuando:

  • Empiezas un proyecto nuevo con Django, Rails, Laravel, Spring o Node.js y no hay una razón concreta para MySQL.
  • Necesitas JSON consultable, datos geográficos (PostGIS), búsqueda de texto con pg_trgm, vectores con pgvector o series temporales.
  • Te importa la integridad: DDL transaccional, restricciones avanzadas y un comportamiento más estricto con los tipos.

Evalúa con más calma cuando:

  • Migras una aplicación grande: el coste está en reescribir consultas, collations y procedimientos almacenados, no en el motor.
  • Vas a depender de un servicio gestionado concreto: revisa qué versiones y extensiones ofrece.

Conclusión

Para un proyecto nuevo sin restricciones, PostgreSQL es la opción más completa y la que menos se queda corta cuando la aplicación crece. MySQL sigue siendo la elección natural para el ecosistema PHP y las aplicaciones que ya lo usan, y con la versión 8 ofrece CTEs y funciones de ventana. Como siguientes pasos, configura copias de seguridad con mysqldump o pg_dump, protege el acceso remoto con el firewall y prueba tu aplicación real sobre el motor elegido antes de decidir por benchmarks genéricos.