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


Descubre más desde Club Programador

Suscríbete y recibe las últimas entradas en tu correo electrónico.

Un comentario en “20 ejercicios intermedios y avanzados de SQL resueltos”

Deja un comentario

Descubre más desde Club Programador

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo