MySQL sigue siendo en 2026 una de las bases de datos relacionales más usadas del mundo. Desde startups hasta gigantes como Facebook, Twitter o YouTube han confiado en MySQL para gestionar millones de registros. Si quieres trabajar en desarrollo web, conocer MySQL no es opcional: es imprescindible. En esta guía completa aprenderás MySQL desde cero, con ejemplos reales y todo en español.

¿Qué es MySQL y por qué aprenderlo?

MySQL es un sistema de gestión de bases de datos relacionales (RDBMS) de código abierto basado en SQL (Structured Query Language). Fue creado en 1995 y actualmente es mantenido por Oracle. Es el componente «M» del famoso stack LAMP (Linux, Apache, MySQL, PHP) y del stack MEAN/MERN cuando se usa con Node.js.

Las razones para aprender MySQL en 2026 son claras: es gratuito y de código abierto, tiene una comunidad enorme, hay documentación abundante en español, y prácticamente todos los proyectos web que usan bases de datos relacionales lo soportan. Además, los conocimientos de MySQL son transferibles a otros sistemas como PostgreSQL o MariaDB.

Instalación de MySQL paso a paso

La forma más sencilla de instalar MySQL en local es usando MySQL Workbench junto con el instalador oficial de MySQL. Descárgalo desde mysql.com. Para macOS, también puedes usar Homebrew:

brew install mysql
brew services start mysql

# Acceder a MySQL desde terminal
mysql -u root -p

En Windows, el instalador de MySQL Community Edition instala todo lo necesario: el servidor MySQL, MySQL Workbench (interfaz gráfica) y MySQL Shell. Para desarrollo con Docker, la opción más cómoda es usar una imagen oficial:

docker run -d   --name mysql-dev   -e MYSQL_ROOT_PASSWORD=tu_password   -e MYSQL_DATABASE=mi_proyecto   -p 3306:3306   mysql:8.0

Conceptos fundamentales de bases de datos relacionales

Antes de escribir SQL, necesitas entender cómo funcionan las bases de datos relacionales. Los conceptos clave son:

Creando tu primera base de datos y tablas

Vamos a construir una base de datos real para una tienda online. Primero, creamos la base de datos y seleccionamos la misma:

-- Crear la base de datos
CREATE DATABASE tienda_online
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

-- Seleccionar la base de datos
USE tienda_online;

Ahora creamos las tablas. Usaremos el principio de normalización para separar los datos correctamente:

