20 ejercicios intermedios y avanzados de SQL resueltos

Estos ejercicios suben el nivel porque obligan a decidir cómo relacionar tablas, qué agrupar y cuándo usar subconsultas o HAVING.

La dificultad ya no está tanto en recordar una sentencia, sino en descomponer un problema en etapas.

Antes de empezar: estrategia para consultas complejas

Una consulta avanzada suele construirse mejor en pasos:

  1. obtener primero las filas base;
  2. agregar los JOIN necesarios;
  3. verificar que no se multipliquen filas inesperadamente;
  4. agrupar;
  5. agregar funciones como SUM o COUNT;
  6. filtrar grupos con HAVING;
  7. recién después incorporar subconsultas o cálculos adicionales.

Si intentamos escribir todo de una vez, es mucho más difícil detectar dónde está el error.

1. Ventas en un rango de fechas

SELECT * FROM ventas
WHERE fecha BETWEEN '2026-01-01' AND '2026-12-31 23:59:59';

Explicación: BETWEEN permite definir un intervalo de fechas inclusivo. En campos DATETIME conviene pensar cuidadosamente el límite superior para no excluir registros del último día.

2. Total vendido por mes

SELECT YEAR(fecha) AS anio, MONTH(fecha) AS mes, SUM(total) AS total_vendido
FROM ventas
GROUP BY YEAR(fecha), MONTH(fecha)
ORDER BY anio, mes;

Explicación: Extraemos año y mes, agrupamos por ambos y sumamos el total. Agrupar solo por mes mezclaría, por ejemplo, enero de distintos años.

3. Cliente con más compras

SELECT c.id, c.nombre, c.apellido, COUNT(v.id) AS cantidad_compras
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellido
ORDER BY cantidad_compras DESC
LIMIT 1;

Explicación: COUNT mide cantidad de operaciones, no dinero gastado.

4. Clientes que gastaron más que el promedio

SELECT c.id,c.nombre,c.apellido,SUM(v.total) AS total_gastado
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id,c.nombre,c.apellido
HAVING SUM(v.total) > (
    SELECT AVG(total_cliente)
    FROM (SELECT SUM(total) AS total_cliente FROM ventas GROUP BY cliente_id) AS totales
);

Explicación: Primero calculamos el total por cliente y luego el promedio de esos totales. Hay dos niveles de agregación: uno dentro de la subconsulta y otro en la consulta exterior.

5. Producto más vendido

SELECT p.id,p.nombre,SUM(vd.cantidad) AS unidades
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id,p.nombre
ORDER BY unidades DESC
LIMIT 1;

Explicación: La métrica es cantidad de unidades, no facturación.

6. Producto con mayor facturación

SELECT p.nombre,SUM(vd.subtotal) AS facturacion
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id,p.nombre
ORDER BY facturacion DESC
LIMIT 1;

Explicación: Aquí la métrica cambia a dinero generado.

7. Categorías sin productos

SELECT c.id,c.nombre
FROM categorias AS c
LEFT JOIN productos AS p ON p.categoria_id = c.id
WHERE p.id IS NULL;

Explicación: LEFT JOIN permite detectar ausencia de relación.

8. Clientes que nunca compraron

SELECT c.id,c.nombre,c.apellido
FROM clientes AS c
WHERE NOT EXISTS (
    SELECT 1 FROM ventas AS v WHERE v.cliente_id = c.id
);

Explicación: NOT EXISTS expresa directamente que no debe existir ninguna venta relacionada. La subconsulta está correlacionada porque utiliza c.id de la fila externa.

9. Ticket promedio por cliente

SELECT c.id,c.nombre,c.apellido,AVG(v.total) AS ticket_promedio
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id,c.nombre,c.apellido;

Explicación: AVG calcula cuánto gasta en promedio el cliente por operación.

10. Categorías con más de 5 productos

SELECT c.id,c.nombre,COUNT(p.id) AS cantidad_productos
FROM categorias AS c
INNER JOIN productos AS p ON p.categoria_id = c.id
GROUP BY c.id,c.nombre
HAVING COUNT(p.id) > 5;

Explicación: HAVING es necesario porque filtramos sobre un COUNT agregado.

11. Total vendido por ciudad

SELECT c.ciudad,SUM(v.total) AS total_vendido
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.ciudad
ORDER BY total_vendido DESC;

Explicación: La ciudad proviene del cliente y el importe de ventas.

12. Última compra de cada cliente

SELECT c.id,c.nombre,c.apellido,MAX(v.fecha) AS ultima_compra
FROM clientes AS c
LEFT JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id,c.nombre,c.apellido;

Explicación: MAX(fecha) obtiene la fecha más reciente de cada grupo.

13. Ventas con más de 3 productos distintos

SELECT venta_id,COUNT(DISTINCT producto_id) AS productos_distintos
FROM ventas_detalles
GROUP BY venta_id
HAVING COUNT(DISTINCT producto_id) > 3;

Explicación: DISTINCT evita contar dos veces el mismo producto dentro de la misma venta.

14. Clientes que compraron en más de una categoría

SELECT c.id,c.nombre,c.apellido,COUNT(DISTINCT p.categoria_id) AS categorias_compradas
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
INNER JOIN ventas_detalles AS vd ON vd.venta_id = v.id
INNER JOIN productos AS p ON p.id = vd.producto_id
GROUP BY c.id,c.nombre,c.apellido
HAVING COUNT(DISTINCT p.categoria_id) > 1;

Explicación: Hay que recorrer cliente → venta → detalle → producto para conocer categorías. Este ejercicio muestra por qué es importante entender el camino entre entidades antes de escribir JOIN.

15. Porcentaje de facturación por categoría

SELECT c.nombre,SUM(vd.subtotal) AS total_categoria,
ROUND(SUM(vd.subtotal)*100/(SELECT SUM(subtotal) FROM ventas_detalles),2) AS porcentaje
FROM categorias AS c
INNER JOIN productos AS p ON p.categoria_id = c.id
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY c.id,c.nombre;

Explicación: Dividimos la facturación de cada categoría por la facturación total. La subconsulta calcula una sola vez el total conceptual contra el que se compara cada grupo.

16. Clientes con compras en distintos meses

SELECT c.id,c.nombre,c.apellido,
COUNT(DISTINCT DATE_FORMAT(v.fecha,'%Y-%m')) AS meses_con_compras
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id,c.nombre,c.apellido
HAVING meses_con_compras > 1;

Explicación: Convertimos cada fecha al período año-mes y contamos períodos distintos.

17. Stock menor que unidades vendidas

SELECT p.id,p.nombre,p.stock,SUM(vd.cantidad) AS unidades_vendidas
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id,p.nombre,p.stock
HAVING p.stock < SUM(vd.cantidad);

Explicación: Comparamos una columna del grupo con un valor agregado.

18. Resumen general por cliente

