En esta clase vamos a repasar cómo se diseña una base de datos correctamente y vamos a entrar a consultas un poco más avanzadas: relaciones, JOINs, agrupaciones, funciones de resumen, filtros con HAVING y consultas anidadas.
El objetivo no es memorizar comandos. El objetivo es entender cómo se piensa una base de datos y cómo SQL nos ayuda a responder preguntas reales.
Aunque hoy un alumno pueda pedirle a una IA que le genere tablas o consultas, necesita saber revisar si eso tiene sentido. La IA puede ayudar a escribir código, pero el criterio de diseño sigue siendo responsabilidad del desarrollador.
Vamos a imaginar una empresa que vende productos. Esa empresa tiene empleados, vendedores, supervisores, clientes de distintos tipos, productos de distintas categorías y ventas.
El sistema debe poder responder preguntas como:
Una tabla representa un conjunto de datos de una misma cosa. Por ejemplo: empleados, clientes, productos o ventas.
Una columna representa un dato específico dentro de la tabla. Por ejemplo, en clientes podríamos tener nombre, correo, teléfono y tipo_cliente_id.
Una fila representa un elemento real guardado en la tabla. Por ejemplo, un cliente específico llamado Ana López.
La llave primaria identifica de forma única cada registro. Normalmente usamos un campo llamado id.
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY
Una llave foránea es un campo que conecta una tabla con otra. Por ejemplo, un cliente tiene un tipo de cliente.
tipo_cliente_id INT UNSIGNED NOT NULL
FOREIGN KEY (tipo_cliente_id) REFERENCES tipos_cliente(id)
Elegir bien el tipo de dato importa. No todo debe ser VARCHAR.
| Tipo | Uso común | Ejemplo |
|---|---|---|
INT |
Números enteros | id, cantidad, edad |
VARCHAR |
Texto corto | nombre, correo, teléfono |
TEXT |
Texto largo | descripción, observaciones |
DECIMAL |
Dinero o cantidades exactas | precio, total, comisión |
DATE |
Fechas sin hora | fecha_nacimiento |
DATETIME |
Fecha y hora | fecha_venta |
TINYINT(1) |
Activo/inactivo, verdadero/falso | activo |
DECIMAL(10,2),
porque después vamos a sumar, promediar, ordenar y calcular reportes.
En una base de datos relacional, las tablas no viven aisladas. Se conectan entre ellas usando llaves primarias y llaves foráneas.
Una categoría puede tener muchos productos. Pero un producto pertenece a una sola categoría.
Una venta puede tener muchos productos, y un producto puede aparecer en muchas ventas.
Para resolver eso usamos una tabla intermedia: venta_detalles.
Este script crea una base de práctica para la clase. Incluye empleados, supervisores, vendedores, clientes, productos, ventas y detalle de ventas.
DROP DATABASE porque es una base de práctica para clase.
No se debe ejecutar sobre una base real con información importante.
-- =========================================================
-- BASE DE DATOS DE PRÁCTICA
-- Tema: SQL Avanzado I
-- Caso: Empresa con empleados, clientes, productos y ventas
-- =========================================================
DROP DATABASE IF EXISTS empresa_sql_avanzado_1;
CREATE DATABASE empresa_sql_avanzado_1
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE empresa_sql_avanzado_1;
-- =========================================================
-- 1. TABLA: departamentos
-- =========================================================
-- Un departamento agrupa empleados.
-- Ejemplo: Ventas, Administración, Sistemas.
CREATE TABLE departamentos (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
activo TINYINT(1) NOT NULL DEFAULT 1
) ENGINE=InnoDB;
-- =========================================================
-- 2. TABLA: empleados
-- =========================================================
-- Guarda empleados de la empresa.
-- Algunos empleados son vendedores.
-- Algunos empleados son supervisores.
--
-- supervisor_id se relaciona con empleados.id.
-- Eso significa que un empleado puede tener como supervisor
-- a otro empleado de la misma tabla.
CREATE TABLE empleados (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
departamento_id INT UNSIGNED NOT NULL,
supervisor_id INT UNSIGNED NULL,
nombre VARCHAR(120) NOT NULL,
puesto VARCHAR(80) NOT NULL,
salario DECIMAL(10,2) NOT NULL DEFAULT 0.00,
fecha_ingreso DATE NOT NULL,
activo TINYINT(1) NOT NULL DEFAULT 1,
CONSTRAINT fk_empleados_departamento
FOREIGN KEY (departamento_id) REFERENCES departamentos(id),
CONSTRAINT fk_empleados_supervisor
FOREIGN KEY (supervisor_id) REFERENCES empleados(id),
INDEX idx_empleados_departamento (departamento_id),
INDEX idx_empleados_supervisor (supervisor_id)
) ENGINE=InnoDB;
-- =========================================================
-- 3. TABLA: tipos_cliente
-- =========================================================
-- Clasifica clientes.
-- Ejemplo: Minorista, Mayorista, Gobierno, Corporativo.
CREATE TABLE tipos_cliente (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
descuento_base DECIMAL(5,2) NOT NULL DEFAULT 0.00,
activo TINYINT(1) NOT NULL DEFAULT 1
) ENGINE=InnoDB;
-- =========================================================
-- 4. TABLA: clientes
-- =========================================================
-- Cada cliente pertenece a un tipo de cliente.
--
-- tipo_cliente_id conecta clientes con tipos_cliente.
CREATE TABLE clientes (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tipo_cliente_id INT UNSIGNED NOT NULL,
nombre VARCHAR(120) NOT NULL,
correo VARCHAR(150) NULL,
telefono VARCHAR(30) NULL,
ciudad VARCHAR(80) NOT NULL,
activo TINYINT(1) NOT NULL DEFAULT 1,
CONSTRAINT fk_clientes_tipo
FOREIGN KEY (tipo_cliente_id) REFERENCES tipos_cliente(id),
INDEX idx_clientes_tipo (tipo_cliente_id),
INDEX idx_clientes_ciudad (ciudad)
) ENGINE=InnoDB;
-- =========================================================
-- 5. TABLA: categorias_producto
-- =========================================================
-- Clasifica productos.
-- Ejemplo: Computadoras, Accesorios, Software.
CREATE TABLE categorias_producto (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
activo TINYINT(1) NOT NULL DEFAULT 1
) ENGINE=InnoDB;
-- =========================================================
-- 6. TABLA: productos
-- =========================================================
-- Cada producto pertenece a una categoría.
CREATE TABLE productos (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
categoria_id INT UNSIGNED NOT NULL,
codigo VARCHAR(50) NOT NULL,
nombre VARCHAR(120) NOT NULL,
precio DECIMAL(10,2) NOT NULL DEFAULT 0.00,
costo DECIMAL(10,2) NOT NULL DEFAULT 0.00,
existencia INT NOT NULL DEFAULT 0,
activo TINYINT(1) NOT NULL DEFAULT 1,
CONSTRAINT fk_productos_categoria
FOREIGN KEY (categoria_id) REFERENCES categorias_producto(id),
UNIQUE KEY uk_productos_codigo (codigo),
INDEX idx_productos_categoria (categoria_id)
) ENGINE=InnoDB;
-- =========================================================
-- 7. TABLA: ventas
-- =========================================================
-- Representa el encabezado de una venta.
--
-- Una venta tiene:
-- - cliente
-- - vendedor
-- - fecha
-- - subtotal
-- - descuento
-- - total
--
-- Los productos vendidos NO se guardan aquí.
-- Se guardan en venta_detalles.
CREATE TABLE ventas (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
cliente_id INT UNSIGNED NOT NULL,
vendedor_id INT UNSIGNED NOT NULL,
folio VARCHAR(50) NOT NULL,
fecha_venta DATETIME NOT NULL,
subtotal DECIMAL(10,2) NOT NULL DEFAULT 0.00,
descuento DECIMAL(10,2) NOT NULL DEFAULT 0.00,
total DECIMAL(10,2) NOT NULL DEFAULT 0.00,
estado VARCHAR(30) NOT NULL DEFAULT 'cerrada',
CONSTRAINT fk_ventas_cliente
FOREIGN KEY (cliente_id) REFERENCES clientes(id),
CONSTRAINT fk_ventas_vendedor
FOREIGN KEY (vendedor_id) REFERENCES empleados(id),
UNIQUE KEY uk_ventas_folio (folio),
INDEX idx_ventas_cliente (cliente_id),
INDEX idx_ventas_vendedor (vendedor_id),
INDEX idx_ventas_fecha (fecha_venta)
) ENGINE=InnoDB;
-- =========================================================
-- 8. TABLA: venta_detalles
-- =========================================================
-- Representa los productos dentro de una venta.
--
-- Si una venta tiene 3 productos, tendrá 3 registros aquí.
CREATE TABLE venta_detalles (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
venta_id INT UNSIGNED NOT NULL,
producto_id INT UNSIGNED NOT NULL,
cantidad INT NOT NULL DEFAULT 1,
precio_unitario DECIMAL(10,2) NOT NULL DEFAULT 0.00,
descuento_linea DECIMAL(10,2) NOT NULL DEFAULT 0.00,
total_linea DECIMAL(10,2) NOT NULL DEFAULT 0.00,
CONSTRAINT fk_detalles_venta
FOREIGN KEY (venta_id) REFERENCES ventas(id)
ON DELETE CASCADE,
CONSTRAINT fk_detalles_producto
FOREIGN KEY (producto_id) REFERENCES productos(id),
INDEX idx_detalles_venta (venta_id),
INDEX idx_detalles_producto (producto_id)
) ENGINE=InnoDB;
-- =========================================================
-- DATOS DE PRÁCTICA
-- =========================================================
INSERT INTO departamentos (nombre) VALUES
('Ventas'),
('Administración'),
('Sistemas'),
('Dirección');
INSERT INTO empleados (
departamento_id,
supervisor_id,
nombre,
puesto,
salario,
fecha_ingreso
) VALUES
(4, NULL, 'Roberto Salinas', 'Director General', 45000.00, '2020-01-10'),
(1, 1, 'Mónica Ramírez', 'Supervisora de Ventas', 28000.00, '2021-03-15'),
(1, 2, 'Luis Herrera', 'Vendedor', 15000.00, '2022-05-01'),
(1, 2, 'Carla Méndez', 'Vendedora', 16000.00, '2022-08-12'),
(1, 2, 'Jorge Castillo', 'Vendedor', 14500.00, '2023-01-20'),
(2, 1, 'Ana Torres', 'Administradora', 19000.00, '2021-11-03'),
(3, 1, 'Daniel Ortega', 'Soporte Técnico', 18000.00, '2023-04-17');
INSERT INTO tipos_cliente (nombre, descuento_base) VALUES
('Minorista', 0.00),
('Mayorista', 8.00),
('Corporativo', 12.00),
('Gobierno', 15.00);
INSERT INTO clientes (
tipo_cliente_id,
nombre,
correo,
telefono,
ciudad
) VALUES
(1, 'Papelería El Lápiz Feliz', 'contacto@lapizfeliz.com', '4771001001', 'León'),
(2, 'Distribuidora Bajío', 'ventas@bajio.com', '4771001002', 'León'),
(3, 'Grupo Empresarial Nova', 'compras@nova.com', '4771001003', 'Querétaro'),
(4, 'Instituto Municipal Norte', 'sistemas@imn.gob.mx', '4771001004', 'León'),
(1, 'Café Punto Azul', 'hola@puntoazul.com', '4771001005', 'Irapuato'),
(2, 'Comercial del Centro', 'contacto@comercialcentro.com', '4771001006', 'Silao'),
(3, 'Corporativo Andes', 'compras@andes.com', '4771001007', 'Guadalajara'),
(1, 'Tienda La Esquina', 'tienda@esquina.com', '4771001008', 'León');
INSERT INTO categorias_producto (nombre) VALUES
('Computadoras'),
('Accesorios'),
('Software'),
('Redes'),
('Impresión');
INSERT INTO productos (
categoria_id,
codigo,
nombre,
precio,
costo,
existencia
) VALUES
(1, 'LAP-001', 'Laptop Oficina 14 pulgadas', 12500.00, 9800.00, 15),
(1, 'LAP-002', 'Laptop Pro 16 pulgadas', 24500.00, 19900.00, 8),
(2, 'MOU-001', 'Mouse inalámbrico', 350.00, 180.00, 80),
(2, 'TEC-001', 'Teclado mecánico', 1450.00, 900.00, 35),
(3, 'SOF-001', 'Licencia antivirus anual', 650.00, 300.00, 200),
(3, 'SOF-002', 'Licencia punto de venta', 3200.00, 1500.00, 25),
(4, 'ROU-001', 'Router empresarial', 2800.00, 1700.00, 20),
(4, 'SWI-001', 'Switch 24 puertos', 4200.00, 2800.00, 12),
(5, 'IMP-001', 'Impresora multifuncional', 3900.00, 2600.00, 10),
(5, 'TON-001', 'Tóner negro estándar', 950.00, 500.00, 50),
(2, 'CAM-001', 'Cámara web HD', 890.00, 430.00, 40),
(4, 'CAB-001', 'Cable de red 3 metros', 85.00, 25.00, 150);
INSERT INTO ventas (
cliente_id,
vendedor_id,
folio,
fecha_venta,
subtotal,
descuento,
total,
estado
) VALUES
(1, 3, 'V-0001', '2026-09-01 10:15:00', 13200.00, 0.00, 13200.00, 'cerrada'),
(2, 4, 'V-0002', '2026-09-01 12:20:00', 7400.00, 592.00, 6808.00, 'cerrada'),
(3, 3, 'V-0003', '2026-09-02 09:40:00', 27700.00, 3324.00, 24376.00, 'cerrada'),
(4, 5, 'V-0004', '2026-09-02 13:05:00', 14250.00, 2137.50, 12112.50, 'cerrada'),
(5, 4, 'V-0005', '2026-09-03 11:30:00', 4100.00, 0.00, 4100.00, 'cerrada'),
(6, 3, 'V-0006', '2026-09-03 15:50:00', 9300.00, 744.00, 8556.00, 'cerrada'),
(7, 5, 'V-0007', '2026-09-04 10:10:00', 32900.00, 3948.00, 28952.00, 'cerrada'),
(1, 4, 'V-0008', '2026-09-04 17:15:00', 1800.00, 0.00, 1800.00, 'cerrada'),
(2, 3, 'V-0009', '2026-09-05 09:10:00', 6200.00, 496.00, 5704.00, 'cerrada'),
(8, 5, 'V-0010', '2026-09-05 18:30:00', 435.00, 0.00, 435.00, 'cerrada');
INSERT INTO venta_detalles (
venta_id,
producto_id,
cantidad,
precio_unitario,
descuento_linea,
total_linea
) VALUES
(1, 1, 1, 12500.00, 0.00, 12500.00),
(1, 3, 2, 350.00, 0.00, 700.00),
(2, 7, 2, 2800.00, 448.00, 5152.00),
(2, 12, 10, 85.00, 68.00, 782.00),
(2, 5, 2, 650.00, 76.00, 1194.00),
(3, 2, 1, 24500.00, 2940.00, 21560.00),
(3, 6, 1, 3200.00, 384.00, 2816.00),
(4, 9, 2, 3900.00, 1170.00, 6630.00),
(4, 10, 5, 950.00, 712.50, 4037.50),
(4, 11, 3, 890.00, 255.00, 2425.00),
(5, 6, 1, 3200.00, 0.00, 3200.00),
(5, 4, 1, 1450.00, 0.00, 1450.00),
(5, 3, 1, 350.00, 0.00, 350.00),
(6, 8, 1, 4200.00, 336.00, 3864.00),
(6, 7, 1, 2800.00, 224.00, 2576.00),
(6, 12, 20, 85.00, 136.00, 1564.00),
(7, 2, 1, 24500.00, 2940.00, 21560.00),
(7, 8, 2, 4200.00, 1008.00, 7392.00),
(8, 3, 2, 350.00, 0.00, 700.00),
(8, 5, 1, 650.00, 0.00, 650.00),
(8, 12, 5, 85.00, 0.00, 425.00),
(9, 9, 1, 3900.00, 312.00, 3588.00),
(9, 10, 2, 950.00, 152.00, 1748.00),
(9, 5, 1, 650.00, 52.00, 598.00),
(10, 12, 3, 85.00, 0.00, 255.00),
(10, 3, 1, 350.00, 0.00, 350.00);
-- Verificación rápida
SHOW TABLES;
Antes de entrar a consultas más avanzadas, repasamos consultas normales.
SELECT *
FROM clientes;
SELECT * significa “trae todas las columnas”.
Para pruebas está bien, pero en proyectos reales conviene pedir solo las columnas necesarias.
SELECT
id,
nombre,
ciudad
FROM clientes;
SELECT
id,
nombre,
ciudad
FROM clientes
WHERE ciudad = 'León';
SELECT
id,
nombre,
precio
FROM productos
ORDER BY precio DESC;
DESC ordena de mayor a menor.
ASC ordena de menor a mayor.
SELECT
id,
nombre,
precio
FROM productos
ORDER BY precio DESC
LIMIT 5;
Un JOIN sirve para juntar información de varias tablas relacionadas.
En sistemas reales casi nunca toda la información está en una sola tabla.
SELECT
c.id,
c.nombre AS cliente,
c.ciudad,
tc.nombre AS tipo_cliente,
tc.descuento_base
FROM clientes c
INNER JOIN tipos_cliente tc ON tc.id = c.tipo_cliente_id
ORDER BY c.nombre ASC;
Aquí usamos alias:
clientes c significa que a la tabla clientes le diremos c.tipos_cliente tc significa que a la tabla tipos_cliente le diremos tc.AS cliente cambia el nombre visual de la columna en el resultado.SELECT
p.codigo,
p.nombre AS producto,
cp.nombre AS categoria,
p.precio,
p.existencia
FROM productos p
INNER JOIN categorias_producto cp ON cp.id = p.categoria_id
ORDER BY cp.nombre ASC, p.nombre ASC;
SELECT
v.folio,
v.fecha_venta,
c.nombre AS cliente,
e.nombre AS vendedor,
v.subtotal,
v.descuento,
v.total
FROM ventas v
INNER JOIN clientes c ON c.id = v.cliente_id
INNER JOIN empleados e ON e.id = v.vendedor_id
ORDER BY v.fecha_venta DESC;
Esta consulta ya junta tres tablas:
SELECT
v.folio,
v.fecha_venta,
c.nombre AS cliente,
e.nombre AS vendedor,
p.nombre AS producto,
vd.cantidad,
vd.precio_unitario,
vd.descuento_linea,
vd.total_linea
FROM venta_detalles vd
INNER JOIN ventas v ON v.id = vd.venta_id
INNER JOIN clientes c ON c.id = v.cliente_id
INNER JOIN empleados e ON e.id = v.vendedor_id
INNER JOIN productos p ON p.id = vd.producto_id
ORDER BY v.fecha_venta ASC, v.folio ASC;
Las funciones de agregación sirven para resumir información. No muestran solamente registros, sino cálculos sobre grupos de registros.
| Función | Qué hace |
|---|---|
COUNT() |
Cuenta registros |
SUM() |
Suma valores |
AVG() |
Calcula promedio |
MIN() |
Obtiene el valor menor |
MAX() |
Obtiene el valor mayor |
SELECT
COUNT(*) AS total_clientes
FROM clientes;
SELECT
SUM(total) AS total_vendido
FROM ventas
WHERE estado = 'cerrada';
SELECT
AVG(total) AS promedio_venta
FROM ventas
WHERE estado = 'cerrada';
SELECT
e.nombre AS vendedor,
COUNT(v.id) AS cantidad_ventas,
SUM(v.total) AS total_vendido,
AVG(v.total) AS promedio_venta
FROM ventas v
INNER JOIN empleados e ON e.id = v.vendedor_id
WHERE v.estado = 'cerrada'
GROUP BY e.id, e.nombre
ORDER BY total_vendido DESC;
Aquí aparece GROUP BY.
Significa que no queremos una fila por cada venta, sino una fila por cada vendedor.
SELECT
tc.nombre AS tipo_cliente,
COUNT(v.id) AS cantidad_ventas,
SUM(v.total) AS total_vendido
FROM ventas v
INNER JOIN clientes c ON c.id = v.cliente_id
INNER JOIN tipos_cliente tc ON tc.id = c.tipo_cliente_id
WHERE v.estado = 'cerrada'
GROUP BY tc.id, tc.nombre
ORDER BY total_vendido DESC;
SELECT
p.codigo,
p.nombre AS producto,
SUM(vd.cantidad) AS unidades_vendidas,
SUM(vd.total_linea) AS total_generado
FROM venta_detalles vd
INNER JOIN productos p ON p.id = vd.producto_id
GROUP BY p.id, p.codigo, p.nombre
ORDER BY unidades_vendidas DESC;
WHERE filtra registros antes de agrupar.
HAVING filtra después de agrupar.
SELECT
e.nombre AS vendedor,
COUNT(v.id) AS cantidad_ventas,
SUM(v.total) AS total_vendido
FROM ventas v
INNER JOIN empleados e ON e.id = v.vendedor_id
WHERE v.estado = 'cerrada'
GROUP BY e.id, e.nombre
HAVING COUNT(v.id) > 2
ORDER BY cantidad_ventas DESC;
No usamos WHERE COUNT(v.id) > 2 porque COUNT() todavía no existe antes de agrupar.
Primero se agrupa, luego se filtra con HAVING.
SELECT
p.nombre AS producto,
SUM(vd.total_linea) AS total_generado
FROM venta_detalles vd
INNER JOIN productos p ON p.id = vd.producto_id
GROUP BY p.id, p.nombre
HAVING SUM(vd.total_linea) > 3000
ORDER BY total_generado DESC;
Una consulta anidada es una consulta dentro de otra consulta. También se le llama subconsulta.
SELECT
id,
codigo,
nombre,
precio
FROM productos
WHERE precio > (
SELECT AVG(precio)
FROM productos
)
ORDER BY precio DESC;
La consulta de adentro calcula el precio promedio de todos los productos:
SELECT AVG(precio)
FROM productos;
Luego la consulta de afuera trae solo los productos cuyo precio es mayor a ese promedio.
SELECT
id,
nombre,
ciudad
FROM clientes
WHERE id IN (
SELECT cliente_id
FROM ventas
)
ORDER BY nombre ASC;
IN sirve para comparar contra una lista de valores.
La subconsulta devuelve una lista de clientes que aparecen en ventas.
SELECT
id,
nombre,
ciudad
FROM clientes
WHERE id NOT IN (
SELECT cliente_id
FROM ventas
)
ORDER BY nombre ASC;
SELECT
folio,
fecha_venta,
total
FROM ventas
WHERE total > (
SELECT AVG(total)
FROM ventas
WHERE estado = 'cerrada'
)
ORDER BY total DESC;
SELECT
id,
codigo,
nombre,
precio
FROM productos
WHERE id NOT IN (
SELECT producto_id
FROM venta_detalles
)
ORDER BY nombre ASC;
Esta consulta ya es más avanzada, pero sigue siendo entendible si la dividimos.
SELECT
vendedor,
total_vendido
FROM (
SELECT
e.id,
e.nombre AS vendedor,
SUM(v.total) AS total_vendido
FROM ventas v
INNER JOIN empleados e ON e.id = v.vendedor_id
WHERE v.estado = 'cerrada'
GROUP BY e.id, e.nombre
) AS ventas_por_vendedor
WHERE total_vendido > (
SELECT AVG(total_vendido)
FROM (
SELECT
vendedor_id,
SUM(total) AS total_vendido
FROM ventas
WHERE estado = 'cerrada'
GROUP BY vendedor_id
) AS resumen
)
ORDER BY total_vendido DESC;
Aquí hay una consulta interna que primero calcula cuánto vendió cada vendedor. Después la consulta externa se queda solo con los vendedores que superan el promedio.
A veces un problema se puede resolver con subconsulta o con JOIN. No siempre hay una sola forma correcta.
SELECT
id,
nombre
FROM clientes
WHERE id IN (
SELECT cliente_id
FROM ventas
);
SELECT DISTINCT
c.id,
c.nombre
FROM clientes c
INNER JOIN ventas v ON v.cliente_id = c.id;
DISTINCT evita repetir clientes.
Si un cliente tiene varias ventas, el JOIN puede traerlo varias veces.
Con DISTINCT mostramos cada cliente una sola vez.
Una consulta avanzada no es avanzada porque sea enorme. Es avanzada cuando responde una pregunta de negocio con claridad.
Ejemplos de preguntas reales:
SELECT
p.nombre AS producto,
SUM(vd.cantidad) AS unidades_vendidas,
SUM(vd.total_linea) AS total_vendido,
SUM((vd.precio_unitario - p.costo) * vd.cantidad) AS utilidad_estimada
FROM venta_detalles vd
INNER JOIN productos p ON p.id = vd.producto_id
GROUP BY p.id, p.nombre
ORDER BY utilidad_estimada DESC;
Esta consulta no solo suma ventas. También calcula utilidad aproximada restando costo al precio de venta.
Si el alumno le pide a una IA “hazme la base de datos”, puede recibir cualquier cosa. Lo correcto es darle contexto y restricciones.
Hazme una base de datos para una empresa.
Diseña una base de datos relacional para una empresa que vende productos.
Debe incluir:
- empleados
- supervisores
- clientes
- tipos de cliente
- productos
- categorías
- ventas
- detalle de ventas
Explica:
- qué representa cada tabla
- cuál es la llave primaria
- cuáles son las llaves foráneas
- qué relaciones existen
- por qué una venta debe tener encabezado y detalle
- qué campos deben ser DECIMAL, VARCHAR, DATE o DATETIME
No mezcles todos los datos en una sola tabla.
No guardes precios como texto.
No dupliques el nombre del cliente en ventas si ya existe cliente_id.
Estos ejercicios son para practicar SQL Avanzado I sin irnos todavía demasiado lejos.
GROUP BY y HAVING.
Explica con tus palabras:
WHERE y HAVING?En esta clase vimos una entrada a SQL avanzado, pero sin perder la base: una consulta compleja solo tiene sentido si la estructura de la base está bien pensada.