-- Tabla de categorías
CREATE TABLE categorias (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nombre VARCHAR(100) NOT NULL,
  descripcion TEXT,
  creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabla de productos
CREATE TABLE productos (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nombre VARCHAR(200) NOT NULL,
  descripcion TEXT,
  precio DECIMAL(10, 2) NOT NULL,
  stock INT DEFAULT 0,
  categoria_id INT,
  imagen_url VARCHAR(500),
  activo BOOLEAN DEFAULT TRUE,
  creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  actualizado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (categoria_id) REFERENCES categorias(id) ON DELETE SET NULL,
  INDEX idx_categoria (categoria_id),
  INDEX idx_activo (activo)
);

-- Tabla de clientes
CREATE TABLE clientes (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nombre VARCHAR(100) NOT NULL,
  apellidos VARCHAR(100) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE,
  telefono VARCHAR(20),
  fecha_registro TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tabla de pedidos
CREATE TABLE pedidos (
  id INT AUTO_INCREMENT PRIMARY KEY,
  cliente_id INT NOT NULL,
  total DECIMAL(10, 2) NOT NULL,
  estado ENUM('pendiente', 'pagado', 'enviado', 'entregado', 'cancelado') DEFAULT 'pendiente',
  direccion_envio TEXT,
  creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (cliente_id) REFERENCES clientes(id)
);

-- Tabla intermedia: líneas de pedido
CREATE TABLE lineas_pedido (
  id INT AUTO_INCREMENT PRIMARY KEY,
  pedido_id INT NOT NULL,
  producto_id INT NOT NULL,
  cantidad INT NOT NULL DEFAULT 1,
  precio_unitario DECIMAL(10, 2) NOT NULL,
  FOREIGN KEY (pedido_id) REFERENCES pedidos(id) ON DELETE CASCADE,
  FOREIGN KEY (producto_id) REFERENCES productos(id)
);

Operaciones CRUD: el corazón de SQL

INSERT: Insertar datos

-- Insertar categorías
INSERT INTO categorias (nombre, descripcion) VALUES
  ('Electrónica', 'Dispositivos y gadgets tecnológicos'),
  ('Programación', 'Libros y cursos de desarrollo'),
  ('Hardware', 'Componentes y periféricos informáticos');

-- Insertar un producto
INSERT INTO productos (nombre, descripcion, precio, stock, categoria_id)
VALUES (
  'Teclado mecánico TKL',
  'Teclado mecánico tenkeyless con switches Cherry MX Red',
  89.99,
  50,
  3
);

-- Insertar múltiples filas de una vez
INSERT INTO clientes (nombre, apellidos, email) VALUES
  ('Ada', 'Lovelace', 'ada@ejemplo.com'),
  ('Alan', 'Turing', 'alan@ejemplo.com'),
  ('Linus', 'Torvalds', 'linus@ejemplo.com');

SELECT: Consultar datos

El SELECT es la sentencia más importante de SQL. Permite recuperar datos de una o varias tablas con múltiples filtros y condiciones:

-- Seleccionar todos los productos activos
SELECT * FROM productos WHERE activo = TRUE;

-- Seleccionar campos específicos con alias
SELECT
  p.nombre AS producto,
  p.precio,
  p.stock,
  c.nombre AS categoria
FROM productos p
INNER JOIN categorias c ON p.categoria_id = c.id
WHERE p.activo = TRUE AND p.stock > 0
ORDER BY p.precio ASC
LIMIT 10;

-- Buscar productos por nombre (búsqueda parcial)
SELECT * FROM productos
WHERE nombre LIKE '%teclado%';

-- Contar productos por categoría
SELECT
  c.nombre AS categoria,
  COUNT(p.id) AS total_productos,
  AVG(p.precio) AS precio_medio,
  MIN(p.precio) AS precio_minimo,
  MAX(p.precio) AS precio_maximo
FROM categorias c
LEFT JOIN productos p ON c.id = p.categoria_id
GROUP BY c.id, c.nombre
HAVING total_productos > 0
ORDER BY total_productos DESC;

UPDATE y DELETE: Modificar y eliminar

-- Actualizar el precio de un producto
UPDATE productos
SET precio = 79.99, actualizado_en = CURRENT_TIMESTAMP
WHERE id = 1;

-- Aplicar descuento del 10% a todos los productos de electrónica
UPDATE productos p
INNER JOIN categorias c ON p.categoria_id = c.id
SET p.precio = ROUND(p.precio * 0.90, 2)
WHERE c.nombre = 'Electrónica';

-- Desactivar productos sin stock (en lugar de borrar)
UPDATE productos SET activo = FALSE WHERE stock = 0;

-- Eliminar con seguridad (primero verificas)
SELECT * FROM pedidos WHERE estado = 'cancelado' AND creado_en < '2025-01-01';

-- Luego eliminas
DELETE FROM pedidos
WHERE estado = 'cancelado' AND creado_en < '2025-01-01';

JOINs: combinando múltiples tablas

Los JOINs son una de las características más poderosas de SQL. Permiten combinar datos de múltiples tablas en una sola consulta:

-- INNER JOIN: solo registros que tienen coincidencia en ambas tablas
SELECT
  p.id,
  c.nombre AS cliente,
  c.email,
  p.total,
  p.estado,
  p.creado_en
FROM pedidos p
INNER JOIN clientes c ON p.cliente_id = c.id
WHERE p.estado = 'pendiente'
ORDER BY p.creado_en DESC;

-- LEFT JOIN: todos los clientes, tengan pedidos o no
SELECT
  c.nombre,
  c.email,
  COUNT(p.id) AS total_pedidos,
  COALESCE(SUM(p.total), 0) AS total_gastado
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.id, c.nombre, c.email
ORDER BY total_gastado DESC;

-- Consulta compleja: detalle completo de un pedido
SELECT
  p.id AS pedido_id,
  CONCAT(c.nombre, ' ', c.apellidos) AS cliente,
  pr.nombre AS producto,
  lp.cantidad,
  lp.precio_unitario,
  (lp.cantidad * lp.precio_unitario) AS subtotal,
  p.estado
FROM pedidos p
INNER JOIN clientes c ON p.cliente_id = c.id
INNER JOIN lineas_pedido lp ON p.id = lp.pedido_id
INNER JOIN productos pr ON lp.producto_id = pr.id
WHERE p.id = 1;

Funciones y operaciones avanzadas

Funciones de agregación

-- Estadísticas de ventas por mes
SELECT
  YEAR(creado_en) AS anyo,
  MONTH(creado_en) AS mes,
  COUNT(*) AS total_pedidos,
  SUM(total) AS ingresos,
  AVG(total) AS ticket_medio
FROM pedidos
WHERE estado != 'cancelado'
GROUP BY YEAR(creado_en), MONTH(creado_en)
ORDER BY anyo DESC, mes DESC;

-- Top 5 productos más vendidos
SELECT
  pr.nombre,
  SUM(lp.cantidad) AS unidades_vendidas,
  SUM(lp.cantidad * lp.precio_unitario) AS ingresos_totales
FROM lineas_pedido lp
INNER JOIN productos pr ON lp.producto_id = pr.id
GROUP BY pr.id, pr.nombre
ORDER BY unidades_vendidas DESC
LIMIT 5;

Subqueries y CTEs

Las subqueries y los CTEs (Common Table Expressions) permiten escribir consultas más legibles y reutilizables:

-- CTE: clientes que han gastado más del promedio
WITH estadisticas_clientes AS (
  SELECT
    cliente_id,
    SUM(total) AS total_gastado
  FROM pedidos
  WHERE estado = 'entregado'
  GROUP BY cliente_id
),
promedio AS (
  SELECT AVG(total_gastado) AS media FROM estadisticas_clientes
)
SELECT
  c.nombre,
  c.email,
  ec.total_gastado
FROM clientes c
INNER JOIN estadisticas_clientes ec ON c.id = ec.cliente_id
CROSS JOIN promedio p
WHERE ec.total_gastado > p.media
ORDER BY ec.total_gastado DESC;

Índices y optimización de rendimiento

Los índices son la herramienta más importante para optimizar el rendimiento de MySQL. Sin índices, MySQL hace un "full table scan" en cada consulta, lo que es muy lento para tablas grandes:

-- Ver índices existentes de una tabla
SHOW INDEX FROM productos;

-- Crear un índice en la columna precio (para ordenaciones y rangos)
CREATE INDEX idx_precio ON productos(precio);

-- Índice compuesto: útil cuando siempre filtras por ambas columnas
CREATE INDEX idx_categoria_activo ON productos(categoria_id, activo);

-- Índice de texto completo: para búsquedas de texto
CREATE FULLTEXT INDEX idx_busqueda ON productos(nombre, descripcion);

-- Usar el índice fulltext
SELECT * FROM productos
WHERE MATCH(nombre, descripcion) AGAINST ('teclado mecánico' IN BOOLEAN MODE);

-- Analizar el rendimiento de una consulta
EXPLAIN SELECT * FROM productos WHERE categoria_id = 1 AND activo = TRUE;

MySQL con Node.js: conexión y consultas

En la práctica, casi siempre accederás a MySQL desde tu aplicación. Con Node.js, el paquete más popular es mysql2, que soporta promesas y prepared statements:

npm install mysql2
// db.js - Configuración de la conexión con pool
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: process.env.DB_HOST || 'localhost',
  user: process.env.DB_USER || 'root',
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME || 'tienda_online',
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
  charset: 'utf8mb4'
});

