Chuleta de SQL
Resumen de consulta rápida con lo esencial de SQL, pensado para MySQL y MariaDB. Los ejemplos usan las tablas del curso: clientes(id, nombre, ciudad, edad) y pedidos(id, cliente_id, fecha, importe).
SELECT: consultas básicas
SELECT nombre, edad
FROM clientes
WHERE ciudad = 'Santander'
ORDER BY edad DESC
LIMIT 10;
-- Marta Ruiz 45 | Ana García 34
| Código | Qué hace |
|---|---|
SELECT * FROM clientes; | Todas las columnas y filas |
SELECT nombre, ciudad FROM clientes; | Solo algunas columnas |
SELECT nombre AS cliente FROM clientes; | Alias de columna |
SELECT edad + 1 AS edad_siguiente FROM clientes; | Columna calculada |
SELECT importe * 1.21 AS con_iva FROM pedidos; | Operaciones: + - * / % |
SELECT DISTINCT ciudad FROM clientes; | Quita repetidos → Santander, Bilbao, Madrid |
SELECT CONCAT(nombre, ' (', ciudad, ')') FROM clientes; | Une textos → Luis Pérez (Bilbao) |
nombre || ' ' || ciudad | Concatenar en PostgreSQL y SQLite (en MySQL || es OR) |
-- comentario / /* ... */ | Comentarios |
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT | Orden en que se escriben las partes |
Más en: ¿Qué es SQL? · SELECT
WHERE y NULL
| Código | Qué hace |
|---|---|
WHERE edad >= 18 | Comparaciones: = <> != < > <= >= |
WHERE ciudad = 'Madrid' AND edad > 20 | Las dos condiciones |
WHERE ciudad = 'Madrid' OR ciudad = 'Bilbao' | Una de las dos |
WHERE NOT edad < 18 | Niega la condición |
WHERE ciudad IN ('Bilbao', 'Madrid') | Está en la lista → Luis, Pablo, Eva |
WHERE ciudad NOT IN ('Madrid') | No está en la lista |
WHERE edad BETWEEN 20 AND 30 | Rango con extremos incluidos → Luis, Eva |
WHERE fecha >= '2026-09-10' | Fechas en formato 'AAAA-MM-DD' → pedidos 3, 4, 5 |
WHERE nombre LIKE 'M%' | Empieza por M → Marta Ruiz |
WHERE nombre LIKE '%ez' | Termina en ez |
WHERE nombre LIKE '_v%' | _ es un carácter → Eva Martín |
WHERE ciudad IS NULL | No tiene valor |
WHERE ciudad IS NOT NULL | Tiene valor |
WHERE ciudad = NULL | Error típico: nunca es verdadero |
NULL + 1 | NULL (cualquier operación con NULL da NULL) |
COALESCE(ciudad, 'sin dato') | Primer valor que no sea NULL |
IFNULL(ciudad, 'sin dato') | Igual, con dos valores (MySQL y SQLite) |
NULLIF(a, b) | NULL si a = b; si no, a |
Cuidado:
NOT IN (subconsulta)no devuelve nada si la subconsulta contiene algún NULL. UsaNOT EXISTS.
ORDER BY y LIMIT
| Código | Qué hace |
|---|---|
ORDER BY edad | De menor a mayor (ASC, por defecto) |
ORDER BY edad DESC | De mayor a menor |
ORDER BY ciudad, nombre | Por ciudad y, si empatan, por nombre |
ORDER BY total DESC | Se puede ordenar por un alias |
LIMIT 2 | Solo las 2 primeras filas |
LIMIT 2 OFFSET 1 | Salta 1 y devuelve 2 (paginación) |
LIMIT 1, 2 | Lo mismo en MySQL: LIMIT salto, cantidad |
FETCH FIRST 2 ROWS ONLY | Estándar (PostgreSQL, Oracle); SQL Server usa TOP |
Los NULL van primero en ASC en MySQL y SQLite, y al final en PostgreSQL.
Más en: ORDER BY y LIMIT
Funciones
| Código (MySQL) | Resultado |
|---|---|
UPPER('Ana') / LOWER('Ana') | 'ANA' / 'ana' |
LENGTH('Santander') | 9 (en bytes; CHAR_LENGTH cuenta caracteres) |
SUBSTRING('Ana García', 1, 3) | 'Ana' (la posición empieza en 1) |
REPLACE('Santander', 'Santander', 'SDR') | 'SDR' |
TRIM(' hola ') | 'hola' |
CONCAT('a', 'b') | 'ab' |
ROUND(120.456, 1) | 120.5 |
CEIL(3.2) / FLOOR(3.7) | 4 / 3 |
ABS(-5) | 5 |
MOD(7, 2) o 7 % 2 | 1 |
NOW() / CURDATE() | Fecha y hora actuales / fecha de hoy |
YEAR(fecha), MONTH(fecha), DAY(fecha) | Partes de una fecha |
DATEDIFF('2026-09-15', '2026-09-01') | 14 (días) |
DATE_ADD(fecha, INTERVAL 7 DAY) | Suma 7 días |
DATE_FORMAT(fecha, '%d/%m/%Y') | '01/09/2026' |
CAST('42' AS SIGNED) + 1 | 43 (en PostgreSQL y SQLite: AS INTEGER) |
Las funciones de fecha cambian mucho: PostgreSQL usa EXTRACT(YEAR FROM fecha) y SQLite strftime('%Y', fecha).
Más en: Funciones
CASE
SELECT nombre,
CASE
WHEN edad < 18 THEN 'menor'
WHEN edad < 30 THEN 'joven'
ELSE 'adulto'
END AS grupo
FROM clientes;
-- Ana adulto | Luis joven | Marta adulto | Pablo menor | Eva joven
| Código | Qué hace |
|---|---|
CASE ciudad WHEN 'Madrid' THEN 'MAD' ELSE 'otra' END | CASE simple: compara con valores |
CASE WHEN ... END sin ELSE | Devuelve NULL si no se cumple nada |
SUM(CASE WHEN importe > 50 THEN 1 ELSE 0 END) | Cuenta con condición |
ORDER BY CASE ciudad WHEN 'Madrid' THEN 1 ELSE 2 END | Orden a medida |
IF(edad >= 18, 'sí', 'no') | Atajo de MySQL (no es estándar) |
Más en: CASE
Agregados y GROUP BY
| Código | Resultado |
|---|---|
SELECT COUNT(*) FROM pedidos; | 5 (filas) |
SELECT COUNT(ciudad) FROM clientes; | Cuenta los valores no NULL |
SELECT COUNT(DISTINCT cliente_id) FROM pedidos; | 4 |
SELECT SUM(importe) FROM pedidos; | 271.74 |
SELECT AVG(importe) FROM pedidos; | 54.348 |
SELECT MIN(importe), MAX(importe) FROM pedidos; | 12.99, 120.50 |
SELECT ciudad, COUNT(*) AS total
FROM clientes
GROUP BY ciudad
ORDER BY total DESC, ciudad;
-- Madrid 2 | Santander 2 | Bilbao 1
| Código | Qué hace |
|---|---|
GROUP BY ciudad | Una fila por cada ciudad |
GROUP BY ciudad, edad | Agrupa por varias columnas |
HAVING COUNT(*) > 1 | Filtra grupos → Madrid (23), Santander (39.5) |
WHERE frente a HAVING | WHERE filtra filas antes de agrupar; HAVING, grupos después |
GROUP BY YEAR(fecha), MONTH(fecha) | Agrupar por una expresión (ventas por mes) |
Regla de oro: en el
SELECTde una consulta conGROUP BY, cada columna debe estar en elGROUP BYo dentro de una función de agregado.
Orden de ejecución: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT.
Más en: COUNT, SUM y AVG · GROUP BY
JOIN
SELECT c.nombre, COUNT(p.id) AS pedidos,
COALESCE(SUM(p.importe), 0) AS gastado
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre
ORDER BY gastado DESC;
-- Marta 1 120.50 | Luis 1 60.00 | Ana 2 57.99 | Eva 1 33.25 | Pablo 0 0
| Código | Qué hace |
|---|---|
FROM clientes c JOIN pedidos p ON p.cliente_id = c.id | INNER JOIN: solo filas con pareja |
FROM clientes c LEFT JOIN pedidos p ON ... | Todos los clientes, tengan pedidos o no |
LEFT JOIN ... WHERE p.id IS NULL | Clientes sin pedidos → Pablo Díaz |
RIGHT JOIN | Todas las filas de la tabla de la derecha |
FULL OUTER JOIN | Todas las de las dos (no existe en MySQL: usa LEFT + UNION + RIGHT) |
CROSS JOIN | Todas las combinaciones (producto cartesiano) |
JOIN pedidos USING (cliente_id) | Atajo cuando la columna se llama igual en las dos |
FROM empleados e JOIN empleados j ON e.jefe_id = j.id | Self join: una tabla consigo misma |
JOIN a ON ... JOIN b ON ... | Encadenar tres o más tablas |
Subconsultas y UNION
| Código | Qué hace |
|---|---|
WHERE importe > (SELECT AVG(importe) FROM pedidos) | Subconsulta escalar → pedidos 2 y 4 |
WHERE id IN (SELECT cliente_id FROM pedidos) | Clientes con algún pedido |
WHERE id NOT IN (SELECT cliente_id FROM pedidos) | Clientes sin pedidos → Pablo Díaz |
WHERE EXISTS (SELECT 1 FROM pedidos p WHERE p.cliente_id = c.id) | Subconsulta correlacionada |
SELECT nombre, (SELECT COUNT(*) FROM pedidos p WHERE p.cliente_id = c.id) FROM clientes c | Subconsulta en el SELECT |
FROM (SELECT ...) AS t | Tabla derivada (necesita alias) |
WHERE importe > ALL (SELECT ...) | Mayor que todos (también ANY) |
WITH totales AS (
SELECT cliente_id, SUM(importe) AS total
FROM pedidos
GROUP BY cliente_id
)
SELECT cliente_id, total FROM totales WHERE total > 50;
-- 1 57.99 | 2 60.00 | 3 120.50
| Código | Qué hace |
|---|---|
SELECT ... UNION SELECT ... | Junta resultados sin repetidos |
SELECT ... UNION ALL SELECT ... | Junta con repetidos (más rápido) |
SELECT ... INTERSECT SELECT ... | Lo que está en los dos (MySQL 8.0.31+) |
SELECT ... EXCEPT SELECT ... | Lo del primero que no está en el segundo (MySQL 8.0.31+) |
Las consultas de un UNION deben tener el mismo número de columnas y tipos compatibles.
Más en: Subconsultas · UNION
INSERT, UPDATE y DELETE
| Código | Qué hace |
|---|---|
INSERT INTO clientes (id, nombre, ciudad, edad) VALUES (6, 'Sara Gil', 'Bilbao', 31); | Añade una fila |
INSERT INTO clientes (id, nombre) VALUES (7, 'Iker'), (8, 'Noa'); | Varias filas; lo que falta queda NULL o DEFAULT |
INSERT INTO copia SELECT * FROM clientes WHERE ciudad = 'Madrid'; | Inserta el resultado de una consulta |
UPDATE clientes SET edad = edad + 1 WHERE id = 2; | Modifica filas |
UPDATE clientes SET ciudad = 'Laredo', edad = 23 WHERE id = 2; | Varias columnas a la vez |
DELETE FROM pedidos WHERE id = 5; | Borra filas |
UPDATE o DELETE sin WHERE | Afecta a TODAS las filas |
TRUNCATE TABLE pedidos; | Vacía la tabla entera (rápido, no se deshace en MySQL) |
INSERT ... ON DUPLICATE KEY UPDATE edad = VALUES(edad); | Inserta o actualiza (MySQL; PostgreSQL y SQLite usan ON CONFLICT) |
Consejo: antes de un
UPDATEoDELETE, ejecuta unSELECTcon el mismoWHEREpara ver qué filas cambiarán.
Más en: INSERT, UPDATE, DELETE
CREATE TABLE y restricciones
CREATE TABLE pedidos (
id INT AUTO_INCREMENT PRIMARY KEY,
cliente_id INT NOT NULL,
fecha DATE DEFAULT (CURRENT_DATE),
importe DECIMAL(10, 2) CHECK (importe >= 0),
FOREIGN KEY (cliente_id) REFERENCES clientes(id)
ON DELETE CASCADE
);
| Código | Qué hace |
|---|---|
CREATE DATABASE tienda CHARACTER SET utf8mb4; | Crea una base de datos |
USE tienda; | La selecciona (MySQL) |
CREATE TABLE IF NOT EXISTS ... | No da error si ya existe |
SHOW TABLES; / DESCRIBE clientes; | Lista tablas / muestra columnas (MySQL) |
CREATE TABLE copia AS SELECT * FROM clientes; | Tabla a partir de una consulta |
id INT AUTO_INCREMENT | Id automático (PostgreSQL: GENERATED ALWAYS AS IDENTITY; SQLite: INTEGER PRIMARY KEY) |
| Tipo (MySQL) | Para qué |
|---|---|
INT, BIGINT | Enteros |
DECIMAL(10, 2) | Dinero: decimales exactos |
FLOAT, DOUBLE | Decimales aproximados |
VARCHAR(100) | Texto corto de longitud variable |
TEXT | Texto largo |
DATE, DATETIME, TIMESTAMP | Fecha / fecha y hora |
BOOLEAN | En MySQL es TINYINT(1): 0 o 1 |
| Restricción | Qué asegura |
|---|---|
PRIMARY KEY | Identifica cada fila: única y no NULL |
NOT NULL | Obligatorio |
UNIQUE | Sin repetidos (por ejemplo, el email) |
DEFAULT valor | Valor si no se indica otro |
CHECK (edad >= 0) | Condición que debe cumplirse (MySQL 8.0.16+) |
FOREIGN KEY ... REFERENCES | El valor debe existir en la otra tabla |
ON DELETE CASCADE / SET NULL / RESTRICT | Qué pasa con los hijos al borrar el padre |
CONSTRAINT chk_edad CHECK (...) | Pone nombre a una restricción |
Más en: CREATE TABLE · Restricciones
ALTER y DROP
| Código | Qué hace |
|---|---|
ALTER TABLE clientes ADD COLUMN email VARCHAR(150); | Añade una columna |
ALTER TABLE clientes DROP COLUMN email; | Borra una columna |
ALTER TABLE clientes RENAME COLUMN ciudad TO localidad; | Renombra una columna |
ALTER TABLE clientes MODIFY nombre VARCHAR(150) NOT NULL; | Cambia el tipo (MySQL; PostgreSQL: ALTER COLUMN ... TYPE) |
ALTER TABLE clientes ADD CONSTRAINT uq_email UNIQUE (email); | Añade una restricción |
ALTER TABLE clientes RENAME TO socios; | Renombra la tabla |
DROP TABLE IF EXISTS copia; | Borra la tabla con sus datos |
DROP DATABASE tienda; | Borra la base de datos entera |
Más en: ALTER y DROP
Vistas e índices
| Código | Qué hace |
|---|---|
CREATE VIEW v_gasto AS SELECT cliente_id, SUM(importe) AS total FROM pedidos GROUP BY cliente_id; | Guarda una consulta con nombre |
SELECT * FROM v_gasto WHERE total > 50; | Se consulta como una tabla |
CREATE OR REPLACE VIEW v_gasto AS ... | Cambia la vista (MySQL y PostgreSQL) |
DROP VIEW v_gasto; | Borra la vista (no los datos) |
CREATE INDEX idx_ciudad ON clientes (ciudad); | Acelera búsquedas por ciudad |
CREATE UNIQUE INDEX idx_email ON clientes (email); | Índice que además impide repetidos |
CREATE INDEX idx_cli_fecha ON pedidos (cliente_id, fecha); | Índice compuesto: el orden importa |
EXPLAIN SELECT * FROM clientes WHERE ciudad = 'Madrid'; | Muestra si se usa un índice |
DROP INDEX idx_ciudad ON clientes; | Borra un índice (PostgreSQL y SQLite: DROP INDEX idx_ciudad;) |
Las claves primarias y las restricciones UNIQUE crean su índice solas. Los índices aceleran las lecturas pero ralentizan un poco las escrituras.
Transacciones
START TRANSACTION;
UPDATE cuentas SET saldo = saldo - 100 WHERE id = 1;
UPDATE cuentas SET saldo = saldo + 100 WHERE id = 2;
COMMIT; -- o ROLLBACK para deshacer todo
| Código | Qué hace |
|---|---|
START TRANSACTION; o BEGIN; | Empieza una transacción |
COMMIT; | Confirma los cambios |
ROLLBACK; | Deshace los cambios desde el inicio |
SAVEPOINT paso1; | Punto de guardado intermedio |
ROLLBACK TO paso1; | Vuelve a ese punto |
SET autocommit = 0; | Desactiva la confirmación automática (MySQL) |
| ACID | Atomicidad, Consistencia, Aislamiento y Durabilidad |
En MySQL solo funcionan con tablas InnoDB (el motor por defecto). CREATE, ALTER y DROP confirman solos y no se pueden deshacer.
Más en: Transacciones
Diseño y permisos
| Concepto | Resumen |
|---|---|
| Entidad-relación | Entidades (tablas), atributos (columnas) y relaciones |
| Relación 1:N | Clave foránea en la tabla del lado N (pedidos.cliente_id) |
| Relación N:M | Tabla intermedia con dos claves foráneas |
| 1FN | Valores atómicos: nada de listas en una celda |
| 2FN | Cada columna depende de toda la clave primaria |
| 3FN | Ninguna columna depende de otra que no sea clave |
| Código (MySQL) | Qué hace |
|---|---|
CREATE USER 'ana'@'localhost' IDENTIFIED BY 'clave'; | Crea un usuario |
GRANT SELECT, INSERT ON tienda.* TO 'ana'@'localhost'; | Da permisos |
GRANT ALL PRIVILEGES ON tienda.* TO 'ana'@'localhost'; | Todos los permisos sobre una base de datos |
SHOW GRANTS FOR 'ana'@'localhost'; | Muestra sus permisos |
REVOKE INSERT ON tienda.* FROM 'ana'@'localhost'; | Quita permisos |
CREATE ROLE lectura; + GRANT lectura TO 'ana'@'localhost'; | Roles: grupos de permisos |
ALTER USER 'ana'@'localhost' IDENTIFIED BY 'nueva'; | Cambia la contraseña |
DROP USER 'ana'@'localhost'; | Borra el usuario |
SQLite no tiene usuarios ni permisos. En PostgreSQL los usuarios son roles y no llevan @'host'.
Más en: Diseño y normalización · Usuarios y permisos
SQL · devschool.es/chuletas/sql/