Cheatsheet SQL e MySQL
Linguagem de consulta e gestão de bases de dados relacionais (SQL genérico e MySQL)
SQL e MySQL
SELECCIONAR y Consultas
Seleccionar Todo
SELECT * FROM clientes; -- Tablas específicas en un JOIN SELECT c.*, p.total FROM clientes c JOIN pedidos p ON p.cliente_id = c.id;
SELECT * devuelve todas las columnas. En producción, prefiere listar las columnas necesarias para reducir el tráfico y evitar exponer datos sensibles.
CONCAT y Operadores de String
-- MySQL
SELECT CONCAT(nombre, ' ', apellido) AS completo
FROM clientes;
-- PostgreSQL / SQL estándar
SELECT nombre || ' ' || apellido AS completo
FROM clientes;
-- Con función de formato
SELECT CONCAT('€', FORMAT(precio, 2)) FROM productos;CONCAT() une strings (MySQL). El operador || hace lo mismo en PostgreSQL/SQLite. Si cualquier valor es NULL, el resultado es NULL — usa COALESCE para evitarlo.
EXISTS y NOT EXISTS
-- Clientes con pedidos
SELECT nombre FROM clientes c
WHERE EXISTS (
SELECT 1 FROM pedidos p
WHERE p.cliente_id = c.id
);
-- Clientes sin pedidos
SELECT nombre FROM clientes c
WHERE NOT EXISTS (
SELECT 1 FROM pedidos p
WHERE p.cliente_id = c.id
);EXISTS verifica si la subquery devuelve al menos una fila. Es más eficiente que IN para tablas grandes porque se detiene en la primera coincidencia. NOT EXISTS invierte la lógica.
Columnas Específicas
SELECT nombre, email, ciudad
FROM clientes;
-- Con alias de columna
SELECT nombre AS cliente,
email AS contacto
FROM clientes;Selecciona solo las columnas necesarias — más rápido y claro. AS renombra la columna en el resultado (alias). El alias es opcional: SELECT nombre cliente también funciona.
Expresiones Aritméticas
SELECT precio * cantidad AS total FROM ítems; SELECT precio * (1 - descuento/100) AS precio_final FROM productos; SELECT SUM(precio * cantidad) AS total_pedido FROM ítems WHERE pedido_id = 42;
Puedes calcular valores directamente en la consulta con +, -, *, / y % (módulo). Combina con AS para dar nombre al resultado.
CASE en el SELECT
SELECT nombre,
CASE
WHEN total > 1000 THEN 'VIP'
WHEN total > 100 THEN 'Regular'
ELSE 'Nuevo'
END AS segmento
FROM clientes;
-- CASE simple (comparación directa)
SELECT status,
CASE status
WHEN 1 THEN 'Activo'
WHEN 0 THEN 'Inactivo'
END AS estado
FROM contas;CASE añade lógica condicional al SELECT. La forma buscada usa WHEN condición; la forma simple compara un valor. ELSE es opcional (sin él, devuelve NULL).
Alias de Tabla
SELECT c.nombre, p.total FROM clientes c JOIN pedidos p ON p.cliente_id = c.id; -- Alias con AS (opcional) SELECT c.nombre FROM clientes AS c;
El alias de tabla abrevia nombres anchos y es esencial en los JOINs y self JOINs. Una vez definido, usa el alias en todas las referencias a la tabla en esa consulta.
Subquery en el SELECT
SELECT nombre,
(SELECT COUNT(*) FROM pedidos p
WHERE p.cliente_id = c.id) AS num_pedidos
FROM clientes c;
-- Subquery escalar
SELECT nombre,
(SELECT MAX(total) FROM pedidos) AS mayor_pedido
FROM clientes;Una subquery en el SELECT debe devolver un único valor (escalar). Se ejecuta para cada fila de la consulta exterior. Para mejor rendimiento, prefiere JOIN cuando sea posible.
DISTINCT
-- Elimina filas duplicadas: SELECT DISTINCT ciudad FROM clientes; -- En varias columnas (combinaciones únicas): SELECT DISTINCT ciudad, país FROM clientes; -- Contar valores distintos: SELECT COUNT(DISTINCT ciudad) FROM clientes; -- DISTINCT se aplica a TODAS -- las columnas del SELECT
DISTINCT elimina duplicados del resultado. Se aplica al conjunto de todas las columnas seleccionadas. COUNT(DISTINCT col) cuenta valores únicos. Tiene un coste de ordenación — úsalo solo cuando sea necesario.
DISTINCT (Sin Duplicados)
SELECT DISTINCT ciudad FROM clientes; -- Múltiples columnas SELECT DISTINCT ciudad, país FROM clientes; -- Contar valores distintos SELECT COUNT(DISTINCT ciudad) FROM clientes;
DISTINCT elimina las filas duplicadas del resultado. Con varias columnas, la combinación completa debe ser única. Puede usarse dentro de COUNT() para contar valores únicos.
UNION y UNION ALL
-- Combinar resultados (sin duplicados) SELECT nombre FROM clientes UNION SELECT nombre FROM proveedores; -- Mantener duplicados (más rápido) SELECT ciudad FROM clientes UNION ALL SELECT ciudad FROM proveedores;
UNION combina los resultados de dos consultas, eliminando los duplicados. UNION ALL los mantiene todos (más rápido). Ambas consultas deben tener el mismo numero de columnas y tipos compatibles.
Alias y subquery en el FROM
-- Alias de tabla y columna:
SELECT c.nombre AS cliente,
p.total AS valor
FROM clientes AS c
JOIN pedidos AS p ON p.cliente_id = c.id;
-- Subquery en el FROM (tabla derivada):
SELECT media.ciudad, media.total
FROM (
SELECT ciudad, AVG(total) AS total
FROM pedidos
GROUP BY ciudad
) AS media
WHERE media.total > 100;Los alias (AS) acortan nombres de tablas y columnas. Una subquery en el FROM (tabla derivada) permite filtrar resultados agregados. El alias de la subquery es obligatorio.
Filtros (DONDE)
Condición Simple
SELECT * FROM productos WHERE precio > 100; SELECT * FROM clientes WHERE activo = 1; SELECT * FROM pedidos WHERE data >= '2024-01-01';
WHERE filtra filas antes de cualquier agregación. Acepta comparaciones con =, >, <, >=, <=, <> (o !=). Strings entre comillas simples.
LIKE (Patrones)
-- Empieza con SELECT * FROM clientes WHERE nombre LIKE 'Ana%'; -- Termina con SELECT * FROM clientes WHERE email LIKE '%@gmail.com'; -- Contiene SELECT * FROM productos WHERE nombre LIKE '%phone%'; -- Un carácter cualquiera SELECT * FROM clientes WHERE nombre LIKE 'A_a';
LIKE hace coincidencia por patrones. % representa cualquier secuencia (0 o más caracteres) y _ representa exactamente un carácter. La sensibilidad a mayúsculas depende del COLLATE.
Filtro con Funciones
-- Filtrar por parte de la fecha SELECT * FROM pedidos WHERE YEAR(data) = 2024; -- Filtrar por longitud de string SELECT * FROM clientes WHERE LENGTH(nombre) > 20; -- Atención: las funciones en la columna impiden índices -- Malo: WHERE YEAR(data) = 2024 -- Bueno: WHERE data >= '2024-01-01' AND data < '2025-01-01'
Puedes usar funciones en el WHERE, pero eso impide el uso de índices en la columna (full scan). Prefiere comparaciones directas con intervalos para mantener el rendimiento.
AND, OR y Paréntesis
SELECT * FROM productos WHERE categoria = 'electrónica' AND precio < 500; -- OR con paréntesis (precedencia) SELECT * FROM clientes WHERE (ciudad = 'Lisbon' OR ciudad = 'Porto') AND activo = 1;
AND exige todas las condiciones; OR exige al menos una. Usa paréntesis para controlar la precedencia — sin ellos, AND tiene prioridad sobre OR.
IS NULL y IS NOT NULL
-- Encontrar valores nulos SELECT * FROM clientes WHERE telefono IS NULL; -- Excluir nulos SELECT * FROM clientes WHERE email IS NOT NULL; -- ERROR: nunca uses = NULL -- WHERE telefono = NULL ← ¡no funciona!
Para verificar NULL, usa siempre IS NULL o IS NOT NULL. El operador = NULL nunca funciona porque NULL significa "desconocido" y cualquier comparación con él devuelve UNKNOWN.
ANY, ALL y SOME
-- Mayor que cualquier valor de la subquery SELECT * FROM productos WHERE precio > ANY (SELECT precio FROM promociones); -- Mayor que todos los valores SELECT * FROM productos WHERE precio > ALL (SELECT precio FROM promociones); -- SOME es sinónimo de ANY WHERE stock > SOME (SELECT mínimo FROM alertas);
ANY (o SOME) devuelve true si la comparación es verdadera para al menos un valor. ALL exige que sea verdadera para todos. Se usan con subqueries que devuelven una sola columna.
BETWEEN (Intervalo)
SELECT * FROM productos WHERE precio BETWEEN 10 AND 100; -- Fechas SELECT * FROM pedidos WHERE data BETWEEN '2024-01-01' AND '2024-12-31'; -- Negación WHERE precio NOT BETWEEN 50 AND 200;
BETWEEN verifica si el valor está en el intervalo (inclusivo en ambos extremos). Funciona con numeros, fechas y strings. Equivale a >= X AND <= Y.
NOT (Negación)
WHERE NOT ciudad = 'Lisbon'; WHERE NOT activo; -- Combinado WHERE NOT (precio > 100 AND stock > 0); -- Equivalente a != WHERE ciudad != 'Lisbon';
NOT niega cualquier condición. Puede usarse con BETWEEN, IN, LIKE, EXISTS. En muchos casos, != o <> son equivalentes y más legibles.
IN (Lista de Valores)
SELECT * FROM clientes
WHERE ciudad IN ('Lisbon', 'Porto', 'Braga');
-- Con subquery
SELECT * FROM pedidos
WHERE cliente_id IN (
SELECT id FROM clientes WHERE activo = 1
);
-- Negación
WHERE status NOT IN ('cancelado', 'devolvido');IN verifica si el valor pertenece a una lista. Más legible que múltiples OR. Puede contener una subquery. NOT IN lo invierte — cuidado con NULL en la subquery (devuelve vacío).
Comparación con NULL Segura
-- MySQL: operador <=> SELECT * FROM clientes WHERE telefono <=> NULL; -- true si es NULL -- COALESCE para comparación WHERE COALESCE(telefono, '') = ''; -- PostgreSQL: IS NOT DISTINCT FROM WHERE telefono IS NOT DISTINCT FROM NULL;
Las comparaciones normales fallan con NULL. El operador <=> (MySQL) trata NULL como un valor comparable. COALESCE sustituye NULL por un valor por defecto antes de comparar.
Ordenamiento y límites
ORDER BY Básico
-- Ascendente (por defecto) SELECT * FROM productos ORDER BY precio ASC; -- Descendente SELECT * FROM productos ORDER BY precio DESC; -- Por varias columnas SELECT * FROM clientes ORDER BY ciudad ASC, nombre DESC;
ORDER BY ordena el resultado. ASC es el valor por defecto (omitible). Con varias columnas, ordena por la primera y desempata por la segunda. Acepta alias: ORDER BY total.
FETCH FIRST (SQL Estándar)
-- SQL estándar (PostgreSQL, Oracle, SQL Server) SELECT * FROM productos ORDER BY precio DESC FETCH FIRST 10 ROWS ONLY; -- Con offset OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY; -- SQL Server SELECT TOP 10 * FROM productos ORDER BY precio DESC;
FETCH FIRST es la alternativa estándar a LIMIT (soportado en PostgreSQL, Oracle, DB2). SQL Server usa TOP. MySQL usa LIMIT. Todos hacen lo mismo: restringir filas.
ORDER BY con Expresión
-- Ordenar por un cálculo SELECT nombre, precio * stock AS valor FROM productos ORDER BY precio * stock DESC; -- Ordenar por posición de la columna SELECT nombre, email, ciudad FROM clientes ORDER BY 3; -- NULLs primero/último (PostgreSQL) ORDER BY telefono NULLS FIRST;
Puedes ordenar por expresiones, alias o posición de la columna (1-based). En PostgreSQL, NULLS FIRST/NULLS LAST controla dónde van los nulos. En MySQL, NULL viene primero en ASC.
Ordenación Aleatoria
-- MySQL SELECT * FROM productos ORDER BY RAND() LIMIT 5; -- PostgreSQL SELECT * FROM productos ORDER BY RANDOM() LIMIT 5; -- SQL Server SELECT TOP 5 * FROM productos ORDER BY NEWID();
Para seleccionar filas aleatorias, cada SGBD tiene su función: RAND() (MySQL), RANDOM() (PostgreSQL), NEWID() (SQL Server). Lento en tablas grandes — considera TABLESAMPLE.
LIMIT y OFFSET
-- Primeros 10 SELECT * FROM productos LIMIT 10; -- Paginación: página 3 (10 por página) SELECT * FROM productos ORDER BY id LIMIT 10 OFFSET 20; -- Sintaxis alternativa (MySQL) SELECT * FROM productos LIMIT 20, 10;
LIMIT restringe el numero de filas. OFFSET salta N filas antes de devolver. Esencial para la paginación. La sintaxis LIMIT offset, count es específica de MySQL.
Orden de las Cláusulas
SELECT columnas -- 5. proyección FROM tabla -- 1. origen WHERE condicion -- 2. filtro de filas GROUP BY columna -- 3. agrupamiento HAVING condicion_grupo -- 4. filtro de grupos ORDER BY columna -- 6. ordenación LIMIT n OFFSET m; -- 7. restricción final
El orden lógico de ejecución: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. Por eso no puedes usar un alias del SELECT en el WHERE.
Top N por Grupo
-- Top 3 productos por categoría (MySQL 8+)
SELECT * FROM (
SELECT nombre, categoria, precio,
ROW_NUMBER() OVER (
PARTITION BY categoria ORDER BY precio DESC
) AS pos
FROM productos
) ranked
WHERE pos <= 3;Para obtener el top N por grupo, usa ROW_NUMBER() con PARTITION BY (window function). La subquery numera las filas dentro de cada grupo; el filtro exterior selecciona las N primeras.
UNIONES
INNER JOIN
SELECT p.nombre AS producto,
c.nombre AS categoria
FROM productos p
INNER JOIN categorias c
ON p.categoria_id = c.id;
-- JOIN es sinónimo de INNER JOIN
SELECT * FROM a JOIN b ON a.id = b.a_id;INNER JOIN devuelve solo las filas con coincidencia en ambas tablas. Las filas sin pareja se excluyen. La palabra INNER es opcional: JOIN solo hace lo mismo.
Self JOIN
-- Jerarquía: empleado → jefe
SELECT e.nombre AS empleado,
c.nombre AS chefe
FROM funcionarios e
JOIN funcionarios c ON e.chefe_id = c.id;
-- LEFT para incluir a quien no tiene jefe
SELECT e.nombre, c.nombre AS chefe
FROM funcionarios e
LEFT JOIN funcionarios c ON e.chefe_id = c.id;Un self JOIN une una tabla consigo misma usando alias diferentes. Esencial para jerarquías (padre-hijo), árboles y comparaciones entre filas de la misma tabla.
LATERAL JOIN (PostgreSQL/MySQL 8)
-- PostgreSQL: subquery correlacionada en el FROM
SELECT c.nombre, top.total
FROM clientes c
JOIN LATERAL (
SELECT total FROM pedidos p
WHERE p.cliente_id = c.id
ORDER BY total DESC LIMIT 3
) top ON true;
-- MySQL 8.0.14+
SELECT c.nombre, t.total
FROM clientes c
JOIN LATERAL (
SELECT total FROM pedidos
WHERE cliente_id = c.id LIMIT 3
) t ON true;LATERAL JOIN permite que la subquery en el FROM referencie columnas de tablas anteriores. Ideal para "top N por grupo" sin window functions. El ON true es obligatorio en PostgreSQL.
LEFT JOIN
SELECT c.nombre, p.total FROM clientes c LEFT JOIN pedidos p ON c.id = p.cliente_id; -- Filtrar solo los que NO tienen coincidencia SELECT c.nombre FROM clientes c LEFT JOIN pedidos p ON c.id = p.cliente_id WHERE p.id IS NULL;
LEFT JOIN devuelve todas las filas de la tabla izquierda, incluso sin coincidencia (las columnas de la derecha quedan NULL). El truco WHERE p.id IS NULL encuentra registros huérfanos.
CROSS JOIN
-- Producto cartesiano: todas las combinaciones SELECT c.nombre AS color, t.nombre AS tamano FROM colores c CROSS JOIN tamanhos t; -- Equivalente implícito (sin ON) SELECT * FROM colores, tamanhos; -- Útil: generar fechas × productos SELECT d.data, p.nombre FROM datas d CROSS JOIN productos p;
CROSS JOIN genera el producto cartesiano: cada fila de A combinada con cada fila de B. Útil para generar matrices (fechas × productos, colores × tamaños). Cuidado: N×M filas pueden ser enormes.
SELF JOIN
-- JOIN de la tabla consigo misma:
-- (empleados con el nombre del jefe)
SELECT e.nombre AS empleado,
c.nombre AS chefe
FROM empleados e
LEFT JOIN empleados c
ON e.chefe_id = c.id;
-- Los alias diferentes (e, c) son
-- obligatorios para distinguir
-- las dos "copias" de la tablaEl SELF JOIN enlaza una tabla consigo misma — esencial para jerarquías (jefe/empleado, categorías padre/hijo). Usa alias diferentes para distinguir las dos referencias a la misma tabla.
RIGHT JOIN y FULL OUTER JOIN
-- RIGHT: todas las filas de la tabla derecha SELECT c.nombre, p.total FROM pedidos p RIGHT JOIN clientes c ON p.cliente_id = c.id; -- FULL OUTER: todas de ambas (PostgreSQL/Oracle) SELECT * FROM a FULL OUTER JOIN b ON a.id = b.a_id; -- MySQL no tiene FULL OUTER — usar UNION SELECT * FROM a LEFT JOIN b ON a.id = b.a_id UNION SELECT * FROM a RIGHT JOIN b ON a.id = b.a_id;
RIGHT JOIN es el espejo de LEFT JOIN. FULL OUTER JOIN devuelve todo de ambas tablas. MySQL no soporta FULL OUTER — simúlalo con un UNION de LEFT y RIGHT.
JOIN con USING
-- Cuando la columna tiene el mismo nombre SELECT * FROM pedidos JOIN clientes USING (cliente_id); -- vs ON (más explícito) SELECT * FROM pedidos p JOIN clientes c ON p.cliente_id = c.id; -- NATURAL JOIN (automático — evitar) SELECT * FROM pedidos NATURAL JOIN clientes;
USING es una abreviación cuando la columna de enlace tiene el mismo nombre en ambas tablas. NATURAL JOIN lo hace automáticamente para todas las columnas con el mismo nombre — peligroso y poco explícito.
Non-equi JOIN (desigualdad)
-- JOIN con condiciones de desigualdad: SELECT p.nombre, e.nombre AS escalao FROM pedidos p JOIN escaloes e ON p.total >= e.mínimo AND p.total < e.máximo; -- Útil para "encajar" valores en -- intervalos (escalones, rangos) -- Atención: puede generar MUCHAS filas -- (producto cartesiano parcial)
Un non-equi JOIN usa condiciones de desigualdad (mayor, menor, BETWEEN) en vez de igualdad. Ideal para mapear valores a intervalos (escalones de precio, comisiones). Atención al volumen de filas generado.
JOIN con Múltiples Condiciones
SELECT *
FROM ventas v
JOIN productos p
ON v.producto_id = p.id
AND v.anio = p.anio_venta;
-- JOIN con una condición extra
JOIN clientes c
ON v.cliente_id = c.id
AND c.país = 'Portugal';ON acepta múltiples condiciones con AND. Útil para tablas con claves compuestas. Las condiciones de filtro en ON vs WHERE se comportan de forma distinta en un LEFT JOIN (los filtros en ON no eliminan filas de la izquierda).
Múltiples JOINs
SELECT p.nombre AS producto,
c.nombre AS categoria,
f.nombre AS proveedor
FROM productos p
JOIN categorias c ON p.categoria_id = c.id
JOIN proveedores f ON p.proveedor_id = f.id
WHERE p.activo = 1;Puedes encadenar varios JOINs en la misma consulta. Cada JOIN añade una tabla. Mantén los alias organizados y verifica que las condiciones ON son correctas para evitar productos cartesianos accidentales.
Agregaciones
COUNT
-- Contar todas las filas SELECT COUNT(*) FROM clientes; -- Contar no-nulos de una columna SELECT COUNT(email) FROM clientes; -- Contar valores distintos SELECT COUNT(DISTINCT ciudad) FROM clientes;
COUNT(*) cuenta todas las filas (incluyendo NULL). COUNT(columna) ignora los nulos. COUNT(DISTINCT col) cuenta valores únicos. Es la única función que nunca devuelve NULL.
HAVING (Filtro de Grupo)
SELECT ciudad, COUNT(*) AS total FROM clientes GROUP BY ciudad HAVING COUNT(*) > 10; -- HAVING con agregación SELECT categoria, AVG(precio) AS media FROM productos GROUP BY categoria HAVING AVG(precio) > 50;
HAVING filtra grupos después del GROUP BY (WHERE filtra filas antes). Usa WHERE para condiciones de fila y HAVING para condiciones de agregación. Puedes usar un alias: HAVING total > 10.
HAVING (filtrar agregaciones)
-- WHERE no puede filtrar agregados; -- HAVING sí (después del GROUP BY): SELECT cliente_id, SUM(total) AS gasto FROM pedidos GROUP BY cliente_id HAVING SUM(total) > 500; -- Orden lógico: -- WHERE filtra filas ANTES -- HAVING filtra grupos DESPUÉS SELECT categoria, COUNT(*) AS n FROM productos WHERE activo = TRUE -- antes GROUP BY categoria HAVING COUNT(*) >= 5; -- después
HAVING filtra resultados agregados (después del GROUP BY), mientras que WHERE filtra filas antes de la agregación. Regla: WHERE primero, HAVING después. No se pueden usar agregados en el WHERE.
SUM, AVG y Aritmética
SELECT SUM(total) AS ingresos FROM pedidos; SELECT AVG(precio) AS media FROM productos; -- Media con NULL tratado como 0 SELECT AVG(COALESCE(descuento, 0)) FROM ítems; -- Suma condicional SELECT SUM(CASE WHEN status = 'pagado' THEN total ELSE 0 END) FROM pedidos;
SUM suma valores y AVG calcula la media. Ambos ignoran NULL. Para una suma condicional, combina con CASE. Si todas las filas son NULL, el resultado es NULL (no 0).
GROUP BY con ROLLUP
-- MySQL / SQL Server SELECT categoria, anio, SUM(total) FROM ventas GROUP BY categoria, anio WITH ROLLUP; -- PostgreSQL (GROUPING SETS) SELECT categoria, anio, SUM(total) FROM ventas GROUP BY ROLLUP (categoria, anio);
ROLLUP añade filas de subtotal y total general al resultado. Con GROUP BY ROLLUP(a, b) obtienes grupos por (a,b), por (a) y el total general. Las filas de subtotal tienen NULL en las columnas agregadas.
GROUP BY múltiples columnas
-- Agrupar por varias columnas: SELECT país, ciudad, COUNT(*) AS total FROM clientes GROUP BY país, ciudad ORDER BY país, total DESC; -- Cada combinación única de -- (país, ciudad) se vuelve una fila -- Con ROLLUP (subtotales + total): SELECT país, ciudad, COUNT(*) FROM clientes GROUP BY país, ciudad WITH ROLLUP; -- (MySQL; PostgreSQL: ROLLUP(país, ciudad))
GROUP BY con varias columnas crea un grupo por cada combinación única. WITH ROLLUP (MySQL) o ROLLUP() (PostgreSQL) añade filas de subtotal y total general — útil para informes.
MIN y MAX
SELECT MIN(precio), MAX(precio) FROM productos; -- Fecha más reciente SELECT MAX(data) AS último_pedido FROM pedidos; -- String: orden alfabético SELECT MIN(nombre), MAX(nombre) FROM clientes;
MIN y MAX devuelven el valor menor y mayor. Funcionan con numeros, fechas y strings (orden alfabético). Ignoran NULL. Útiles para encontrar límites y fechas extremas.
GROUP_CONCAT (MySQL)
-- MySQL: concatenar los valores del grupo
SELECT categoria,
GROUP_CONCAT(nombre ORDER BY nombre SEPARATOR ', ')
FROM productos
GROUP BY categoria;
-- PostgreSQL: STRING_AGG
SELECT categoria,
STRING_AGG(nombre, ', ' ORDER BY nombre)
FROM productos
GROUP BY categoria;GROUP_CONCAT (MySQL) une los valores del grupo en una string. STRING_AGG hace lo mismo en PostgreSQL. Útil para listas de tags, nombres o IDs. Límite de tamaño: group_concat_max_len.
GROUP BY
SELECT ciudad, COUNT(*) AS total FROM clientes GROUP BY ciudad; -- Múltiples columnas SELECT anio, mes, SUM(valor) AS total FROM ventas GROUP BY anio, mes ORDER BY anio, mes;
GROUP BY agrupa filas con el mismo valor y aplica agregaciones a cada grupo. Todas las columnas del SELECT que no están agregadas deben estar en el GROUP BY (en modo ONLY_FULL_GROUP_BY).
Agregación con JOIN
SELECT c.nombre,
COUNT(p.id) AS num_pedidos,
COALESCE(SUM(p.total), 0) AS total_gasto
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.id, c.nombre
ORDER BY total_gasto DESC;Combina JOIN con GROUP BY para agregar datos relacionados. Usa LEFT JOIN para incluir clientes sin pedidos. COALESCE convierte NULL (sin pedidos) en 0.
INSERTAR, ACTUALIZAR, ELIMINAR
INSERT Simple
INSERT INTO clientes (nombre, email, ciudad)
VALUES ('Ana Silva', 'ana@mail.com', 'Lisbon');
-- Con valores por defecto
INSERT INTO productos (nombre, precio)
VALUES ('Teclado', DEFAULT);INSERT añade filas. Especifica siempre las columnas explícitamente (no confíes en el orden). DEFAULT usa el valor por defecto de la columna. El id con AUTO_INCREMENT se genera automáticamente.
UPDATE Básico
UPDATE productos
SET precio = 99.90
WHERE id = 5;
-- Múltiples columnas
UPDATE clientes
SET email = 'nuevo@mail.com',
actualizado_em = NOW()
WHERE id = 10;UPDATE modifica filas existentes. Usa siempre WHERE para limitar las filas afectadas — sin él, actualiza todas. Prueba antes con un SELECT usando la misma condición.
REPLACE (MySQL)
-- Borra y reinserta si la clave existe REPLACE INTO productos (id, nombre, precio) VALUES (5, 'Ratón Pro', 39.90); -- Equivalente a: -- DELETE + INSERT (si clave duplicada) -- INSERT normal (si no existe)
REPLACE (MySQL) borra la fila existente e inserta una nueva si hay conflicto de clave. Diferente del upsert: las columnas no especificadas quedan con valores por defecto (no se mantienen). Prefiere ON DUPLICATE KEY UPDATE.
INSERT Múltiple
INSERT INTO productos (nombre, precio, stock)
VALUES
('Ratón', 25.90, 100),
('Teclado', 49.90, 50),
('Monitor', 299.00, 20);Inserta varias filas en un único INSERT separando los conjuntos de valores con comas. Mucho más rápido que múltiples INSERTs individuales (menos round-trips al servidor).
UPDATE con Cálculo y JOIN
-- Aumentar un 10% UPDATE productos SET precio = precio * 1.10 WHERE categoria = 'electrónica'; -- UPDATE con JOIN (MySQL) UPDATE pedidos p JOIN clientes c ON p.cliente_id = c.id SET p.descuento = 10 WHERE c.vip = 1; -- PostgreSQL UPDATE pedidos SET descuento = 10 FROM clientes c WHERE cliente_id = c.id AND c.vip = 1;
UPDATE puede usar la columna actual (precio = precio * 1.1) y JOINs para actualizar en base a otra tabla. La sintaxis de UPDATE JOIN varía entre MySQL y PostgreSQL.
INSERT de múltiples filas
-- Varias filas en un solo INSERT:
INSERT INTO productos (nombre, precio)
VALUES
('Teclado', 45.00),
('Ratón', 25.50),
('Monitor', 180.00);
-- Mucho más rápido que N INSERTs
-- (una única transacción)
-- INSERT desde SELECT:
INSERT INTO productos_arquivo
(nombre, precio)
SELECT nombre, precio
FROM productos
WHERE descontinuado = TRUE;Un INSERT con varias filas (VALUES (...), (...)) es mucho más rápido que INSERTs separados (una sola transacción). INSERT ... SELECT copia datos de otra tabla sin salir de SQL.
INSERT desde SELECT
-- Copiar datos entre tablas INSERT INTO arquivo_pedidos SELECT * FROM pedidos WHERE anio < 2020; -- Con columnas específicas INSERT INTO informe (nombre, total) SELECT nombre, SUM(total) FROM pedidos GROUP BY nombre;
INSERT ... SELECT copia los resultados de una consulta a otra tabla. Las columnas deben ser compatibles en tipo y orden. Ideal para archivos, informes y tablas de resumen.
DELETE
DELETE FROM clientes WHERE activo = 0; -- Con JOIN (MySQL) DELETE p FROM pedidos p JOIN clientes c ON p.cliente_id = c.id WHERE c.bloqueado = 1; -- Limpiar una tabla antigua DELETE FROM logs WHERE data < '2023-01-01' LIMIT 10000;
DELETE elimina filas. Usa siempre WHERE — sin él borra todo. LIMIT en DELETE (MySQL) permite borrar por lotes para no bloquear la tabla. En PostgreSQL, usa DELETE ... USING para JOINs.
UPDATE con JOIN
-- Actualizar en base a otra tabla: -- MySQL: UPDATE pedidos p JOIN clientes c ON c.id = p.cliente_id SET p.descuento = 10 WHERE c.vip = TRUE; -- PostgreSQL / SQL estándar: UPDATE pedidos SET descuento = 10 FROM clientes WHERE pedidos.cliente_id = clientes.id AND clientes.vip = TRUE; -- ¡La sintaxis varía entre SGBDs!
UPDATE con JOIN actualiza filas en base a otra tabla. La sintaxis difiere: MySQL usa UPDATE ... JOIN; PostgreSQL usa la cláusula FROM. Verifica la documentación de tu SGBD.
INSERT ON DUPLICATE KEY (Upsert)
-- MySQL: insertar o actualizar
INSERT INTO stats (producto_id, visualizaciones)
VALUES (42, 1)
ON DUPLICATE KEY UPDATE
visualizaciones = visualizaciones + 1;
-- PostgreSQL: ON CONFLICT
INSERT INTO stats (producto_id, visualizaciones)
VALUES (42, 1)
ON CONFLICT (producto_id)
DO UPDATE SET visualizaciones = stats.visualizaciones + 1;Un upsert inserta si no existe, o actualiza si ya existe. En MySQL usa ON DUPLICATE KEY UPDATE; en PostgreSQL usa ON CONFLICT ... DO UPDATE. Requiere una clave única/primaria.
TRUNCATE vs DELETE
-- TRUNCATE: elimina todo, reinicia AUTO_INCREMENT TRUNCATE TABLE logs; -- DELETE sin WHERE: elimina todo (más lento) DELETE FROM logs; -- TRUNCATE no dispara triggers -- TRUNCATE no puede tener WHERE -- TRUNCATE es DDL (no transaccional en MySQL)
TRUNCATE elimina todas las filas rápidamente y reinicia el AUTO_INCREMENT. No acepta WHERE, no dispara triggers y en MySQL no es transaccional. Usa DELETE cuando necesites condiciones o rollback.
Crear tablas y tipos
CREATE TABLE
CREATE TABLE productos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
precio DECIMAL(8,2) DEFAULT 0.00,
stock INT UNSIGNED DEFAULT 0,
activo BOOLEAN DEFAULT TRUE,
creado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);CREATE TABLE define columnas con tipo y constraints. La PRIMARY KEY identifica cada fila. DEFAULT establece el valor por defecto. AUTO_INCREMENT (MySQL) genera IDs secuenciales automáticamente.
Constraints de Integridad
CREATE TABLE contas (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
saldo DECIMAL(10,2) CHECK (saldo >= 0),
tipo ENUM('pessoal', 'empresa') DEFAULT 'pessoal',
creado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);Constraints: NOT NULL (obligatorio), UNIQUE (sin duplicados), CHECK (validación), DEFAULT (valor por defecto), ENUM (valores permitidos). Garantizan integridad a nivel de BD.
CREATE TABLE AS SELECT
-- Crear tabla a partir de una consulta CREATE TABLE clientes_vip AS SELECT nombre, email, total_gasto FROM clientes WHERE total_gasto > 1000; -- Solo estructura (sin datos) CREATE TABLE nueva LIKE clientes;
CREATE TABLE AS SELECT (CTAS) crea una tabla con los resultados de una consulta. No copia índices ni constraints — solo datos y tipos. LIKE copia la estructura completa (MySQL).
Tipos Numéricos
-- Enteros TINYINT (1 byte: -128 a 127) SMALLINT (2 bytes) INT (4 bytes: -2B a 2B) BIGINT (8 bytes) -- Decimales DECIMAL(10,2) -- exacto (¡dinero!) FLOAT -- aproximado (4 bytes) DOUBLE -- aproximado (8 bytes)
Usa DECIMAL para valores monetarios (exacto). FLOAT/DOUBLE son aproximados (errores de redondeo). INT es suficiente para la mayoría de los IDs. UNSIGNED duplica el límite positivo.
Clave Primaria y Foránea
CREATE TABLE pedidos (
id INT PRIMARY KEY AUTO_INCREMENT,
cliente_id INT NOT NULL,
total DECIMAL(8,2),
FOREIGN KEY (cliente_id)
REFERENCES clientes(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);La PRIMARY KEY identifica cada fila (única + no nula). La FOREIGN KEY enlaza a otra tabla. ON DELETE CASCADE borra los pedidos si se borra el cliente. ON DELETE SET NULL pone NULL en vez de borrar.
Tipos Especiales (ENUM, JSON, UUID)
-- ENUM: valores fijos
status ENUM('pendiente', 'pagado', 'enviado')
-- JSON (MySQL 5.7+, PostgreSQL)
datos JSON,
SELECT datos->>'$.nombre' FROM tabla;
-- UUID (PostgreSQL)
id UUID DEFAULT gen_random_uuid()
-- BOOLEAN (alias de TINYINT(1) en MySQL)
activo BOOLEAN DEFAULT TRUEENUM restringe a valores fijos (eficiente pero rígido). El tipo JSON guarda documentos flexibles. El UUID es un identificador global único (128 bits). BOOLEAN en MySQL es TINYINT(1) internamente.
Tipos de Texto
CHAR(10) -- tamaño fijo (códigos, siglas) VARCHAR(255) -- tamaño variable (nombres, emails) TEXT -- hasta 64KB (descripciones) MEDIUMTEXT -- hasta 16MB LONGTEXT -- hasta 4GB -- PostgreSQL VARCHAR(n), TEXT, CHAR(n)
CHAR tiene tamaño fijo (rellena con espacios). VARCHAR almacena solo lo necesario. TEXT para textos anchos (sin límite práctico). En PostgreSQL, TEXT y VARCHAR tienen igual rendimiento.
ALTER TABLE
-- Añadir columna ALTER TABLE clientes ADD telefono VARCHAR(20); -- Eliminar columna ALTER TABLE clientes DROP COLUMN fax; -- Modificar el tipo (MySQL) ALTER TABLE productos MODIFY precio DECIMAL(10,2); -- PostgreSQL ALTER TABLE productos ALTER COLUMN precio TYPE NUMERIC(10,2); -- Renombrar columna ALTER TABLE clientes RENAME COLUMN nombre TO nombre_completo;
ALTER TABLE modifica la estructura: añadir (ADD), eliminar (DROP), cambiar tipo (MODIFY/ALTER COLUMN) o renombrar columnas. En tablas grandes puede ser lento (recrea la tabla).
CREATE TABLE AS
-- Crear tabla a partir de una query: CREATE TABLE clientes_vip AS SELECT id, nombre, email FROM clientes WHERE total_gasto > 1000; -- Copiar estructura + datos de otra: CREATE TABLE clientes_backup AS SELECT * FROM clientes; -- Solo la estructura (sin datos): CREATE TABLE nueva AS SELECT * FROM clientes WHERE 1 = 0; -- La nueva tabla NO hereda índices, -- constraints ni claves primarias
CREATE TABLE AS SELECT (CTAS) crea una tabla a partir del resultado de una query, copiando estructura y datos. No hereda índices, constraints ni claves — añádelos después si es necesario.
Tipos de Fecha y Hora
DATE -- '2024-01-15' TIME -- '14:30:00' DATETIME -- '2024-01-15 14:30:00' TIMESTAMP -- como DATETIME + zona horaria YEAR -- 2024 -- PostgreSQL DATE, TIME, TIMESTAMP, TIMESTAMPTZ, INTERVAL
DATE guarda solo la fecha. DATETIME guarda fecha y hora. TIMESTAMP convierte a UTC al guardar (bueno para apps multi-zona). Elige el tipo más restrictivo que cubra la necesidad.
DROP y RENAME TABLE
-- Eliminar tabla (¡irreversible!) DROP TABLE IF EXISTS temp_logs; -- Renombrar tabla RENAME TABLE clientes TO clientes_antigos; -- MySQL: múltiples RENAME TABLE a TO a_new, b TO b_new; -- PostgreSQL ALTER TABLE clientes RENAME TO clientes_antigos;
DROP TABLE elimina la tabla y todos los datos. IF EXISTS evita el error si no existe. RENAME cambia el nombre sin perder datos. Cuidado: DROP es irreversible (sin ROLLBACK en DDL en MySQL).
Tablas temporales
-- Existe solo en la sesión actual: CREATE TEMPORARY TABLE tmp_totais ( cliente_id INT, total DECIMAL(10,2) ); INSERT INTO tmp_totais SELECT cliente_id, SUM(total) FROM pedidos GROUP BY cliente_id; -- Usar como cualquier tabla: SELECT * FROM tmp_totais WHERE total > 100; -- Eliminada automáticamente cuando -- la sesión/conexión termina DROP TEMPORARY TABLE IF EXISTS tmp_totais;
Las tablas TEMPORARY existen solo en la sesión actual y se eliminan al cerrar la conexión. Ideales para resultados intermedios complejos. Solo son visibles para quien las creó.
Vistas, índices y transacciones
Crear y Usar VIEW
CREATE VIEW clientes_ativos AS SELECT id, nombre, email FROM clientes WHERE activo = 1; -- Usar como tabla SELECT * FROM clientes_ativos WHERE ciudad = 'Lisbon'; -- Eliminar DROP VIEW IF EXISTS clientes_ativos;
Una VIEW es una consulta guardada que se usa como tabla virtual. No almacena datos (ejecuta la query al consultar). Útil para simplificar consultas complejas y restringir el acceso a columnas.
Transacciones (ACID)
START TRANSACTION; UPDATE contas SET saldo = saldo - 100 WHERE id = 1; UPDATE contas SET saldo = saldo + 100 WHERE id = 2; -- Si todo salió bien: COMMIT; -- Si hubo un error: ROLLBACK;
Una transacción agrupa operaciones atómicas: o todas tienen éxito (COMMIT) o ninguna se aplica (ROLLBACK). Garantiza consistencia (ej: transferencias). Requiere un motor transaccional (InnoDB en MySQL).
CTE Recursiva
WITH RECURSIVE hierarquia AS (
-- Ancla: nivel raíz
SELECT id, nombre, chefe_id, 1 AS nivel
FROM funcionarios WHERE chefe_id IS NULL
UNION ALL
-- Recursión: hijos
SELECT f.id, f.nombre, f.chefe_id, h.nivel + 1
FROM funcionarios f
JOIN hierarquia h ON f.chefe_id = h.id
)
SELECT * FROM hierarquia ORDER BY nivel;La CTE recursiva (WITH RECURSIVE) se referencia a sí misma. Tiene una parte ancla (caso base) y una recursiva. Ideal para jerarquías, árboles y grafos. Requiere UNION ALL entre las partes.
CTE (WITH)
-- Common Table Expression: WITH ventas_2024 AS ( SELECT cliente_id, SUM(total) AS total FROM pedidos WHERE YEAR(data) = 2024 GROUP BY cliente_id ) SELECT c.nombre, v.total FROM ventas_2024 v JOIN clientes c ON c.id = v.cliente_id WHERE v.total > 1000; -- Más legible que las subconsultas -- anidadas; puedes encadenar varias: -- WITH a AS (...), b AS (...)
CTE (WITH nombre AS (...)) crea una tabla temporal con nombre para la consulta — mucho más legible que las subconsultas anidadas. Puedes encadenar varias CTEs separadas por comas. Soportado en todos los SGBD modernos.
VIEW Actualizable
-- VIEW simple (actualizable)
CREATE VIEW vw_productos AS
SELECT id, nombre, precio FROM productos WHERE activo = 1;
-- INSERT/UPDATE a través de la VIEW
INSERT INTO vw_productos (nombre, precio) VALUES ('Nuevo', 10);
UPDATE vw_productos SET precio = 15 WHERE id = 5;
-- Con WITH CHECK OPTION (validación)
CREATE VIEW vw_ativos AS
SELECT * FROM clientes WHERE activo = 1
WITH CHECK OPTION;Las VIEWs simples (sin JOIN, GROUP BY, DISTINCT) son actualizables. WITH CHECK OPTION impide insertar/actualizar filas que no cumplan la condición de la VIEW.
SAVEPOINT
START TRANSACTION; INSERT INTO pedidos (cliente_id, total) VALUES (1, 50); SAVEPOINT depois_pedido; INSERT INTO ítems (pedido_id, producto_id) VALUES (99, 5); -- Error en el segundo INSERT: revertir solo hasta el savepoint ROLLBACK TO depois_pedido; -- El pedido se mantiene, el ítem no COMMIT;
SAVEPOINT crea un punto intermedio en la transacción. ROLLBACK TO revierte solo hasta ese punto (no deshace todo). Útil para operaciones por lotes donde un error no debe anular el trabajo anterior.
Window Functions (Básico)
SELECT nombre, departamento, salario,
RANK() OVER (PARTITION BY departamento ORDER BY salario DESC) AS rank,
AVG(salario) OVER (PARTITION BY departamento) AS media_dept
FROM funcionarios;
-- ROW_NUMBER vs RANK vs DENSE_RANK
ROW_NUMBER() -- 1,2,3,4 (sin empates)
RANK() -- 1,1,3,4 (salta)
DENSE_RANK() -- 1,1,2,3 (no salta)Las window functions calculan sobre un "grupo" sin colapsar filas. PARTITION BY define el grupo; ORDER BY el orden. RANK, ROW_NUMBER y DENSE_RANK difieren en el tratamiento de empates.
Window functions
-- Agregación SIN agrupar filas:
SELECT nombre, departamento, salario,
AVG(salario) OVER (
PARTITION BY departamento
) AS media_dept,
salario - AVG(salario) OVER (
PARTITION BY departamento
) AS diff
FROM empleados;
-- Ranking:
SELECT nombre,
ROW_NUMBER() OVER (
ORDER BY salario DESC
) AS posición
FROM empleados;
-- OVER() define la "ventana";
-- PARTITION BY divide en gruposLas window functions calculan agregados sobre un grupo de filas sin colapsarlas (a diferencia de GROUP BY). OVER (PARTITION BY ...) define la ventana. Incluye ROW_NUMBER, RANK, LAG, LEAD. MySQL 8+ y PostgreSQL.
Crear y Gestionar Índices
-- Índice simple CREATE INDEX idx_email ON clientes(email); -- Índice compuesto CREATE INDEX idx_cat_precio ON productos(categoria, precio); -- Índice único CREATE UNIQUE INDEX idx_cpf ON clientes(cpf); -- Eliminar DROP INDEX idx_email ON clientes; -- MySQL DROP INDEX idx_email; -- PostgreSQL
Los índices aceleran WHERE, JOIN y ORDER BY. El índice compuesto funciona para la primera columna (o ambas en orden). UNIQUE también garantiza unicidad. Los índices ocupan espacio y ralentizan INSERT/UPDATE.
Subconsultas (FROM, WHERE, SELECT)
-- En el WHERE (IN)
SELECT nombre FROM clientes
WHERE id IN (SELECT cliente_id FROM pedidos);
-- En el FROM (tabla derivada)
SELECT categoria, media FROM (
SELECT categoria, AVG(precio) AS media
FROM productos GROUP BY categoria
) t WHERE media > 50;
-- Correlacionada
SELECT nombre, (
SELECT COUNT(*) FROM pedidos p
WHERE p.cliente_id = c.id
) AS num_pedidos FROM clientes c;Las subconsultas pueden estar en el WHERE (filtro), en el FROM (tabla derivada) o en el SELECT (escalar). La correlacionada referencia la consulta exterior (se ejecuta por fila). Prefiere JOIN cuando sea posible por rendimiento.
Window Functions (Avanzado)
SELECT mes, total,
LAG(total, 1) OVER (ORDER BY mes) AS mes_anterior,
LEAD(total, 1) OVER (ORDER BY mes) AS mes_siguiente,
SUM(total) OVER (ORDER BY mes) AS acumulado,
total * 100.0 / SUM(total) OVER () AS porcentaje
FROM ventas_mensais;LAG/LEAD acceden a filas anteriores/siguientes. SUM() OVER (ORDER BY) calcula totales acumulados. El OVER () vacío se aplica a toda la tabla. Sustituye auto-JOINs complejos.
EXPLAIN (Plan de Ejecución)
EXPLAIN SELECT * FROM productos WHERE categoria = 'electrónica' AND precio > 100; -- MySQL: formato JSON EXPLAIN FORMAT=JSON SELECT ...; -- PostgreSQL: con métricas reales EXPLAIN ANALYZE SELECT ...;
EXPLAIN muestra cómo se ejecutará la consulta: si usa un índice, cuántas filas examina, el tipo de scan. type: ALL = full scan (malo). type: ref/range = usa índice (bueno). ANALYZE la ejecuta y muestra tiempos reales.
CTE (Common Table Expression)
WITH ventas_mensais AS (
SELECT MONTH(data) AS mes, SUM(total) AS total
FROM pedidos
WHERE YEAR(data) = 2024
GROUP BY MONTH(data)
)
SELECT mes, total,
total - LAG(total) OVER (ORDER BY mes) AS diferencia
FROM ventas_mensais;La CTE (WITH) crea una tabla temporal con nombre para la consulta. Más legible que las subconsultas anidadas. Puede referenciarse varias veces. Soportada en MySQL 8+, PostgreSQL, SQL Server.
Prepared Statements
-- MySQL
PREPARE stmt FROM 'SELECT * FROM clientes WHERE id = ?';
SET @id = 42;
EXECUTE stmt USING @id;
DEALLOCATE PREPARE stmt;
-- En la aplicación (PHP/PDO)
-- $stmt = $pdo->prepare('SELECT * FROM clientes WHERE id = ?');
-- $stmt->execute([$id]);Los prepared statements separan el SQL de los datos: previenen la SQL injection y permiten reutilizar el plan de ejecución. El ? es el placeholder. En la práctica, usa el driver del lenguaje (PDO, psycopg2) en vez de SQL puro.
Funciones útiles
Funciones de String
UPPER('hola') -- 'HOLA'
LOWER('HOLA') -- 'hola'
LENGTH('hola') -- 4 (bytes en MySQL)
CHAR_LENGTH('hola') -- 4 (caracteres)
TRIM(' hola ') -- 'hola'
SUBSTRING('hola', 1, 2) -- 'ho'
REPLACE('hola', 'á', 'a') -- 'hola'
LEFT('hola', 2) -- 'ho'
RIGHT('hola', 2) -- 'la'Funciones de manipulación de texto. LENGTH cuenta bytes; CHAR_LENGTH cuenta caracteres (importante con acentos/UTF-8). SUBSTRING(str, pos, len) es 1-based. TRIM elimina espacios en los extremos.
CAST y CONVERT
-- Conversión explícita de tipo
SELECT CAST('123' AS INT);
SELECT CAST(precio AS CHAR) FROM productos;
SELECT CAST('2024-01-15' AS DATE);
-- MySQL: CONVERT
SELECT CONVERT(nombre, CHAR) FROM clientes;
-- PostgreSQL: :: (abreviatura)
SELECT '123'::INT, precio::TEXT FROM productos;CAST convierte entre tipos explícitamente (estándar SQL). CONVERT es una alternativa (MySQL). En PostgreSQL, el operador :: es más conciso. Las conversiones implícitas pueden causar pérdida de precisión.
REGEXP (Expresiones Regulares)
-- MySQL
SELECT * FROM clientes
WHERE email REGEXP '^[a-z]+@[a-z]+\\.com$';
-- PostgreSQL (operador ~)
SELECT * FROM clientes
WHERE email ~ '^[a-z]+@[a-z]+\.com$';
-- MySQL 8: REGEXP_LIKE
WHERE REGEXP_LIKE(telefono, '^[0-9]{9}$');REGEXP (MySQL) o ~ (PostgreSQL) hace correspondencia por expresiones regulares. Más potente que LIKE pero más lento. Usa ^ (inicio), $ (fin), [0-9] (clase), {n} (repetición).
Funciones de Fecha
NOW() -- fecha y hora actual
CURDATE() -- solo fecha
YEAR('2024-03-15') -- 2024
MONTH('2024-03-15') -- 3
DAY('2024-03-15') -- 15
DATEDIFF('2024-12-31', '2024-01-01') -- 365
DATE_ADD(NOW(), INTERVAL 7 DAY) -- +7 días
DATE_FORMAT(NOW(), '%d/%m/%Y') -- '15/03/2024'Funciones para trabajar con fechas. DATEDIFF devuelve la diferencia en días. DATE_ADD/DATE_SUB suman/restan intervalos. DATE_FORMAT formatea la salida (MySQL). En PostgreSQL usa TO_CHAR.
IF e IIF (Condicional)
-- MySQL: IF(condición, valor_true, valor_false) SELECT IF(precio > 100, 'caro', 'barato') FROM productos; -- SQL Server: IIF SELECT IIF(stock > 0, 'disponible', 'agotado') FROM productos; -- Estándar SQL: CASE (funciona en todos) SELECT CASE WHEN precio > 100 THEN 'caro' ELSE 'barato' END FROM productos;
IF() (MySQL) e IIF() (SQL Server) son condicionales de 3 argumentos. CASE es el estándar SQL universal y más flexible (múltiples condiciones). Prefiere CASE por portabilidad.
Funciones de fecha
-- Fecha/hora actual:
SELECT NOW(); -- fecha + hora
SELECT CURDATE(); -- solo fecha (MySQL)
-- Extraer partes:
SELECT YEAR(data), MONTH(data), DAY(data)
FROM pedidos;
-- Aritmética de fechas (MySQL):
SELECT DATE_ADD(NOW(), INTERVAL 7 DAY);
SELECT DATEDIFF('2024-12-31', NOW());
-- PostgreSQL:
SELECT NOW() + INTERVAL '7 days';
SELECT EXTRACT(YEAR FROM data);
-- Formatear (MySQL):
SELECT DATE_FORMAT(NOW(), '%d/%m/%Y');Funciones de fecha: NOW()/CURDATE() dan la fecha actual; YEAR()/MONTH() extraen partes; DATE_ADD/INTERVAL hacen aritmética. La sintaxis varía entre MySQL y PostgreSQL.
Funciones Numéricas
ROUND(3.14159, 2) -- 3.14 CEIL(4.1) -- 5 FLOOR(4.9) -- 4 ABS(-10) -- 10 MOD(10, 3) -- 1 POWER(2, 10) -- 1024 SQRT(144) -- 12 RAND() -- aleatorio 0-1
ROUND redondea a N decimales. CEIL redondea hacia arriba, FLOOR hacia abajo. MOD devuelve el resto de la división. ROUND usa redondeo "banker's" en algunos SGBD (0.5 → par más cercano).
Funciones de Agregación de String
-- MySQL SELECT GROUP_CONCAT(nombre SEPARATOR '; ') FROM productos GROUP BY categoria; -- PostgreSQL SELECT STRING_AGG(nombre, '; ' ORDER BY nombre) FROM productos GROUP BY categoria; -- SQL Server SELECT STRING_AGG(nombre, '; ') FROM productos GROUP BY categoria;
Funciones que concatenan los valores de un grupo en una string. GROUP_CONCAT (MySQL), STRING_AGG (PostgreSQL/SQL Server). Aceptan un separador personalizado y ordenación interna.
CONCAT y LENGTH
-- Concatenar strings:
SELECT CONCAT(nombre, ' ', apellido) AS completo
FROM clientes;
-- Operador || (estándar SQL):
SELECT nombre || ' ' || apellido FROM clientes;
-- Tamaño de la string:
SELECT LENGTH(nombre) FROM clientes;
-- Mayúsculas/minúsculas:
SELECT UPPER(nombre), LOWER(email) FROM clientes;
-- Substring:
SELECT SUBSTRING(nombre, 1, 3) FROM clientes;
-- TRIM elimina espacios:
SELECT TRIM(' hola ');Funciones de string: CONCAT() o el operador || juntan strings; LENGTH() da el tamaño; UPPER/LOWER cambian la capitalización; SUBSTRING() extrae partes; TRIM() elimina espacios.
COALESCE e IFNULL
-- Devuelve el primero no-NULL SELECT COALESCE(telefono, móvil, 'sem contacto') FROM clientes; -- MySQL: IFNULL (solo 2 args) SELECT IFNULL(descuento, 0) FROM productos; -- PostgreSQL: NULLIF (inverso) SELECT NULLIF(stock, 0) FROM productos; -- Devuelve NULL si stock = 0 (evita división por cero)
COALESCE devuelve el primer valor no-NULL de la lista (estándar SQL). IFNULL es de MySQL (solo 2 args). NULLIF(a, b) devuelve NULL si a=b — útil para evitar la división por cero.
Funciones de Sistema y Metadata
-- Información de la sesión SELECT DATABASE(); -- BD actual SELECT USER(); -- usuario SELECT VERSION(); -- versión del SGBD SELECT LAST_INSERT_ID(); -- último AUTO_INCREMENT -- Listar tablas SHOW TABLES; -- MySQL \dt -- PostgreSQL (psql) -- Describir una tabla DESCRIBE clientes; -- MySQL \d clientes -- PostgreSQL (psql)
Las funciones de sistema devuelven información sobre la sesión y la estructura. LAST_INSERT_ID() devuelve el último ID generado. SHOW TABLES/DESCRIBE son comandos MySQL; en PostgreSQL usa \dt y \d en psql.
Consejos y buenas prácticas
Evitar SELECT *
-- Malo: trae todo (columnas innecesarias, más I/O) SELECT * FROM clientes; -- Bueno: solo lo necesario SELECT id, nombre, email FROM clientes; -- En JOINs, nunca uses * SELECT c.nombre, p.total FROM clientes c JOIN pedidos p ON c.id = p.cliente_id;
SELECT * trae columnas innecesarias, aumenta el tráfico de red e impide optimizaciones de índices covering. Lista siempre las columnas necesarias. En JOINs, puede traer columnas de tablas equivocadas.
Comentarios SQL
-- Comentario de línea (MySQL, PostgreSQL) # Comentario de línea (solo MySQL) /* Comentario de bloque (todos los SGBD) */ -- Consejo: documentar consultas complejas /* Informe mensual: suma ventas por categoría Excluye devoluciones (status != 3) */ SELECT categoria, SUM(total) FROM ventas WHERE status != 3 GROUP BY categoria;
Usa -- para comentarios de línea (estándar SQL), # (solo MySQL) y /* */ para bloques. Documenta las consultas complejas con el "porqué" y no el "qué". Útil para desactivar partes temporalmente.
Paginación eficiente (keyset)
-- Un OFFSET grande es LENTO -- (recorre y descarta N filas): SELECT * FROM pedidos ORDER BY id LIMIT 20 OFFSET 100000; -- La keyset pagination es RÁPIDA -- (usa el índice directamente): SELECT * FROM pedidos WHERE id > 100000 -- último id visto ORDER BY id LIMIT 20; -- Guarda el último id de la página -- y pásalo en la siguiente petición
Un OFFSET grande obliga a recorrer y descartar miles de filas (lento). La keyset pagination (WHERE id > último) usa el índice directamente — velocidad constante en cualquier página. Ideal para feeds y listados anchos.
Prevenir SQL Injection
-- NUNCA concatenar input: -- "SELECT * FROM users WHERE id = " + input ← PELIGRO -- SIEMPRE usar prepared statements: SELECT * FROM users WHERE id = ?; SELECT * FROM users WHERE email = ?; -- Validar y escapar en el lado de la aplicación -- Usar un ORM (Eloquent, SQLAlchemy, etc.)
Nunca concatenes input del usuario en SQL — permite SQL injection. Usa siempre prepared statements con placeholders (? o :nombre). Valida los tipos en el servidor. Prefiere un ORM con query builder.
Consultas N+1 y Rendimiento
-- Problema N+1: 1 query + N queries SELECT * FROM clientes; -- 1 SELECT * FROM pedidos WHERE cliente_id = 1; -- N SELECT * FROM pedidos WHERE cliente_id = 2; -- N... -- Solución: JOIN (1 query) SELECT c.nombre, p.total FROM clientes c LEFT JOIN pedidos p ON c.id = p.cliente_id; -- O: IN (2 queries) SELECT * FROM clientes; SELECT * FROM pedidos WHERE cliente_id IN (1,2,3,...);
El problema N+1 ocurre cuando haces 1 consulta para la lista + N para los relacionados. Solución: usa JOIN o WHERE IN con los IDs. En los ORMs, usa eager loading (with() en Eloquent).
EXPLAIN (analizar consultas)
-- Ver el plan de ejecución: EXPLAIN SELECT * FROM pedidos WHERE cliente_id = 42; -- Qué buscar: -- type: ALL -> recorre todo (¡malo!) -- type: ref/eq_ref -> usa índice (bueno) -- rows: estimación de filas leídas -- Con ANALYZE (MySQL 8+): EXPLAIN ANALYZE SELECT ...; -- muestra tiempos reales -- Si type=ALL en un filtro frecuente, -- falta un índice en esa columna
EXPLAIN muestra cómo se ejecutará la consulta. type: ALL significa un recorrido completo (falta índice); ref/eq_ref indican uso de índice. EXPLAIN ANALYZE da tiempos reales. La herramienta nº 1 de optimización.
Indexación Inteligente
-- Indexar columnas de WHERE y JOIN CREATE INDEX idx_email ON clientes(email); -- Índice compuesto: el orden importa CREATE INDEX idx_cat_precio ON productos(categoria, precio); -- Funciona para: WHERE categoria = X -- Funciona para: WHERE categoria = X AND precio > Y -- NO funciona para: WHERE precio > Y (solo) -- Verificar si usa el índice EXPLAIN SELECT * FROM productos WHERE categoria = 'x';
Indexa las columnas usadas en WHERE, JOIN y ORDER BY. En los índices compuestos, el orden importa: funciona para la primera columna (prefijo). No indexes todo — cada índice ralentiza INSERT/UPDATE.
Tipos Correctos = Rendimiento
-- Malo: VARCHAR para todo
código VARCHAR(255) -- si siempre son 10 chars
-- Bueno: tipo restrictivo
código CHAR(10) -- tamaño fijo
precio DECIMAL(8,2) -- no FLOAT para dinero
activo BOOLEAN -- no VARCHAR('sim'/'no')
data TIMESTAMP -- no VARCHAR('2024-01-15')Elige el tipo más restrictivo: CHAR para tamaño fijo, DECIMAL para dinero, BOOLEAN para flags, TIMESTAMP para fechas. Tipos correctos = menos espacio, índices más pequeños, comparaciones más rápidas.
Siempre WHERE en UPDATE/DELETE
-- PELIGRO: afecta a TODAS las filas UPDATE productos SET activo = 0; DELETE FROM logs; -- SEGURO: probar antes SELECT COUNT(*) FROM productos WHERE categoria = 'x'; UPDATE productos SET activo = 0 WHERE categoria = 'x'; -- Consejo: usar una transacción para probar START TRANSACTION; DELETE FROM logs WHERE data < '2023-01-01'; -- Verificar el resultado, después: -- COMMIT; o ROLLBACK;
Sin WHERE, UPDATE/DELETE afecta a todas las filas. Prueba siempre con un SELECT primero. Usa transacciones para poder revertir. En producción, haz backups antes de operaciones masivas.
Convenciones de Nomenclatura
-- Tablas: plural, snake_case clientes, pedidos, categorias_producto -- Columnas: singular, snake_case nombre, email, creado_em, cliente_id -- Claves: tabla_singular_id cliente_id, producto_id -- Índices: idx_tabla_columna idx_clientes_email, idx_pedidos_data -- Constraints: fk_, uk_, chk_ fk_pedidos_cliente, uk_clientes_email
Usa snake_case y nombres descriptivos. Tablas en plural (clientes), FKs como tabla_id. Índices con el prefijo idx_. Evita palabras reservadas (order, group, select). Consistencia > preferencia personal.