module.exports = pool;

// productos.service.js - Consultas con prepared statements
const db = require('./db');

async function obtenerProductos(categoriaId, limite = 20) {
  const [rows] = await db.query(
    'SELECT p.*, c.nombre AS categoria FROM productos p LEFT JOIN categorias c ON p.categoria_id = c.id WHERE p.activo = ? AND (? IS NULL OR p.categoria_id = ?) LIMIT ?',
    [true, categoriaId, categoriaId, limite]
  );
  return rows;
}

async function crearProducto({ nombre, descripcion, precio, stock, categoriaId }) {
  const [result] = await db.query(
    'INSERT INTO productos (nombre, descripcion, precio, stock, categoria_id) VALUES (?, ?, ?, ?, ?)',
    [nombre, descripcion, precio, stock, categoriaId]
  );
  return { id: result.insertId, nombre, precio, stock };
}

async function actualizarStock(id, cantidad) {
  await db.query(
    'UPDATE productos SET stock = stock + ? WHERE id = ?',
    [cantidad, id]
  );
}

module.exports = { obtenerProductos, crearProducto, actualizarStock };

MySQL vs PostgreSQL: ¿cuál elegir en 2026?

CaracterísticaMySQLPostgreSQL
Velocidad de lectura⭐⭐⭐⭐⭐ Excelente⭐⭐⭐⭐ Muy buena
Soporte JSON avanzado⭐⭐⭐ Básico⭐⭐⭐⭐⭐ Completo (JSONB)
Cumplimiento SQL estándar⭐⭐⭐ Parcial⭐⭐⭐⭐⭐ Completo
Facilidad de aprendizaje⭐⭐⭐⭐⭐ Muy fácil⭐⭐⭐⭐ Fácil
Hosting disponible⭐⭐⭐⭐⭐ Universal⭐⭐⭐⭐⭐ Universal
Uso en el mundo⭐⭐⭐⭐⭐ Muy amplio⭐⭐⭐⭐ Amplio y creciendo