SELECT c.id,c.nombre,c.apellido,
COUNT(v.id) AS cantidad_ventas,
COALESCE(SUM(v.total),0) AS total_gastado,
COALESCE(AVG(v.total),0) AS ticket_promedio,
MAX(v.fecha) AS ultima_compra
FROM clientes AS c
LEFT JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id,c.nombre,c.apellido
ORDER BY total_gastado DESC;

Explicación: COALESCE reemplaza NULL por cero para clientes sin ventas. Como usamos LEFT JOIN, los clientes sin operaciones siguen apareciendo en el reporte.

19. Desafío: producto más vendido por categoría

-- Intentá resolverlo combinando GROUP BY,
-- SUM(cantidad) y una subconsulta o función de ventana.

Explicación: El desafío consiste en obtener el máximo dentro de cada categoría, no el máximo global.

20. Desafío final

-- Reporte por categoría:
-- cantidad de productos
-- unidades vendidas
-- facturación
-- precio promedio
-- producto más vendido

Explicación: Este ejercicio combina relaciones, agregaciones y ranking por grupo.

Cómo depurar una consulta avanzada

Cuando una consulta devuelve números incorrectos, no siempre hay un error de sintaxis. Un JOIN puede multiplicar filas y alterar SUM o COUNT.

Una técnica útil es ir agregando columnas de identificación temporalmente para observar qué filas está produciendo cada unión.

Consejo para resolver consultas avanzadas

  1. escribí primero la consulta más simple posible;
  2. probá cada JOIN por separado;
  3. agregá agrupaciones después;
  4. verificá la subconsulta de forma independiente;
  5. recién al final optimizá.

Primero buscá una consulta correcta y comprensible. Después utilizá índices y EXPLAIN para estudiar su rendimiento.

¿Qué sigue?

Con esta base ya podés comenzar a conectar MySQL con PHP u otro lenguaje.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← 20 ejercicios de SQL resueltos

20 ejercicios de SQL resueltos: SELECT, WHERE, JOIN, GROUP BY y subconsultas

Estos 20 ejercicios permiten practicar SQL de forma progresiva. Antes de mirar cada solución, intentá resolver el enunciado por tu cuenta.

Base de datos utilizada

Trabajamos con clientes, categorías, productos, ventas y ventas_detalles.

Las relaciones principales son:

  • productos.categoria_id → categorias.id;
  • ventas.cliente_id → clientes.id;
  • ventas_detalles.venta_id → ventas.id;
  • ventas_detalles.producto_id → productos.id.

Conviene tener este mapa mental antes de resolver los ejercicios con JOIN.

Cómo leer cada ejercicio

Antes de mirar la solución, intentá responder:

  1. ¿qué resultado final necesito?
  2. ¿qué tabla tiene esa información?
  3. ¿necesito filtrar filas?
  4. ¿necesito relacionar tablas?
  5. ¿necesito agrupar o calcular un resumen?

Resolver SQL es, en gran parte, aprender a traducir una pregunta del negocio a esas decisiones.

1. Mostrar todos los clientes

SELECT * FROM clientes;

Explicación: Usamos SELECT * porque queremos todas las columnas de todas las filas.

2. Mostrar nombre y apellido

SELECT nombre, apellido FROM clientes;

Explicación: Seleccionar solo las columnas necesarias hace la consulta más clara.

3. Clientes de Santa Rosa

SELECT * FROM clientes WHERE ciudad = 'Santa Rosa';

Explicación: WHERE filtra las filas por ciudad. La consulta primero parte de todos los clientes y después conserva solamente los que cumplen la condición.

4. Productos con precio mayor a 100000

SELECT nombre, precio FROM productos WHERE precio > 100000;

Explicación: El operador > compara valores numéricos.

5. Productos con stock bajo

SELECT nombre, stock FROM productos WHERE stock < 10;

Explicación: Este tipo de consulta puede utilizarse para alertas de reposición.

6. Ordenar productos por precio

SELECT nombre, precio FROM productos ORDER BY precio DESC;

Explicación: DESC ordena del valor más alto al más bajo.

7. Tres productos más caros

SELECT nombre, precio FROM productos ORDER BY precio DESC LIMIT 3;

Explicación: Primero ordenamos y luego LIMIT conserva solo las tres primeras filas.

8. Contar productos

SELECT COUNT(*) AS total_productos FROM productos;

Explicación: COUNT(*) devuelve cuántas filas tiene la tabla.

9. Precio promedio

SELECT AVG(precio) AS precio_promedio FROM productos;

Explicación: AVG calcula la media aritmética de la columna.

10. Productos por categoría

SELECT categoria_id, COUNT(*) AS cantidad
FROM productos
GROUP BY categoria_id;

Explicación: GROUP BY crea un grupo por categoría y COUNT cuenta sus productos. El resultado tendrá una fila por cada categoria_id, no una fila por cada producto.

11. Productos con categoría

SELECT p.nombre AS producto, c.nombre AS categoria
FROM productos AS p
INNER JOIN categorias AS c ON p.categoria_id = c.id;

Explicación: JOIN combina cada producto con la categoría relacionada. La condición p.categoria_id = c.id indica exactamente qué filas deben emparejarse.

12. Ventas con nombre del cliente

SELECT v.id, v.fecha, c.nombre, c.apellido, v.total
FROM ventas AS v
INNER JOIN clientes AS c ON v.cliente_id = c.id;

Explicación: Reemplazamos el identificador del cliente por información legible.

13. Total comprado por cliente

SELECT c.id, c.nombre, c.apellido, SUM(v.total) AS total_comprado
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellido;

Explicación: SUM acumula las ventas dentro de cada grupo de cliente. Primero JOIN conecta ventas con clientes; después GROUP BY reúne las ventas de la misma persona y finalmente SUM calcula el total.

14. Clientes que gastaron más de 100000

SELECT c.nombre, c.apellido, SUM(v.total) AS total_comprado
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellido
HAVING SUM(v.total) > 100000;

Explicación: HAVING filtra después de calcular la suma por cliente. No usamos WHERE para SUM(v.total) porque ese valor todavía no existe antes de formar los grupos.

15. Clientes sin ventas

SELECT c.id, c.nombre, c.apellido
FROM clientes AS c
LEFT JOIN ventas AS v ON v.cliente_id = c.id
WHERE v.id IS NULL;

Explicación: LEFT JOIN conserva clientes sin coincidencia; NULL identifica los que nunca compraron. Es un patrón muy usado para buscar “registros sin relación”.

16. Productos más vendidos

SELECT p.nombre, SUM(vd.cantidad) AS unidades_vendidas
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id, p.nombre
ORDER BY unidades_vendidas DESC;

Explicación: Sumamos cantidades por producto y ordenamos el ranking.

17. Producto con mayor facturación

SELECT p.nombre, SUM(vd.subtotal) AS facturacion
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id, p.nombre
ORDER BY facturacion DESC
LIMIT 1;

Explicación: El primer registro del ranking es el producto con mayor facturación.

18. Ventas por encima del promedio

SELECT id, cliente_id, total
FROM ventas
WHERE total > (SELECT AVG(total) FROM ventas);

