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 →


Descubre más desde Club Programador

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

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

Deja un comentario

Descubre más desde Club Programador

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

Seguir leyendo