Base de Datos SQL JOIN GROUP BY Consultas Anidadas

Tema 10: Repaso de Base de Datos y SQL Avanzado I

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.

Contenido de la clase

  1. Objetivo de la clase
  2. Problema que vamos a modelar
  3. Conceptos básicos de diseño de base de datos
  4. Relaciones entre tablas
  5. Script SQL de ejemplo con datos
  6. Consultas base de repaso
  7. JOINs explicados
  8. COUNT, SUM, AVG, MIN, MAX y GROUP BY
  9. HAVING
  10. Consultas anidadas
  11. Ejercicios

1) Objetivo de la clase

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.

Idea clave:
Una base de datos bien diseñada permite que el sistema crezca sin volverse un desastre. Una base mal diseñada obliga a hacer consultas raras, repetir datos, duplicar información y complicar el código.

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.

2) Problema que vamos a modelar

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:

Empresa │ ├── Empleados │ ├── Vendedores │ └── Supervisores │ ├── Clientes │ └── Tipos de cliente │ ├── Productos │ └── Categorías de producto │ └── Ventas └── Detalle de ventas

3) Conceptos básicos para diseñar una base de datos

Tabla

Una tabla representa un conjunto de datos de una misma cosa. Por ejemplo: empleados, clientes, productos o ventas.

Campo o columna

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.

Registro o fila

Una fila representa un elemento real guardado en la tabla. Por ejemplo, un cliente específico llamado Ana López.

Llave primaria

La llave primaria identifica de forma única cada registro. Normalmente usamos un campo llamado id.

id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY

Llave foránea

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)
Explicación sencilla:
La llave primaria dice: “este soy yo”.
La llave foránea dice: “yo pertenezco o me relaciono con aquel registro de otra tabla”.

Tipos de datos

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
Error común:
Guardar precios como texto. Un precio debe ser numérico, normalmente DECIMAL(10,2), porque después vamos a sumar, promediar, ordenar y calcular reportes.

4) Relaciones entre tablas

En una base de datos relacional, las tablas no viven aisladas. Se conectan entre ellas usando llaves primarias y llaves foráneas.

Uno a muchos

Una categoría puede tener muchos productos. Pero un producto pertenece a una sola categoría.

categorias.id ↓ productos.categoria_id

Muchos a muchos

Una venta puede tener muchos productos, y un producto puede aparecer en muchas ventas. Para resolver eso usamos una tabla intermedia: venta_detalles.

ventas ↓ venta_detalles ↑ productos
Regla práctica:
Si una cosa puede tener muchas de otra, probablemente necesitas una relación. Si ambas cosas pueden repetirse entre sí, probablemente necesitas una tabla intermedia.

5) Script SQL de ejemplo con tablas y datos

Este script crea una base de práctica para la clase. Incluye empleados, supervisores, vendedores, clientes, productos, ventas y detalle de ventas.

Ojo:
Este script usa DROP DATABASE porque es una base de práctica para clase. No se debe ejecutar sobre una base real con información importante.
SQL script_empresa_sql_avanzado_1.sql
-- =========================================================
-- 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;

6) Consultas base de repaso

Antes de entrar a consultas más avanzadas, repasamos consultas normales.

Consultar todos los clientes

SELECT *
FROM clientes;

SELECT * significa “trae todas las columnas”. Para pruebas está bien, pero en proyectos reales conviene pedir solo las columnas necesarias.

Consultar columnas específicas

SELECT
  id,
  nombre,
  ciudad
FROM clientes;

Filtrar con WHERE

SELECT
  id,
  nombre,
  ciudad
FROM clientes
WHERE ciudad = 'León';

Ordenar resultados

SELECT
  id,
  nombre,
  precio
FROM productos
ORDER BY precio DESC;

DESC ordena de mayor a menor. ASC ordena de menor a mayor.

Limitar resultados

SELECT
  id,
  nombre,
  precio
FROM productos
ORDER BY precio DESC
LIMIT 5;
Pregunta que responde esta consulta:
¿Cuáles son los 5 productos más caros?

7) JOINs explicados

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.

Ejemplo 1: clientes con su tipo de cliente

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:

Ejemplo 2: productos con su categoría

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;
Qué estamos preguntando:
Muéstrame los productos, pero también dime a qué categoría pertenece cada uno.

Ejemplo 3: ventas con cliente y vendedor

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:

ventas ↓ cliente_id clientes ventas ↓ vendedor_id empleados

Ejemplo 4: detalle completo de ventas

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;
Importante:
Esta consulta muestra renglones de productos vendidos. Si una venta tuvo 3 productos, aparecerán 3 filas para esa venta. Eso no significa que haya 3 ventas, significa que hay 3 detalles dentro de la venta.

8) Funciones de agregación y GROUP BY

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

Ejemplo 1: total de clientes

SELECT
  COUNT(*) AS total_clientes
FROM clientes;

Ejemplo 2: total vendido en toda la empresa

SELECT
  SUM(total) AS total_vendido
FROM ventas
WHERE estado = 'cerrada';