Explicación: La subconsulta calcula el promedio y la consulta externa compara cada venta. Conviene ejecutar primero la subconsulta sola para comprobar qué valor produce.

19. Productos nunca vendidos

SELECT p.id, p.nombre
FROM productos AS p
LEFT JOIN ventas_detalles AS vd ON vd.producto_id = p.id
WHERE vd.id IS NULL;

Explicación: Buscamos productos que no tienen ninguna fila relacionada en detalles.

20. Cliente que más gastó

SELECT c.id, c.nombre, c.apellido, SUM(v.total) AS total_gastado
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellido
ORDER BY total_gastado DESC
LIMIT 1;

Explicación: Agrupamos, sumamos, ordenamos y conservamos el primer cliente.

Qué conceptos aparecen en estos 20 ejercicios

  • selección de columnas;
  • filtros con WHERE;
  • orden y límites;
  • funciones de agregación;
  • GROUP BY y HAVING;
  • INNER JOIN y LEFT JOIN;
  • subconsultas;
  • rankings simples.

Cómo practicar

  1. identificá qué tabla contiene el dato;
  2. decidí si necesitás WHERE;
  3. si hay varias tablas, buscá la relación;
  4. si necesitás resumir, pensá en GROUP BY;
  5. recién después escribí SQL.

¿Qué sigue?

20 ejercicios intermedios y avanzados.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Proyecto práctico SQL/MySQL

20 ejercicios intermedios y avanzados →

Proyecto práctico SQL/MySQL: sistema de ventas con tablas, relaciones, JOIN, reportes e índices

En este proyecto vamos a aplicar los principales conceptos de SQL y MySQL construyendo un pequeño sistema de ventas. La idea no es solamente copiar consultas, sino entender por qué existe cada tabla y cómo se relacionan.

El objetivo del proyecto

Queremos que al finalizar puedas mirar un requerimiento simple y convertirlo en tablas relacionadas, además de construir consultas de reporte sobre ellas.

Este proyecto integra lo visto anteriormente en lugar de presentar conceptos aislados.

¿Qué vamos a modelar?

El sistema manejará:

  • clientes;
  • categorías;
  • productos;
  • ventas;
  • detalle de cada venta.

¿Por qué varias tablas?

Separar entidades evita duplicación. Un cliente puede tener muchas ventas, una categoría muchos productos y una venta muchos detalles.

Las relaciones principales son:

  • cliente 1 → N ventas;
  • categoría 1 → N productos;
  • venta 1 → N detalles;
  • producto 1 → N detalles.

El detalle actúa como puente entre la venta y los productos comprados.

Crear la base

CREATE DATABASE tienda;
USE tienda;

Tabla clientes

Guarda la información propia del cliente. email es UNIQUE para evitar duplicados.

CREATE TABLE clientes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    apellido VARCHAR(100) NOT NULL,
    email VARCHAR(150) UNIQUE,
    ciudad VARCHAR(100),
    activo BOOLEAN DEFAULT TRUE,
    fecha_alta DATETIME DEFAULT CURRENT_TIMESTAMP
);

Tabla categorias

Las categorías se separan para no repetir el nombre en cada producto.

CREATE TABLE categorias (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL UNIQUE
);

Tabla productos

categoria_id conecta el producto con su categoría.

CREATE TABLE productos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(150) NOT NULL,
    precio DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    categoria_id INT NOT NULL,
    activo BOOLEAN DEFAULT TRUE,
    FOREIGN KEY (categoria_id) REFERENCES categorias(id)
);

Tabla ventas

Representa la cabecera de una operación. Guarda cliente, fecha y total.

CREATE TABLE ventas (
    id INT AUTO_INCREMENT PRIMARY KEY,
    cliente_id INT NOT NULL,
    fecha DATETIME DEFAULT CURRENT_TIMESTAMP,
    total DECIMAL(12,2) NOT NULL DEFAULT 0,
    FOREIGN KEY (cliente_id) REFERENCES clientes(id)
);

Cabecera y detalle

Este patrón aparece en facturas, pedidos, presupuestos y remitos. La cabecera representa la operación general; el detalle representa sus ítems.

Una venta puede tener diez productos sin repetir diez veces la fecha, el cliente y otros datos generales.

Tabla ventas_detalles

Cada fila representa un producto dentro de una venta. Guardamos el precio unitario de ese momento para conservar el valor histórico.

CREATE TABLE ventas_detalles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    venta_id INT NOT NULL,
    producto_id INT NOT NULL,
    cantidad INT NOT NULL,
    precio_unitario DECIMAL(10,2) NOT NULL,
    subtotal DECIMAL(12,2) NOT NULL,
    FOREIGN KEY (venta_id) REFERENCES ventas(id) ON DELETE CASCADE,
    FOREIGN KEY (producto_id) REFERENCES productos(id)
);

Registrar una venta con transacción

Venta, detalle y stock deben mantenerse consistentes. Por eso usamos una transacción.

Si se crea la venta pero falla el descuento de stock, no queremos dejar una operación parcialmente registrada.

START TRANSACTION;

INSERT INTO ventas (cliente_id, total)
VALUES (1, 0);

SET @venta_id = LAST_INSERT_ID();

INSERT INTO ventas_detalles (venta_id, producto_id, cantidad, precio_unitario, subtotal)
VALUES
(@venta_id, 3, 2, 95000, 190000),
(@venta_id, 4, 1, 45000, 45000);

UPDATE productos SET stock = stock - 2 WHERE id = 3;
UPDATE productos SET stock = stock - 1 WHERE id = 4;
UPDATE ventas SET total = 235000 WHERE id = @venta_id;

COMMIT;

¿Qué hace LAST_INSERT_ID()?

Devuelve el último identificador AUTO_INCREMENT generado en esa conexión. Lo usamos para saber qué venta acabamos de crear.

Es importante porque los detalles necesitan conocer el id de la cabecera recién insertada.

De guardar datos a obtener información

El diseño de tablas es solo la primera parte. El verdadero valor aparece cuando combinamos esas tablas para responder preguntas del negocio.

Consultar ventas con clientes

SELECT v.id, v.fecha, c.nombre, c.apellido, v.total
FROM ventas AS v
INNER JOIN clientes AS c
    ON v.cliente_id = c.id
ORDER BY v.fecha DESC;

JOIN permite reemplazar el cliente_id por información legible del cliente.

Detalle completo de una venta

SELECT v.id AS venta,
       c.nombre AS cliente,
       p.nombre AS producto,
       vd.cantidad,
       vd.precio_unitario,
       vd.subtotal
FROM ventas AS v
INNER JOIN clientes AS c ON v.cliente_id = c.id
INNER JOIN ventas_detalles AS vd ON vd.venta_id = v.id
INNER JOIN productos AS p ON vd.producto_id = p.id;

Total comprado por cliente

Agrupamos por cliente y sumamos sus ventas.

SELECT c.id, c.nombre, c.apellido,
       SUM(v.total) AS total_comprado
FROM clientes AS c
INNER JOIN ventas AS v ON v.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellido
ORDER BY total_comprado DESC;

