PHP & Python SKool · infodocencia.net

Manual Pareto de MySQL & MariaDB

El 20% de SQL que resuelve el 80% de los problemas reales en un servidor MySQL/MariaDB de hosting compartido — pensado para seguir justo después de las lecciones de SQLite del curso.

← Volver al curso
Cómo usar este manual: no es un curso, es una chuleta de referencia. Practicaste DDL y DQL en SQLite dentro del curso; aquí tienes exactamente lo que cambia en MySQL/MariaDB real, más lo esencial que no cabía en las lecciones (usuarios, backups, transacciones, índices). Con esto ya puedes trabajar en un hosting real y buscar el resto según lo necesites — esa es la parte "modo avanzado".
01

MySQL/MariaDB vs SQLite: qué cambia

MariaDB es un fork de MySQL prácticamente compatible al 100% en el SQL cotidiano — todo lo de aquí sirve para ambos salvo que se diga lo contrario. La diferencia real está frente a SQLite:

ConceptoSQLite (curso)MySQL/MariaDB (hosting real)
ArquitecturaFichero embebido, sin servidorServidor cliente-servidor, con usuario/contraseña
AutoincrementoINTEGER PRIMARY KEY (autoincrementa solo)INT AUTO_INCREMENT PRIMARY KEY
Tipos de textoTEXT para casi todoVARCHAR(n), TEXT, CHAR(n) según tamaño
FechasTEXT con formato ISODATE, DATETIME, TIMESTAMP nativos
Motor de almacenamientoÚnico, integradoInnoDB (transaccional, recomendado) o MyISAM (más simple, sin transacciones)
Identificadores con espaciosComillas dobles o [ ]Backticks: `nombre columna`
BooleanosNo existen, se usa 0/1BOOLEAN = alias de TINYINT(1)
Concatenar textoa || bCONCAT(a, b)

El SELECT, WHERE, JOIN, GROUP BY que ya conoces del curso funcionan exactamente igual en los dos.

02

Conectarse y explorar la base de datos

En un hosting compartido normalmente accedes por phpMyAdmin (interfaz web) o por línea de comandos si tienes SSH. Los comandos de exploración son iguales en ambos casos:

-- Ver todas las bases de datos disponibles
SHOW DATABASES;

-- Seleccionar una base de datos para trabajar en ella
USE nombre_basedatos;

-- Ver las tablas de la base de datos activa
SHOW TABLES;

-- Ver la estructura de una tabla (columnas, tipos, claves)
DESCRIBE libros;
-- o de forma equivalente:
SHOW COLUMNS FROM libros;

-- Ver el SQL exacto con el que se creó una tabla
SHOW CREATE TABLE libros;

Desde PHP te conectas con PDO (recomendado) o mysqli: new PDO("mysql:host=localhost;dbname=midb", $usuario, $clave).

03

DDL — crear y modificar tablas

Igual que en SQLite, pero con tipos más específicos y con ENGINE=InnoDB explícito (para tener transacciones y claves foráneas de verdad):

CREATE TABLE libros (
  id           INT AUTO_INCREMENT PRIMARY KEY,
  titulo       VARCHAR(200) NOT NULL,
  autor_id     INT,
  precio       DECIMAL(6,2) NOT NULL DEFAULT 0,
  anio         YEAR,
  stock        INT DEFAULT 0,
  creado_en    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (autor_id) REFERENCES autores(id) ON DELETE SET NULL
) ENGINE=InnoDB;
OperaciónSQL
Añadir columnaALTER TABLE libros ADD COLUMN isbn VARCHAR(20);
Modificar tipo de columnaALTER TABLE libros MODIFY precio DECIMAL(8,2);
Renombrar columnaALTER TABLE libros CHANGE anio publicado YEAR;
Borrar columnaALTER TABLE libros DROP COLUMN isbn;
Borrar tablaDROP TABLE libros;
Vaciar tabla (rápido, resetea autoincremento)TRUNCATE TABLE libros;
DECIMAL, no FLOAT, para dinero. DECIMAL(6,2) guarda exactamente 2 decimales sin errores de redondeo binario. Para precios, siempre DECIMAL.
04

DML — insertar, actualizar, borrar

-- Insertar varias filas de una vez (mucho más rápido que una a una)
INSERT INTO clientes (nombre, email, ciudad) VALUES
  ('Ana García', 'ana@example.com', 'Madrid'),
  ('Luis Fernández', 'luis@example.com', 'Barcelona');

-- "Upsert": inserta, y si la clave ya existe, actualiza en su lugar
INSERT INTO clientes (id, nombre, email) VALUES (1, 'Ana García', 'ana2@example.com')
ON DUPLICATE KEY UPDATE email = VALUES(email);

