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 cursoMariaDB 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:
| Concepto | SQLite (curso) | MySQL/MariaDB (hosting real) |
|---|---|---|
| Arquitectura | Fichero embebido, sin servidor | Servidor cliente-servidor, con usuario/contraseña |
| Autoincremento | INTEGER PRIMARY KEY (autoincrementa solo) | INT AUTO_INCREMENT PRIMARY KEY |
| Tipos de texto | TEXT para casi todo | VARCHAR(n), TEXT, CHAR(n) según tamaño |
| Fechas | TEXT con formato ISO | DATE, DATETIME, TIMESTAMP nativos |
| Motor de almacenamiento | Único, integrado | InnoDB (transaccional, recomendado) o MyISAM (más simple, sin transacciones) |
| Identificadores con espacios | Comillas dobles o [ ] | Backticks: `nombre columna` |
| Booleanos | No existen, se usa 0/1 | BOOLEAN = alias de TINYINT(1) |
| Concatenar texto | a || b | CONCAT(a, b) |
El SELECT, WHERE, JOIN, GROUP BY que ya conoces del curso funcionan exactamente igual en los dos.
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).
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ón | SQL |
|---|---|
| Añadir columna | ALTER TABLE libros ADD COLUMN isbn VARCHAR(20); |
| Modificar tipo de columna | ALTER TABLE libros MODIFY precio DECIMAL(8,2); |
| Renombrar columna | ALTER TABLE libros CHANGE anio publicado YEAR; |
| Borrar columna | ALTER TABLE libros DROP COLUMN isbn; |
| Borrar tabla | DROP TABLE libros; |
| Vaciar tabla (rápido, resetea autoincremento) | TRUNCATE TABLE libros; |
DECIMAL(6,2) guarda exactamente 2 decimales sin errores de redondeo binario. Para precios, siempre DECIMAL.
-- 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;
UPDATE/DELETE con condiciones complejas, ejecuta el mismo WHERE dentro de un SELECT para ver qué filas se van a tocar.
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 | = <> > < >= <= |
| Rango | precio BETWEEN 10 AND 20 |
| Lista | ciudad IN ('Madrid','Sevilla') |
| Nulo / no nulo | col IS NULL / IS NOT NULL |
| Combinar condiciones | AND, OR, NOT (usa paréntesis siempre que mezcles AND/OR) |
| Alias de columna/tabla | SELECT precio AS pvp FROM libros AS l |
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;
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.
-- 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)
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ía | Funciones |
|---|---|
| Texto | CONCAT(a,b), UPPER(), LOWER(), TRIM(), SUBSTRING(s,ini,len), LENGTH() |
| Fecha/hora | NOW(), CURDATE(), DATE_FORMAT(f,'%d/%m/%Y'), DATEDIFF(f1,f2), DATE_ADD(f, INTERVAL 7 DAY) |
| Nulos | COALESCE(col, 'sin valor') — primer valor no nulo de la lista |
| Numéricas | ROUND(n,2), CEIL(), FLOOR(), ABS() |
| Condicional en línea | CASE WHEN precio > 15 THEN 'caro' ELSE 'normal' END |
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.
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.
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';
DROP ni GRANT).
-- 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.
| Riesgo | Cómo evitarlo |
|---|---|
| Inyección SQL crítico | Nunca concatenes variables de usuario dentro del SQL. Usa sentencias preparadas: PDO::prepare() con parámetros ? o :nombre. |
| Contraseñas en texto plano | Guarda hashes con password_hash() en PHP, nunca la contraseña original ni siquiera cifrada reversiblemente. |
| Usuario con demasiados permisos | El usuario de la aplicación solo necesita SELECT/INSERT/UPDATE/DELETE, casi nunca DROP ni GRANT. |
| Backups ausentes | Programa 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']]);
| Quiero… | SQL |
|---|---|
| Crear tabla | CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY, ...) ENGINE=InnoDB; |
| Insertar | INSERT INTO t (a,b) VALUES (1,2); |
| Consultar | SELECT * FROM t WHERE a=1 ORDER BY b DESC LIMIT 10; |
| Buscar texto | WHERE col LIKE '%algo%' · fonético: SOUNDEX(col)=SOUNDEX('algo') |
| Unir tablas | FROM a JOIN b ON a.id=b.a_id (o LEFT JOIN) |
| Subconsulta | WHERE id IN (SELECT ... ) |
| Agrupar | GROUP BY col HAVING COUNT(*)>1 |
| Actualizar | UPDATE t SET a=1 WHERE id=5; |
| Borrar | DELETE FROM t WHERE id=5; |
| Índice | CREATE INDEX idx ON t(col); |
| Transacción | START TRANSACTION; ... COMMIT; / ROLLBACK; |
| Backup | mysqldump -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.