Productos más vendidos

SELECT p.nombre,
       SUM(vd.cantidad) AS unidades_vendidas
FROM productos AS p
INNER JOIN ventas_detalles AS vd ON vd.producto_id = p.id
GROUP BY p.id, p.nombre
ORDER BY unidades_vendidas DESC;

Clientes sin ventas

LEFT JOIN conserva todos los clientes y luego filtramos los que no tienen coincidencia.

SELECT c.id, c.nombre, c.apellido
FROM clientes AS c
LEFT JOIN ventas AS v ON v.cliente_id = c.id
WHERE v.id IS NULL;

Ventas superiores al promedio

SELECT id, cliente_id, total
FROM ventas
WHERE total > (
    SELECT AVG(total)
    FROM ventas
);

Validar stock antes de vender

El ejemplo descuenta stock directamente para concentrarse en SQL. En una aplicación real deberíamos verificar que exista cantidad suficiente y manejar concurrencia para evitar vender más unidades de las disponibles.

Índices

Estas columnas participan frecuentemente en JOIN y filtros.

No agregamos índices “porque sí”: los elegimos en función de las consultas reales del sistema.

CREATE INDEX idx_productos_categoria ON productos(categoria_id);
CREATE INDEX idx_ventas_cliente_fecha ON ventas(cliente_id, fecha);
CREATE INDEX idx_detalles_producto ON ventas_detalles(producto_id);

Qué conceptos aplicamos

  • normalización;
  • claves primarias y foráneas;
  • INSERT y UPDATE;
  • transacciones;
  • JOIN;
  • GROUP BY;
  • subconsultas;
  • índices.

Ejercicios de ampliación

  1. agregar formas de pago;
  2. crear reporte mensual;
  3. obtener cliente que más gastó;
  4. detectar productos nunca vendidos;
  5. registrar devoluciones.

¿Qué sigue?

20 ejercicios de SQL resueltos.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Normalización de bases de datos

20 ejercicios de SQL resueltos →

Normalización de bases de datos: 1FN, 2FN y 3FN con ejemplos

La normalización es un proceso de diseño que busca organizar los datos para reducir redundancias e inconsistencias.

Normalización no es solo teoría

Las formas normales intentan evitar problemas reales de mantenimiento. Cuando una tabla mezcla demasiados conceptos, los errores aparecen al insertar, actualizar o eliminar.

La normalización obliga a preguntarnos de qué depende cada dato.

¿Qué problema resuelve?

Cuando la misma información se repite en muchas filas, actualizarla puede volverse peligroso. También puede ser difícil insertar o eliminar datos sin efectos no deseados.

Anomalía de actualización

El mismo dato aparece varias veces y debe modificarse en todas.

Ejemplo: si el teléfono de un profesor está repetido en 50 filas de alumnos y cambia, debemos actualizar las 50. Si una queda distinta, la base ya es inconsistente.

Anomalía de inserción

No podemos guardar una información sin crear otra que no debería ser necesaria.

Por ejemplo, no deberíamos necesitar inventar un alumno para poder registrar un nuevo curso.

Anomalía de eliminación

Al eliminar una fila perdemos información adicional.

Si el único registro del curso “SQL” estuviera mezclado con un alumno y eliminamos ese alumno, podríamos perder también la información del curso.

Primera Forma Normal: 1FN

Una tabla cumple 1FN cuando cada celda contiene un valor atómico y no listas de valores.

“Atómico” significa que, para el modelo que estamos diseñando, el valor se trata como una unidad. Guardar varios teléfonos separados por comas rompe esa idea porque luego es difícil buscar, validar o relacionar cada teléfono.

id | nombre | telefonos
1  | Ana    | 123456, 654321

Ese campo mezcla dos teléfonos. Es mejor crear otra tabla.

Separación correcta

alumnos
-------
id
nombre

telefonos_alumnos
-----------------
id
alumno_id
telefono

Dependencia funcional

Decimos que un dato depende de otro cuando conocer la clave permite determinar ese valor.

Por ejemplo, si alumno_id = 10 identifica a Ana, entonces el nombre depende de alumno_id.

Segunda Forma Normal: 2FN

Además de cumplir 1FN, todos los atributos no clave deben depender de toda la clave primaria.

El problema aparece especialmente con claves compuestas.

Si la clave está formada por dos columnas, un atributo no clave no debería depender solo de una mitad de esa clave.

alumno_id | curso_id | alumno_nombre | curso_nombre | nota

Si la clave es (alumno_id, curso_id), alumno_nombre depende solo de alumno_id y curso_nombre solo de curso_id.

Por qué separar en tres tablas

alumnos guarda datos propios del alumno, cursos datos propios del curso e inscripciones datos de la relación, como nota o fecha de inscripción.

Cada dato queda entonces en el lugar del que realmente depende.

Solución 2FN

Separar alumnos, cursos e inscripciones.

Tercera Forma Normal: 3FN

Además de cumplir 2FN, un atributo no clave no debería depender de otro atributo no clave.

alumno_id | nombre | ciudad_id | ciudad_nombre | provincia

El nombre de ciudad y provincia dependen de ciudad_id, no directamente del alumno.

Dependencia transitiva

Ocurre cuando A determina B y B determina C. Entonces C depende indirectamente de A.

En términos prácticos: si un dato describe a otra entidad intermedia y no a la fila principal, probablemente merece su propia tabla.

Muchos a muchos

Se resuelve habitualmente con una tabla intermedia.

CREATE TABLE alumnos_cursos (
    alumno_id INT NOT NULL,
    curso_id INT NOT NULL,
    fecha_inscripcion DATE,
    PRIMARY KEY (alumno_id, curso_id),
    FOREIGN KEY (alumno_id) REFERENCES alumnos(id),
    FOREIGN KEY (curso_id) REFERENCES cursos(id)
);

¿Hasta dónde normalizar?

En aplicaciones transaccionales, llegar a 3FN suele ser una base razonable de diseño. Existen formas normales más avanzadas, pero no siempre hacen falta para sistemas comunes.

Normalizar no significa dividir todo

El objetivo es mejorar consistencia y diseño, no crear tablas sin necesidad.

Desnormalización

A veces se duplica información intencionalmente para rendimiento o reportes. Debe ser una decisión consciente y medida.

Ejemplo histórico

En una venta conviene guardar precio_unitario aunque el producto ya tenga un precio actual, porque necesitamos conservar el valor histórico cobrado.

Resumen

  • 1FN: valores atómicos.
  • 2FN: sin dependencias parciales.
  • 3FN: sin dependencias transitivas.

Errores comunes

  • pensar que normalizar es solo crear más tablas;
  • guardar listas separadas por comas;
  • duplicar datos sin motivo;
  • desnormalizar antes de medir rendimiento.

Ejercicios

  1. normalizar teléfonos múltiples;
  2. separar alumnos y cursos;
  3. identificar dependencia parcial;
  4. identificar dependencia transitiva;
  5. diseñar clientes, pedidos y productos.

¿Qué sigue?