UPDATE libros SET stock = stock - 1 WHERE id = 1;

DELETE FROM libros WHERE stock = 0;
Sin WHERE, afecta a toda la tabla. Antes de un UPDATE/DELETE con condiciones complejas, ejecuta el mismo WHERE dentro de un SELECT para ver qué filas se van a tocar.
05

DQL — SELECT a fondo

SELECT DISTINCT ciudad
FROM clientes
WHERE ciudad IS NOT NULL
  AND (pais = 'España' OR pais IS NULL)
ORDER BY ciudad ASC
LIMIT 10 OFFSET 20;   -- página 3 de resultados, 10 por página
Necesito…SQL
Comparar= <> > < >= <=
Rangoprecio BETWEEN 10 AND 20
Listaciudad IN ('Madrid','Sevilla')
Nulo / no nulocol IS NULL / IS NOT NULL
Combinar condicionesAND, OR, NOT (usa paréntesis siempre que mezcles AND/OR)
Alias de columna/tablaSELECT precio AS pvp FROM libros AS l
06

LIKE, REGEXP y SOUNDEX — búsqueda de texto

Esto es lo que no existe (o no existe igual) en SQLite y sí en MySQL/MariaDB:

-- LIKE: comodines % (cualquier cosa) y _ (un carácter)
SELECT * FROM libros WHERE titulo LIKE '%amor%';
SELECT * FROM clientes WHERE email LIKE '_@%.com';

-- REGEXP: patrones completos (más potente que LIKE)
SELECT * FROM clientes WHERE nombre REGEXP '^(Ana|Luis)';

-- SOUNDEX: busca por cómo "suena" una palabra, no cómo se escribe
-- Útil para nombres mal escritos: "Gonzalez" y "Gonzales" suenan igual
SELECT * FROM clientes WHERE SOUNDEX(nombre) = SOUNDEX('Gonzalez');

-- Combinado: candidatos con nombre parecido a "Sanches"
SELECT nombre FROM clientes
WHERE SOUNDEX(nombre) = SOUNDEX('Sanches')
ORDER BY nombre;
¿Cuándo usar cada uno? LIKE para patrones simples y rápidos (empieza por, contiene). REGEXP cuando el patrón es más complejo (varias alternativas, posiciones). SOUNDEX cuando el usuario puede haber escrito mal el nombre y quieres encontrarlo igual — típico en buscadores de clientes o formularios de contacto.
07

JOIN, subconsultas y UNION

-- INNER JOIN: solo lo que coincide en ambas tablas
SELECT l.titulo, a.nombre
FROM libros l
INNER JOIN autores a ON l.autor_id = a.id;

-- LEFT JOIN: todo lo de la izquierda, coincida o no
SELECT c.nombre, COUNT(p.id) AS num_pedidos
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.nombre;

-- RIGHT JOIN existe en MySQL (no en SQLite): es un LEFT JOIN invertido
SELECT * FROM pedidos p RIGHT JOIN clientes c ON p.cliente_id = c.id;

-- Subconsulta como filtro
SELECT titulo FROM libros
WHERE precio > (SELECT AVG(precio) FROM libros);

-- UNION: apila resultados de dos SELECT (mismas columnas), sin duplicados
SELECT nombre, 'cliente' AS tipo FROM clientes
UNION
SELECT nombre, 'autor' AS tipo FROM autores;
-- UNION ALL hace lo mismo pero SIN eliminar duplicados (más rápido)
08

Agregación y funciones útiles

SELECT
  a.nombre,
  COUNT(l.id)          AS num_libros,
  SUM(l.stock)         AS stock_total,
  AVG(l.precio)        AS precio_medio,
  MAX(l.precio)         AS precio_maximo
FROM autores a
JOIN libros l ON l.autor_id = a.id
GROUP BY a.nombre
HAVING COUNT(l.id) > 1     -- filtra grupos, no filas
ORDER BY num_libros DESC;
CategoríaFunciones
TextoCONCAT(a,b), UPPER(), LOWER(), TRIM(), SUBSTRING(s,ini,len), LENGTH()
Fecha/horaNOW(), CURDATE(), DATE_FORMAT(f,'%d/%m/%Y'), DATEDIFF(f1,f2), DATE_ADD(f, INTERVAL 7 DAY)
NulosCOALESCE(col, 'sin valor') — primer valor no nulo de la lista
NuméricasROUND(n,2), CEIL(), FLOOR(), ABS()
Condicional en líneaCASE WHEN precio > 15 THEN 'caro' ELSE 'normal' END
09

Índices y rendimiento

Un índice es una estructura auxiliar que acelera las búsquedas (a costa de un poco más de espacio y de INSERTs ligeramente más lentos). Regla del 80/20: indexa las columnas que usas en WHERE, JOIN y ORDER BY, no todas.