Para la mayoría de proyectos web en 2026, ambas son excelentes opciones. MySQL sigue siendo la elección más común por su velocidad de lectura y la enorme cantidad de hosting disponible. PostgreSQL es preferible cuando necesitas JSON avanzado, tipos de datos complejos o un cumplimiento SQL más estricto.

Buenas prácticas de seguridad en MySQL

  1. Usa siempre prepared statements: Nunca concatenes variables directamente en las consultas SQL. Es la principal protección contra SQL Injection.
  2. Crea usuarios con privilegios mínimos: No uses el usuario root en tu aplicación. Crea un usuario con solo los permisos que necesita (SELECT, INSERT, UPDATE, DELETE).
  3. Cifra las contraseñas: Nunca guardes contraseñas en texto plano. Usa bcrypt o Argon2.
  4. Haz copias de seguridad regularmente: Con mysqldump o soluciones automáticas del hosting.
  5. No expongas MySQL a internet: El puerto 3306 no debe ser accesible públicamente. Usa conexiones SSH o VPN.

Conclusión

MySQL es una base de datos madura, potente y ampliamente soportada que cualquier desarrollador web debería conocer. Desde las operaciones CRUD básicas hasta los JOINs complejos, los índices y la integración con Node.js, este artículo te ha dado las bases para trabajar con MySQL de forma profesional.

El siguiente paso es practicar con proyectos reales. Descarga MySQL Workbench, crea la base de datos de la tienda online de este tutorial y experimenta con las consultas. Cuanto más escribas SQL, más natural se volverá. ¿Tienes alguna pregunta sobre MySQL? Déjala en los comentarios.