Proyecto práctico completo.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Triggers en MySQL

Proyecto práctico SQL/MySQL →

Triggers en MySQL: BEFORE, AFTER, INSERT, UPDATE y DELETE con ejemplos

Un trigger es una acción automática que MySQL ejecuta cuando ocurre un evento sobre una tabla.

¿Qué problema resuelve?

Permite ejecutar lógica automáticamente ante inserciones, modificaciones o eliminaciones, incluso si la operación proviene de distintas aplicaciones.

¿Cuándo conviene un trigger?

Un trigger es útil cuando una regla debe ejecutarse automáticamente sin importar qué aplicación haya realizado el cambio.

Ejemplos típicos:

  • registrar auditorías;
  • guardar históricos;
  • validar ciertas condiciones;
  • mantener datos derivados muy controlados.

La contrapartida es que la lógica queda “oculta” detrás de la operación principal, por eso conviene usarlos con criterio.

Eventos disponibles

  • INSERT
  • UPDATE
  • DELETE

BEFORE y AFTER

  • BEFORE: antes del cambio.
  • AFTER: después de que el cambio fue realizado.

Qué valores existen en cada evento

No siempre están disponibles OLD y NEW:

  • INSERT: existe NEW;
  • UPDATE: existen OLD y NEW;
  • DELETE: existe OLD.

NEW

NEW representa los valores nuevos de la fila.

En un BEFORE INSERT o BEFORE UPDATE incluso podemos modificar ciertos valores de NEW antes de que se guarden.

OLD

OLD representa los valores anteriores.

Es especialmente útil para auditorías porque permite comparar qué tenía una columna antes y qué tendrá después.

BEFORE INSERT

DELIMITER //
CREATE TRIGGER trg_productos_stock_insert
BEFORE INSERT ON productos
FOR EACH ROW
BEGIN
    IF NEW.stock < 0 THEN
        SET NEW.stock = 0;
    END IF;
END //
DELIMITER ;

BEFORE INSERT: corregir o rechazar

En el ejemplo anterior elegimos corregir un stock negativo y transformarlo en cero. En otros casos puede ser mejor rechazar la operación con SIGNAL.

Corregir silenciosamente y bloquear con error son decisiones distintas de negocio.

BEFORE UPDATE

Sirve para validar o transformar los nuevos valores antes de guardarlos.

Comparar OLD y NEW

IF OLD.precio <> NEW.precio THEN
    INSERT INTO precios_auditoria(producto_id, precio_anterior, precio_nuevo)
    VALUES(NEW.id, OLD.precio, NEW.precio);
END IF;

Auditoría con OLD y NEW

Si queremos registrar cambios de precio, podemos guardar ambos valores en una tabla de auditoría. Así queda trazabilidad de qué cambió.

AFTER INSERT

Es útil para auditorías o acciones posteriores al alta.

En AFTER el cambio principal ya ocurrió, por lo que se usa normalmente para registrar consecuencias, no para modificar el valor que se está insertando.

BEFORE DELETE

Podemos guardar un registro histórico antes de eliminar la fila.

SIGNAL

Permite generar un error y bloquear una operación inválida.

IF NEW.stock < 0 THEN
    SIGNAL SQLSTATE '45000'
    SET MESSAGE_TEXT = 'El stock no puede ser negativo';
END IF;

FOR EACH ROW

Un trigger se ejecuta una vez por cada fila afectada. Si un UPDATE modifica 10.000 filas, el trigger puede ejecutarse 10.000 veces.

Ver triggers

SHOW TRIGGERS;

Eliminar trigger

DROP TRIGGER IF EXISTS trg_productos_stock_insert;

Triggers dentro de transacciones

Las operaciones que ejecuta un trigger forman parte del mismo contexto transaccional de la sentencia que lo disparó. Si la operación principal se revierte, sus efectos asociados también pueden revertirse según el motor y las tablas involucradas.

Trigger o restricción

Si una regla puede resolverse con NOT NULL, UNIQUE, CHECK o FOREIGN KEY, normalmente esas opciones son más simples y explícitas.

El problema de la lógica oculta

Una sentencia UPDATE puede aparentar modificar una tabla, pero un trigger podría generar efectos adicionales. Por eso deben documentarse bien.

Errores comunes

  • usar triggers para todo;
  • crear efectos secundarios difíciles de detectar;
  • ignorar el costo por fila;
  • duplicar reglas que ya existen como restricciones.

Ejercicios

  1. impedir precios negativos;
  2. registrar cambios de precio;
  3. guardar filas antes de DELETE;
  4. crear historial de stock.

¿Qué sigue?

Normalización de bases de datos.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Funciones almacenadas

Normalización de bases de datos →

Funciones almacenadas en MySQL: CREATE FUNCTION, RETURN y diferencias con PROCEDURE

Una función almacenada es un bloque de lógica guardado en MySQL que recibe parámetros y devuelve un valor.

¿Qué problema resuelve una función?

Una función encapsula un cálculo que produce un valor. Ese cálculo puede reutilizarse en SELECT, WHERE, ORDER BY u otras expresiones.

Ejemplos típicos: calcular un impuesto, aplicar una regla matemática o devolver una clasificación.

¿En qué se diferencia de un procedimiento?

Una función se usa dentro de expresiones SQL y devuelve un único valor. Un procedimiento se ejecuta con CALL y está más orientado a procesos.

La forma de pensarla es similar a una función de programación tradicional: recibe argumentos y produce un resultado.

Primera función

DELIMITER //
CREATE FUNCTION sumar(p_a INT, p_b INT)
RETURNS INT
DETERMINISTIC
BEGIN
    RETURN p_a + p_b;
END //
DELIMITER ;

Parámetros de una función

Los parámetros aparecen entre paréntesis después del nombre. Cada uno necesita un nombre y un tipo.

En sumar(p_a INT, p_b INT), la función recibe dos enteros.

RETURNS

Declara el tipo de dato que devuelve la función.

Ese tipo debe ser compatible con el valor que finalmente entregue RETURN.

RETURNS DECIMAL(10,2)

RETURN

Entrega el valor final.

RETURN p_a + p_b;

RETURN termina la función

Cuando MySQL alcanza RETURN, obtiene el valor de salida de esa ejecución.

DETERMINISTIC

Indica que, con los mismos valores de entrada, la función devuelve siempre el mismo resultado.

Por ejemplo, una función que suma 10 + 5 siempre devuelve 15. Una función basada en la fecha actual no tendría esa característica.

Usar una función en SELECT

SELECT sumar(10, 5);

Función para calcular IVA

CREATE FUNCTION calcular_iva(
    p_importe DECIMAL(10,2),
    p_porcentaje DECIMAL(5,2)
)
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
    RETURN p_importe * p_porcentaje / 100;
END

Aplicar una función a cada fila

SELECT nombre,
       precio,
       calcular_iva(precio, 21) AS iva
FROM productos;

La función se ejecuta para cada fila procesada.

Si la consulta devuelve 100.000 productos, la función podría evaluarse 100.000 veces. Por eso una función cómoda desde el punto de vista del código puede tener un costo importante.

Función con IF

IF p_edad >= 18 THEN
    RETURN 'Mayor de edad';
ELSE
    RETURN 'Menor de edad';
END IF;

Variables locales

DECLARE v_descuento DECIMAL(10,2);

Función que consulta una tabla

CREATE FUNCTION cantidad_alumnos_curso(p_curso_id INT)
RETURNS INT
READS SQL DATA
BEGIN
    DECLARE v_total INT;
    SELECT COUNT(*)
    INTO v_total
    FROM alumnos
    WHERE curso_id = p_curso_id;
    RETURN v_total;
END

Usar funciones en WHERE

Es posible, pero puede afectar el rendimiento o impedir aprovechar índices.

Funciones y efectos secundarios

Conceptualmente una función es más fácil de mantener cuando se comporta como un cálculo: recibe datos y devuelve un valor. Para procesos que modifican muchas tablas o ejecutan flujos complejos suele ser más claro usar procedimientos o la lógica de aplicación.

Función vs procedimiento

  • Función: devuelve un valor y puede participar en SELECT.
  • Procedimiento: se ejecuta con CALL y puede representar procesos con varias operaciones.

Eliminar una función

DROP FUNCTION IF EXISTS sumar;

Rendimiento

Una función aplicada a millones de filas puede ejecutarse millones de veces. Conviene medir su costo.

Errores comunes

  • olvidar RETURNS;
  • devolver un tipo incompatible;
  • usar una función para un proceso complejo;
  • aplicar funciones sobre columnas indexadas sin analizar impacto.

Ejercicios

  1. función para duplicar un número;
  2. calcular IVA;
  3. aplicar descuento;
  4. clasificar nota;
  5. contar alumnos de un curso.

¿Qué sigue?

Triggers en MySQL.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Procedimientos almacenados

Triggers en MySQL →

Procedimientos almacenados en MySQL: CREATE PROCEDURE, parámetros y ejemplos prácticos

Un procedimiento almacenado es un bloque de instrucciones SQL guardado dentro de MySQL para poder ejecutarlo cuando sea necesario.

¿Dónde vive un procedimiento almacenado?

El procedimiento se guarda dentro del servidor MySQL, no dentro del código PHP, JavaScript o Python de la aplicación.

Esto permite que distintas aplicaciones puedan invocar la misma operación, aunque también significa que parte de la lógica queda fuera del repositorio de código si no se versiona adecuadamente.

¿Qué problema resuelve?

Si una misma operación SQL compleja se repite en distintos lugares, un procedimiento permite centralizarla y reutilizarla.

También puede agrupar varias sentencias, recibir parámetros, usar variables, tomar decisiones y devolver resultados.

Primer procedimiento

DELIMITER //

CREATE PROCEDURE listar_alumnos()
BEGIN
    SELECT *
    FROM alumnos;
END //

DELIMITER ;

¿Qué significa DELIMITER?

MySQL usa normalmente el punto y coma para terminar una sentencia. Como dentro de un procedimiento hay varios puntos y coma, cambiamos temporalmente el delimitador.

BEGIN y END

Marcan el inicio y el final del bloque de instrucciones.

Crear no significa ejecutar

CREATE PROCEDURE registra el procedimiento en MySQL. Su contenido no se ejecuta en ese momento.

La ejecución ocurre cuando lo invocamos con CALL.

CALL

Un procedimiento se ejecuta con CALL.

CALL listar_alumnos();

Parámetros: entrada y salida

Los parámetros permiten que un mismo procedimiento trabaje con distintos valores sin tener que reescribirlo.

  • IN: entra un valor;
  • OUT: sale un valor;
  • INOUT: entra y puede salir modificado.

Parámetros IN

Los parámetros IN reciben valores desde afuera.

DELIMITER //
CREATE PROCEDURE alumnos_por_edad(
    IN p_edad INT
)
BEGIN
    SELECT nombre, apellido, edad
    FROM alumnos
    WHERE edad = p_edad;
END //
DELIMITER ;

Lo ejecutamos así:

CALL alumnos_por_edad(25);

Parámetros OUT

Un parámetro OUT permite devolver un valor.

DELIMITER //
CREATE PROCEDURE contar_alumnos(
    OUT p_total INT
)
BEGIN
    SELECT COUNT(*)
    INTO p_total
    FROM alumnos;
END //
DELIMITER ;
CALL contar_alumnos(@total);
SELECT @total;

INOUT

INOUT recibe un valor y también puede devolverlo modificado.

CREATE PROCEDURE incrementar(INOUT p_numero INT)
BEGIN
    SET p_numero = p_numero + 1;
END

Parámetros y variables no son lo mismo

Un parámetro conecta el procedimiento con quien lo llama. Una variable local existe solo mientras el procedimiento se está ejecutando.

Variables locales

Se crean con DECLARE.

DECLARE v_promedio DECIMAL(10,2);

SELECT … INTO

Permite guardar el resultado de una consulta en una variable.

SELECT AVG(edad)
INTO v_promedio
FROM alumnos;

IF, ELSEIF y ELSE

Los procedimientos pueden tomar decisiones.

IF p_nota >= 8 THEN
    SET v_estado = 'Promocionado';
ELSEIF p_nota >= 6 THEN
    SET v_estado = 'Aprobado';
ELSE
    SET v_estado = 'Desaprobado';
END IF;

Procedimiento con INSERT

CREATE PROCEDURE crear_alumno(
    IN p_nombre VARCHAR(100),
    IN p_apellido VARCHAR(100),
    IN p_edad INT
)
BEGIN
    INSERT INTO alumnos(nombre, apellido, edad)
    VALUES(p_nombre, p_apellido, p_edad);
END

Procedimientos con varias operaciones

Un procedimiento puede insertar una cabecera, crear detalles, actualizar stock y validar condiciones. Cuando esas acciones forman una sola operación de negocio, normalmente conviene combinarlas con una transacción.

Procedimientos y transacciones

Un procedimiento puede formar parte de una transacción cuando varias operaciones deben ejecutarse juntas.

Eliminar procedimiento

DROP PROCEDURE IF EXISTS listar_alumnos;

Ver procedimientos existentes

SHOW PROCEDURE STATUS;

Procedimientos vs lógica de aplicación

No toda lógica debe ir a la base. Los procedimientos son útiles para operaciones fuertemente relacionadas con datos o reutilizadas por distintos sistemas.

Una desventaja es que la lógica queda más repartida: parte en la aplicación y parte en MySQL. En proyectos grandes eso puede dificultar pruebas, despliegues y mantenimiento si no existe una estrategia clara.

Errores comunes

  • olvidar DELIMITER;
  • confundir IN, OUT e INOUT;
  • crear procedimientos demasiado grandes;
  • duplicar lógica que ya existe en la aplicación;
  • no validar parámetros.

Ejercicios

  1. listar productos;
  2. buscar por categoría;
  3. crear producto;
  4. actualizar precio;
  5. devolver cantidad mediante OUT.

¿Qué sigue?

Funciones almacenadas.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Vistas en SQL

Funciones almacenadas →

Vistas en SQL: CREATE VIEW, cómo usarlas y cuándo convienen

Una vista permite guardar una consulta con un nombre y consultarla después como si fuera una tabla virtual.

¿Qué problema resuelve una vista?

Si una consulta compleja se repite en distintos reportes, podemos centralizarla y reutilizarla.

Una vista no es una copia de la tabla

Una vista guarda la definición de una consulta. Cuando la consultamos, MySQL vuelve a ejecutar esa lógica sobre las tablas originales.

Eso significa que, si cambian los datos de las tablas base, el resultado de la vista también cambia.

CREATE VIEW

CREATE VIEW vista_alumnos_cursos AS
SELECT a.id, a.nombre, a.apellido,
       c.nombre AS curso
FROM alumnos AS a
LEFT JOIN cursos AS c
    ON a.curso_id = c.id;

La vista guarda la definición de la consulta.

Desde ese momento podemos usar vista_alumnos_cursos en SELECT como si fuera una tabla, aunque realmente dependa de alumnos y cursos.

Consultar una vista

SELECT *
FROM vista_alumnos_cursos;

¿Por qué simplifica el trabajo?

Sin la vista, cada reporte debería repetir el JOIN completo. Con la vista, el consumidor solo necesita consultar una estructura ya preparada.

Esto ayuda a evitar duplicación de SQL y reduce el riesgo de que distintos reportes implementen la misma lógica de manera diferente.

Filtrar una vista

SELECT *
FROM vista_alumnos_cursos
WHERE curso = 'Programación';

Filtrar al consultar vs filtrar dentro de la vista

Podemos crear una vista general y aplicar WHERE cada vez, o crear una vista que ya represente un subconjunto concreto.

La segunda opción puede ser útil cuando esa regla se reutiliza constantemente.

Vista con filtros incorporados

CREATE VIEW alumnos_activos AS
SELECT id, nombre, apellido, email
FROM alumnos
WHERE activo = TRUE;

Vista con agregaciones

CREATE VIEW resumen_cursos AS
SELECT c.id, c.nombre, COUNT(a.id) AS cantidad_alumnos
FROM cursos AS c
LEFT JOIN alumnos AS a
    ON c.id = a.curso_id
GROUP BY c.id, c.nombre;

CREATE OR REPLACE VIEW

Reemplaza la definición de una vista existente.

CREATE OR REPLACE VIEW alumnos_activos AS
SELECT id, nombre, apellido, email, fecha_ingreso
FROM alumnos
WHERE activo = TRUE;

ALTER VIEW

Otra forma de modificar la definición.

DROP VIEW

DROP VIEW IF EXISTS alumnos_activos;

Dependencias de una vista

Como la vista depende de tablas y columnas concretas, ciertos cambios estructurales en esas tablas pueden romperla. Por ejemplo, eliminar una columna utilizada por la vista obliga a revisar su definición.

¿Una vista guarda datos?

Una vista tradicional normalmente no almacena una copia independiente de las filas: consulta las tablas originales.

Vistas y seguridad

Podemos exponer solo ciertas columnas y ocultar información sensible.

Por ejemplo, una vista pública de clientes podría mostrar nombre y ciudad, pero excluir documento, teléfono o email.

Esto no reemplaza una política de permisos correcta, pero puede formar parte de ella.

¿Se puede hacer INSERT o UPDATE sobre una vista?

Algunas vistas simples son actualizables. Las que incluyen agregaciones, GROUP BY, DISTINCT o lógica compleja pueden no serlo.

Antes de tratar una vista como si fuera una tabla editable conviene verificar sus características. Una vista de reporte suele pensarse principalmente para lectura.

WITH CHECK OPTION

Evita que una modificación realizada a través de la vista haga que el registro deje de cumplir la condición de esa vista.

CREATE VIEW alumnos_activos AS
SELECT *
FROM alumnos
WHERE activo = TRUE
WITH CHECK OPTION;

Vistas y rendimiento

Una vista no acelera automáticamente una consulta. El rendimiento sigue dependiendo de índices, joins, filtros y plan de ejecución.

Errores comunes

  • crear vistas para consultas que no se reutilizan;
  • suponer que almacenan una copia independiente;
  • actualizar vistas complejas sin verificar si son actualizables;
  • crear demasiadas capas de vistas.

Ejercicios

  1. crear vista de alumnos activos;
  2. crear vista alumnos+cursos;
  3. crear resumen por curso;
  4. modificar una vista;
  5. crear una vista que oculte columnas sensibles.

¿Qué sigue?

Procedimientos almacenados.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Índices en MySQL

Procedimientos almacenados →

Índices en MySQL: qué son, cómo funcionan y cuándo usarlos

Una consulta puede funcionar perfecto con 100 filas y volverse lenta con 10 millones. Los índices ayudan a encontrar registros más eficientemente.

¿Qué es un índice?

Es una estructura auxiliar que organiza ciertos valores para que el motor pueda localizar filas sin recorrer necesariamente toda la tabla.

Es parecido al índice de un libro: no reemplaza el contenido, pero ayuda a encontrarlo.

¿Cómo busca MySQL sin un índice útil?

Una posibilidad es recorrer fila por fila y comprobar la condición. Esto se conoce como table scan o recorrido completo.

Con 50 filas puede ser irrelevante. Con millones de filas, puede ser costoso.

Consulta sin índice

SELECT *
FROM clientes
WHERE email = 'ana@email.com';

Si no existe un índice útil, MySQL puede tener que revisar muchas filas.

Qué cambia al crear un índice

El índice agrega una estructura ordenada que permite al motor reducir drásticamente la cantidad de filas candidatas en ciertas búsquedas.

No modifica los datos de la tabla ni cambia el resultado de la consulta: cambia la forma en que MySQL puede llegar a ellos.

CREATE INDEX

CREATE INDEX idx_clientes_email
ON clientes(email);

PRIMARY KEY también es un índice

Las claves primarias tienen un índice asociado.

Por eso una búsqueda por id suele ser eficiente incluso si nunca creamos manualmente un índice llamado idx_id.

UNIQUE INDEX

Además de acelerar búsquedas, obliga a que los valores sean únicos.

CREATE UNIQUE INDEX uq_clientes_email
ON clientes(email);

Índices compuestos

Pueden incluir varias columnas.

CREATE INDEX idx_clientes_ciudad_apellido
ON clientes(ciudad, apellido);

Índice simple vs índice compuesto

Un índice simple tiene una sola columna. Un índice compuesto combina varias.

La combinación debería reflejar consultas reales. No conviene crear índices compuestos al azar.

El orden importa

En (ciudad, apellido), MySQL puede aprovechar el índice para ciudad o ciudad+apellido, pero no necesariamente igual para apellido solamente.

Esto se conoce como regla del prefijo izquierdo.

Por ejemplo, (cliente_id, fecha) puede ser excelente para “ventas de un cliente entre fechas”, pero no necesariamente para consultas que solo filtran por fecha.