-- Índice normal sobre una columna muy consultada
CREATE INDEX idx_libros_autor ON libros(autor_id);

-- Índice único (además de acelerar, prohíbe duplicados)
CREATE UNIQUE INDEX idx_clientes_email ON clientes(email);

-- Ver qué índices tiene ya una tabla
SHOW INDEX FROM libros;

-- Ver si una consulta concreta está usando índices (diagnóstico)
EXPLAIN SELECT * FROM libros WHERE autor_id = 3;

Las claves primarias y las columnas UNIQUE ya crean índice automáticamente — no hace falta duplicarlo.

10

Transacciones

Cuando varias sentencias deben ejecutarse todas o ninguna (por ejemplo, mover stock entre dos tablas), una transacción evita quedarte a medias si algo falla. Requiere ENGINE=InnoDB.

START TRANSACTION;

UPDATE cuentas SET saldo = saldo - 100 WHERE id = 1;
UPDATE cuentas SET saldo = saldo + 100 WHERE id = 2;

-- Si ambas han ido bien:
COMMIT;

-- Si algo falló a mitad de camino (deshace todo lo anterior):
-- ROLLBACK;

En PHP con PDO: $pdo->beginTransaction(); ... $pdo->commit(); / $pdo->rollBack(); dentro de un try/catch.

11

Usuarios y permisos

En un hosting compartido normal, el panel (cPanel/Plesk) ya crea el usuario y la base de datos por ti y no necesitas esto. Es útil saber que existe, para servidores propios o VPS:

CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'contraseña_fuerte';
GRANT SELECT, INSERT, UPDATE, DELETE ON midb.* TO 'app_user'@'localhost';
FLUSH PRIVILEGES;

-- Ver qué permisos tiene un usuario
SHOW GRANTS FOR 'app_user'@'localhost';
Nunca conectes tu web con el usuario administrador de la base de datos. Crea un usuario con solo los permisos que la aplicación necesita (normalmente ni DROP ni GRANT).
12

Backup y restauración

-- Exportar toda una base de datos a un fichero .sql (desde terminal)
mysqldump -u usuario -p midb > copia.sql

-- Restaurar ese fichero en una base de datos (vacía o existente)
mysql -u usuario -p midb < copia.sql

Si no tienes acceso SSH (típico en hosting compartido), phpMyAdmin hace lo mismo desde el navegador: pestaña Exportar para la copia, pestaña Importar para restaurarla.

Haz copia antes de cualquier ALTER TABLE o migración grande, no después de que algo salga mal.

13

Seguridad: lo mínimo imprescindible

RiesgoCómo evitarlo
Inyección SQL críticoNunca concatenes variables de usuario dentro del SQL. Usa sentencias preparadas: PDO::prepare() con parámetros ? o :nombre.
Contraseñas en texto planoGuarda hashes con password_hash() en PHP, nunca la contraseña original ni siquiera cifrada reversiblemente.
Usuario con demasiados permisosEl usuario de la aplicación solo necesita SELECT/INSERT/UPDATE/DELETE, casi nunca DROP ni GRANT.
Backups ausentesPrograma mysqldump periódico (cron) o usa el backup automático del hosting.
-- MAL: vulnerable a inyección SQL
-- "SELECT * FROM clientes WHERE email = '" . $_GET['email'] . "'"

-- BIEN: sentencia preparada con PDO
-- $stmt = $pdo->prepare('SELECT * FROM clientes WHERE email = ?');
-- $stmt->execute([$_GET['email']]);
14

Chuleta final — 1 página

Quiero…SQL
Crear tablaCREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, ...) ENGINE=InnoDB;
InsertarINSERT INTO t (a,b) VALUES (1,2);
ConsultarSELECT * FROM t WHERE a=1 ORDER BY b DESC LIMIT 10;
Buscar textoWHERE col LIKE '%algo%' · fonético: SOUNDEX(col)=SOUNDEX('algo')
Unir tablasFROM a JOIN b ON a.id=b.a_id (o LEFT JOIN)
SubconsultaWHERE id IN (SELECT ... )
AgruparGROUP BY col HAVING COUNT(*)>1
ActualizarUPDATE t SET a=1 WHERE id=5;
BorrarDELETE FROM t WHERE id=5;
ÍndiceCREATE INDEX idx ON t(col);
TransacciónSTART TRANSACTION; ... COMMIT; / ROLLBACK;
Backupmysqldump -u u -p db > copia.sql

Con esto tienes el 20% de MySQL/MariaDB que cubre el 80% del trabajo diario. El resto — vistas, procedimientos almacenados, triggers, replicación, particionado — llega solo cuando un proyecto concreto lo pide.