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:
- obtener primero las filas base;
- agregar los JOIN necesarios;
- verificar que no se multipliquen filas inesperadamente;
- agrupar;
- agregar funciones como SUM o COUNT;
- filtrar grupos con HAVING;
- 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
- escribí primero la consulta más simple posible;
- probá cada JOIN por separado;
- agregá agrupaciones después;
- verificá la subconsulta de forma independiente;
- 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: