Proyecto SQL: análisis y auditoría de ventas
Responde preguntas de negocio sobre una tienda, comprueba qué representa cada fila y detecta cuándo un JOIN puede inflar una métrica.
Al terminar, podrás
- Traducir una pregunta de ventas a una unidad de análisis concreta antes de escribir SQL.
- Combinar
clientes,pedidos,pagos,detalle_pedidoyproductossin mezclar granularidades ni duplicar importes por accidente. - Calcular métricas con
JOIN,GROUP BY,HAVINGy agregaciones. - Auditar un total de cabecera comparándolo con la suma de sus líneas.
- Usar
NOT EXISTSpara encontrar clientes sin ventas válidas según la regla del proyecto.
Antes de empezar: conviene dominar JOIN, GROUP BY, HAVING y EXISTS. Este proyecto no forma parte de las 45 lecciones principales: úsalo como práctica de transferencia.
Escenario y regla de negocio
Trabajas con la base educativa de la tienda. La dirección quiere un informe sencillo para revisar ventas y detectar incoherencias antes de compartir cifras. Para este proyecto, un pedido con estado 'cancelado' no cuenta como venta válida; esa es una regla del ejercicio, no una regla universal de SQL.
| Tabla | Qué representa una fila | Relación clave |
|---|---|---|
clientes | Un cliente | clientes.id → pedidos.cliente_id |
pedidos | Un pedido y su total de cabecera | pedidos.id → detalle_pedido.pedido_id |
pagos | Un pago registrado | pagos.pedido_id → pedidos.id |
detalle_pedido | Una línea de un pedido | producto_id → productos.id |
productos | Un producto | Se une desde cada línea |
Regla de control: antes de agregar, escribe en una frase qué representa una fila de tu conjunto intermedio. Si pasas de una fila por pedido a una fila por línea, un importe de cabecera puede repetirse varias veces.
Plan del proyecto
- Resume el importe registrado por método de pago y valida el total.
- Encuentra clientes con al menos 200 € de facturación válida.
- Calcula qué productos aportan más ingreso a partir de las líneas.
- Compara el total del pedido con la suma de sus líneas para localizar discrepancias.
- Detecta clientes sin ninguna venta válida y explica por qué
NOT EXISTSencaja con la pregunta.
Las 15 filas de pagos se resumen en tres métodos y suman 2.659,00 €. Ese total coincide con la suma de los pedidos no cancelados de este dataset, pero no conviertas esa coincidencia en una regla universal: otro sistema podría admitir pagos parciales, reembolsos o varios pagos por pedido.
| metodo | pagos | importe |
|---|---|---|
| transferencia | 4 | 1590,40 |
| tarjeta | 7 | 632,70 |
| paypal | 4 | 435,90 |
SELECT pg.metodo,
COUNT(*) AS pagos,
ROUND(SUM(pg.importe), 2) AS importe
FROM pagos AS pg
JOIN pedidos AS p ON p.id = pg.pedido_id
WHERE p.estado <> 'cancelado'
GROUP BY pg.metodo
ORDER BY importe DESC, pg.metodo;Empieza con una métrica de la tabla pagos. Una fila intermedia representa un pago registrado; une pedidos solo para aplicar la regla del proyecto y excluir cancelados antes de agrupar por método.
Etapa 1 · Importe registrado por método de pago
Un error que puede inflar la facturación
Sumar el total de cabecera después de multiplicar el pedido por sus líneas
SELECT SUM(p.total)
FROM pedidos AS p
JOIN detalle_pedido AS d ON d.pedido_id = p.id
WHERE p.estado <> 'cancelado';Si un pedido tiene tres líneas, p.total aparece tres veces en el conjunto unido. El JOIN no está “mal”: cambió la granularidad a una fila por línea. El error es seguir tratando p.total como si cada pedido apareciera una sola vez.
Etapa 2 · Clientes con al menos 200 €
Filtra después de resumir
Devuelve los clientes cuya facturación no cancelada sea de al menos 200 €. Incluye nombre, número de pedidos y facturación, y ordena de mayor a menor facturación.
Criterio de éxito: deben aparecer cinco clientes; Sara Gómez queda primera con 1.112,00 € y Marta Ruiz cierra el resultado con 215,90 €.
WHERE antes de agrupar.SUM(p.total) pertenece a HAVING.Solución razonada
SELECT c.nombre,
COUNT(p.id) AS pedidos,
ROUND(SUM(p.total), 2) AS facturacion
FROM clientes AS c
JOIN pedidos AS p ON p.cliente_id = c.id
WHERE p.estado <> 'cancelado'
GROUP BY c.id, c.nombre
HAVING SUM(p.total) >= 200
ORDER BY facturacion DESC, c.nombre;| nombre | pedidos | facturacion |
|---|---|---|
| Sara Gómez | 2 | 1112,00 |
| Ana Torres | 2 | 377,40 |
| Hugo Castro | 2 | 268,00 |
| Luis Martín | 2 | 253,40 |
| Marta Ruiz | 2 | 215,90 |
WHERE decide qué pedidos entran en el cálculo; HAVING decide qué grupos sobreviven después de sumar.
Etapa 3 · Productos que más ingreso aportan
Cambia la unidad a línea de pedido
Calcula las unidades vendidas y el ingreso de cada producto usando cantidad * precio_unitario. Excluye pedidos cancelados y muestra los cinco productos con mayor ingreso.
Criterio de éxito: Portátil Pro queda primero con 999,00 €; Monitor 24 aparece segundo con 3 unidades y 537,00 €.
detalle_pedido: una fila representa una línea.pedidos para conocer el estado y productos para obtener el nombre.SUM(d.cantidad) y SUM(d.cantidad * d.precio_unitario).Solución razonada
SELECT pr.nombre,
SUM(d.cantidad) AS unidades,
ROUND(SUM(d.cantidad * d.precio_unitario), 2) AS ingreso
FROM detalle_pedido AS d
JOIN pedidos AS p ON p.id = d.pedido_id
JOIN productos AS pr ON pr.id = d.producto_id
WHERE p.estado <> 'cancelado'
GROUP BY pr.id, pr.nombre
ORDER BY ingreso DESC, pr.nombre
LIMIT 5;| nombre | unidades | ingreso |
|---|---|---|
| Portátil Pro | 1 | 999,00 |
| Monitor 24 | 3 | 537,00 |
| Disco SSD | 3 | 267,00 |
| Auriculares | 4 | 253,70 |
| Monitor 27 | 1 | 249,00 |
Ahora sí tiene sentido agregar importes de línea, porque cada fila intermedia representa una línea y el cálculo usa columnas de esa misma granularidad.
Límite de cinco filas por motor: la consulta del proyecto usa LIMIT 5 porque la base local se ejecuta en SQLite. PostgreSQL y MySQL también aceptan LIMIT; en SQL Server usa SELECT TOP (5) junto con el mismo ORDER BY.
Etapa 4 · Audita cabecera contra líneas
El proyecto contiene deliberadamente una discrepancia para que practiques una comprobación de integridad. Agrupa las líneas por pedido y compara su suma con pedidos.total.
SELECT p.id,
p.total AS total_cabecera,
ROUND(SUM(d.cantidad * d.precio_unitario), 2) AS total_lineas
FROM pedidos AS p
JOIN detalle_pedido AS d ON d.pedido_id = p.id
GROUP BY p.id, p.total
HAVING ABS(p.total - SUM(d.cantidad * d.precio_unitario)) > 0.001
ORDER BY p.id;| id | total_cabecera | total_lineas |
|---|---|---|
| 113 | 98,00 | 93,90 |
La consulta detecta el pedido 113, cuya cabecera dice 98,00 € y cuyas líneas suman 93,90 €. SQL te permite localizar la diferencia; no te dice qué valor es el correcto. Corregirlo requeriría conocer la regla del sistema origen, impuestos, descuentos u otros ajustes que este dataset no modela.
Etapa 5 · Clientes sin ventas válidas
Formula la ausencia con NOT EXISTS
Encuentra clientes para los que no exista ningún pedido no cancelado. Ordena los nombres alfabéticamente.
Criterio de éxito: el resultado debe contener exactamente a Mario Gil y Pablo Sanz.
clientes.p.cliente_id = c.id.'cancelado'.Solución razonada
SELECT c.nombre
FROM clientes AS c
WHERE NOT EXISTS (
SELECT 1
FROM pedidos AS p
WHERE p.cliente_id = c.id
AND p.estado <> 'cancelado'
)
ORDER BY c.nombre;| nombre |
|---|
| Mario Gil |
| Pablo Sanz |
NOT EXISTS expresa directamente la pregunta: conserva al cliente cuando la subconsulta no encuentra ni una fila que cumpla la condición.
Criterios para considerar terminado el proyecto
| Comprobación | Debes poder explicar |
|---|---|
| Granularidad | Por qué la etapa 1 usa una fila por pedido y la etapa 3 una fila por línea. |
| Filtros | Por qué el estado se filtra antes de agregar. |
| Agregación | Por qué la condición de 200 € usa HAVING. |
| Auditoría | Por qué detectar una diferencia no autoriza a elegir automáticamente cuál total corregir. |
| Ausencia | Qué fila tendría que aparecer en la subconsulta para que un cliente quede fuera de NOT EXISTS. |
Qué has demostrado
- Puedes definir la unidad de análisis antes de escribir la agregación.
- Puedes separar métricas de cabecera y métricas de detalle.
- Puedes validar un informe con recuentos y resultados esperados, no solo con sintaxis válida.
- Puedes detectar una incoherencia sin inventar una explicación que los datos no contienen.
- Puedes resolver una pregunta de ausencia con una subconsulta correlacionada.
Siguiente transferencia: cambia una regla del proyecto —por ejemplo, incluir cancelados o medir unidades en lugar de ingreso— y predice qué etapas deben cambiar antes de editar el SQL.