Ejemplo 3: promedio de venta

SELECT
  AVG(total) AS promedio_venta
FROM ventas
WHERE estado = 'cerrada';

Ejemplo 4: ventas por vendedor

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.

Sin GROUP BY: venta 1 venta 2 venta 3 venta 4 Con GROUP BY vendedor: Luis Herrera total vendido Carla Méndez total vendido Jorge Castillo total vendido

Ejemplo 5: ventas por tipo de cliente

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;

Ejemplo 6: productos más vendidos por cantidad

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;
Pregunta que responde:
¿Qué productos se han vendido más y cuánto dinero han generado?

9) HAVING: filtrar resultados agrupados

WHERE filtra registros antes de agrupar. HAVING filtra después de agrupar.

WHERE: Filtra filas normales antes del GROUP BY. HAVING: Filtra resultados calculados después del GROUP BY.

Ejemplo: vendedores con más de 2 ventas

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.

Ejemplo: productos que generaron más de 3000 en ventas

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;

10) Consultas anidadas

Una consulta anidada es una consulta dentro de otra consulta. También se le llama subconsulta.

Idea sencilla:
Primero SQL resuelve la consulta de adentro. Luego usa ese resultado para resolver la consulta de afuera.

Ejemplo 1: productos con precio mayor al promedio

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.

Ejemplo 2: clientes que han comprado

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.

Ejemplo 3: clientes que no han comprado

SELECT
  id,
  nombre,
  ciudad
FROM clientes
WHERE id NOT IN (
  SELECT cliente_id
  FROM ventas
)
ORDER BY nombre ASC;
Comentario importante:
Esta consulta sirve para detectar clientes registrados que todavía no han tenido ventas. En sistemas reales esto puede usarse para seguimiento comercial.

Ejemplo 4: ventas mayores al promedio de ventas

SELECT
  folio,
  fecha_venta,
  total
FROM ventas
WHERE total > (
  SELECT AVG(total)
  FROM ventas
  WHERE estado = 'cerrada'
)
ORDER BY total DESC;

Ejemplo 5: productos que nunca se han vendido

SELECT
  id,
  codigo,
  nombre,
  precio
FROM productos
WHERE id NOT IN (
  SELECT producto_id
  FROM venta_detalles
)
ORDER BY nombre ASC;

Ejemplo 6: vendedores que han vendido más que el promedio por vendedor

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.

Este es el tipo de pensamiento que deben desarrollar:
No intentar resolver todo de golpe. Primero preguntar: “¿Qué dato necesito calcular primero?” y después “¿cómo uso ese resultado?”.

11) Comparación: subconsulta vs JOIN

A veces un problema se puede resolver con subconsulta o con JOIN. No siempre hay una sola forma correcta.

Clientes que han comprado usando subconsulta

SELECT
  id,
  nombre
FROM clientes
WHERE id IN (
  SELECT cliente_id
  FROM ventas
);

Clientes que han comprado usando JOIN

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.

Conclusión:
Una subconsulta puede ser más fácil de leer para ciertas preguntas. Un JOIN puede ser mejor cuando necesitamos mostrar información de ambas tablas.

12) SQL avanzado no es escribir consultas largas

Una consulta avanzada no es avanzada porque sea enorme. Es avanzada cuando responde una pregunta de negocio con claridad.

Ejemplos de preguntas reales:

Ejemplo: utilidad por producto

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.

13) Cómo pedirle esto a una IA correctamente

Si el alumno le pide a una IA “hazme la base de datos”, puede recibir cualquier cosa. Lo correcto es darle contexto y restricciones.

Prompt malo

Hazme una base de datos para una empresa.

Prompt mejor

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.
Mensaje para clase:
Saber usar IA no es pedir “hazlo todo”. Saber usar IA es explicar el problema, poner restricciones y revisar si la respuesta respeta el diseño.

14) Ejercicios para clase

Estos ejercicios son para practicar SQL Avanzado I sin irnos todavía demasiado lejos.

  1. Mostrar todos los productos con su categoría.
  2. Mostrar todas las ventas con nombre del cliente y nombre del vendedor.
  3. Mostrar cuántas ventas hizo cada vendedor.
  4. Mostrar cuánto dinero vendió cada vendedor.
  5. Mostrar el promedio de venta por vendedor.
  6. Mostrar los productos cuyo precio sea mayor al precio promedio de todos los productos.
  7. Mostrar clientes que sí han comprado usando una subconsulta.
  8. Mostrar clientes que no han comprado usando una subconsulta.
  9. Mostrar productos que nunca se han vendido.
  10. Mostrar vendedores cuyo total vendido sea mayor a 10000 usando GROUP BY y HAVING.

Ejercicio extra

Explica con tus palabras:

15) Cierre de la clase

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.

Resumen:
Primero se diseña bien la base.
Luego se entienden las relaciones.
Después se hacen consultas que respondan preguntas reales.
Y finalmente la IA puede ayudar a escribir código, pero nosotros debemos saber si lo que genera está bien.