Índices y ORDER BY

SELECT *
FROM clientes
WHERE ciudad = 'Santa Rosa'
ORDER BY apellido;

Índices y JOIN

Las columnas utilizadas para relacionar tablas suelen ser candidatas importantes para indexación.

No asumir: medir

La presencia de un índice no garantiza que MySQL vaya a usarlo. El optimizador compara alternativas y elige el plan que estima más conveniente.

Por eso debemos mirar el plan de ejecución.

EXPLAIN

EXPLAIN muestra cómo MySQL planea ejecutar una consulta.

EXPLAIN SELECT *
FROM clientes
WHERE email = 'ana@email.com';

Qué mirar en EXPLAIN

Algunos campos útiles son:

  • type: tipo de acceso;
  • possible_keys: índices candidatos;
  • key: índice elegido;
  • rows: cantidad estimada de filas a examinar.

possible_keys y key

  • possible_keys: índices que podrían utilizarse;
  • key: índice elegido.

Table scan

Un acceso tipo ALL puede indicar un recorrido completo. No siempre es malo: en tablas pequeñas puede ser razonable.

Selectividad

Un índice suele ser más útil cuando reduce mucho la cantidad de filas candidatas.

Emails, documentos o códigos únicos suelen tener alta selectividad.

Índices en columnas con pocos valores

Un índice sobre una columna booleana puede aportar poco si casi todos los registros tienen el mismo valor.

¿Por qué no indexar todo?

Cada índice debe mantenerse cuando insertamos, modificamos o eliminamos datos. Además ocupa espacio.

En una tabla con muchísimas escrituras, un exceso de índices puede empeorar el rendimiento.

Costo de los índices

Los índices aceleran lecturas, pero agregan trabajo en:

  • INSERT;
  • UPDATE;
  • DELETE;
  • almacenamiento.

SHOW INDEX

SHOW INDEX FROM clientes;

DROP INDEX

DROP INDEX idx_clientes_email ON clientes;

Índices duplicados o redundantes

Antes de crear un nuevo índice conviene revisar los existentes. Dos índices muy similares pueden aportar poco y aumentar el costo de mantenimiento.

Ejemplo práctico

CREATE INDEX idx_ventas_cliente_fecha
ON ventas(cliente_id, fecha);

Este índice puede ayudar a consultar ventas de un cliente ordenadas por fecha.

Errores comunes

  • crear índices para todas las columnas;
  • ignorar el orden en índices compuestos;
  • no usar EXPLAIN;
  • crear índices redundantes;
  • optimizar sin datos representativos.

Ejercicios

  1. crear índice sobre email;
  2. crear índice único sobre código;
  3. crear índice compuesto ciudad+apellido;
  4. comparar EXPLAIN antes y después.

¿Qué sigue?

Vistas en SQL.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← Transacciones en SQL

Vistas en SQL →

Transacciones en SQL: START TRANSACTION, COMMIT y ROLLBACK con ejemplos

Una transacción permite agrupar varias operaciones para que se comporten como una sola unidad. Es fundamental cuando un proceso no puede quedar “a mitad de camino”.

El problema de las operaciones parciales

En una transferencia bancaria hay dos pasos:

  1. restar dinero de una cuenta;
  2. sumarlo en otra.

Si el primer paso ocurre y el segundo falla, los datos quedan inconsistentes.

La idea de “todo o nada”

Una transacción agrupa instrucciones que pertenecen al mismo proceso de negocio. Si una falla, podemos evitar que las demás queden confirmadas.

El objetivo no es solo “poder deshacer”, sino mantener la base en un estado coherente.

START TRANSACTION

Marca el inicio de una transacción.

START TRANSACTION;

COMMIT

Confirma los cambios de forma definitiva.

Después de COMMIT, un ROLLBACK de esa misma transacción ya no puede revertirlos.

COMMIT;

ROLLBACK

Revierte los cambios realizados desde el inicio de la transacción.

ROLLBACK es útil cuando detectamos un error de validación, falta de stock, saldo insuficiente o cualquier condición que impida completar el proceso.

ROLLBACK;

Ejemplo completo

START TRANSACTION;

UPDATE cuentas
SET saldo = saldo - 1000
WHERE id = 1;

UPDATE cuentas
SET saldo = saldo + 1000
WHERE id = 2;

COMMIT;

¿Qué pasaría si la segunda operación falla?

En el ejemplo de transferencia, si restamos 1000 de la cuenta 1 y luego falla la suma en la cuenta 2, debemos ejecutar ROLLBACK. Así el descuento inicial también se deshace.

Ese comportamiento es la base de la atomicidad.

Propiedades ACID

Atomicidad

Todo se ejecuta o nada se ejecuta.

Consistencia

La base debe pasar de un estado válido a otro estado válido.

Aislamiento

Transacciones concurrentes no deberían interferir incorrectamente.

Por ejemplo, dos ventas simultáneas no deberían descontar el mismo último producto sin coordinación.

Durabilidad

Una vez hecho COMMIT, los cambios deben persistir.

SAVEPOINT

Permite crear puntos intermedios dentro de una transacción.

START TRANSACTION;
UPDATE productos SET stock = stock - 1 WHERE id = 10;
SAVEPOINT despues_stock;
UPDATE ventas SET estado = 'procesada' WHERE id = 50;

Podemos volver solo hasta ese punto:

ROLLBACK TO despues_stock;

RELEASE SAVEPOINT

Cuando ya no necesitamos un punto intermedio podemos liberarlo.

RELEASE SAVEPOINT despues_stock;

Autocommit

MySQL suele confirmar automáticamente cada sentencia individual cuando no hay una transacción explícita.

Eso significa que un UPDATE ejecutado fuera de una transacción puede quedar confirmado inmediatamente.

SELECT @@autocommit;

Ejemplo: venta y stock

START TRANSACTION;
INSERT INTO ventas (cliente_id, total)
VALUES (15, 25000);
UPDATE productos
SET stock = stock - 1
WHERE id = 3;
COMMIT;

Transacciones y errores de aplicación

En una aplicación real, el código normalmente hace algo así:

  1. inicia la transacción;
  2. ejecuta las operaciones;
  3. si todo sale bien, hace COMMIT;
  4. si ocurre una excepción o validación fallida, hace ROLLBACK.

Cuándo usar transacciones

  • pagos;
  • transferencias;
  • stock;
  • facturas con detalles;
  • procesos que modifican varias tablas.

Errores comunes

  • olvidar COMMIT;
  • no ejecutar ROLLBACK ante error;
  • mantener transacciones abiertas demasiado tiempo;
  • incluir tareas innecesarias dentro de la transacción.

Ejercicios

  1. crear una transferencia;
  2. simular un error y hacer ROLLBACK;
  3. registrar venta y descontar stock;
  4. crear y usar un SAVEPOINT.

¿Qué sigue?

Índices y optimización.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Ruta SQL / MySQL

Ver todos los contenidos de SQL / MySQL

← INSERT, UPDATE y DELETE

Índices en MySQL →