Stored Procedure (Procedimiento Almacenado) Simple
Este SP:
- Crea la factura.
- La guarda como “PENDIENTE”.
- Calcula neto, IVA y total.
La deja lista para que después un sistema externo la envíe a AFIP
CREATE TABLE clientes (
id INT AUTO_INCREMENT PRIMARY KEY,
razon_social VARCHAR(150),
cuit VARCHAR(20)
);
INSERT INTO clientes (razon_social, cuit) VALUES
('Transporte Los Andes S.A.', '30-71234567-9'),
('Comercial Martínez SRL', '33-80123456-3'),
('AgroServicios del Norte S.A.', '30-65432109-1'),
('Consultora Innovar SRL', '33-78901234-5'),
('Logística del Plata S.A.', '30-89012345-6'),
('Panificados San Jorge S.R.L.', '33-56789012-7'),
('Metalúrgica El Progreso S.A.', '30-91234567-4'),
('Lácteos Santa María S.R.L.', '33-67890123-8'),
('Distribuidora Luz y Fuerza S.A.', '30-82345679-2'),
('Software NovaTech S.R.L.', '33-93456780-1'),
('Estudio Contable Ríos & Asociados', '23-34567890-5'),
('ElectroServicios Patagonia S.A.', '30-45678901-3');
SELECT * FROM clientes;
CREATE TABLE facturas (
id INT AUTO_INCREMENT PRIMARY KEY,
cliente_id INT,
tipo VARCHAR(3), -- A, B o C
neto DECIMAL(12,2),
iva DECIMAL(12,2),
total DECIMAL(12,2),
fecha DATETIME,
estado VARCHAR(20), -- PENDIENTE / APROBADA / RECHAZADA
FOREIGN KEY (cliente_id) REFERENCES clientes(id)
);
INSERT INTO facturas (cliente_id, tipo, neto, iva, total, fecha, estado) VALUES
(1, 'A', 15000.00, 3150.00, 18150.00, '2025-01-05 10:30:00', 'APROBADA'), -- 1
(2, 'A', 32000.00, 6720.00, 38720.00, '2025-01-06 14:12:00', 'APROBADA'), -- 2
(3, 'B', 8700.00, 1827.00, 10527.00, '2025-01-07 09:18:00', 'APROBADA'), -- 3
(4, 'B', 12900.00, 2709.00, 15609.00, '2025-01-08 11:52:00', 'PENDIENTE'), -- 4
(5, 'C', 4500.00, 0.00, 4500.00, '2025-01-09 16:40:00', 'APROBADA'), -- 5 (C → sin IVA)
(6, 'A', 21200.00, 4452.00, 25652.00, '2025-01-10 13:25:00', 'APROBADA'), -- 6
(7, 'B', 9800.00, 2058.00, 11858.00, '2025-01-11 10:04:00', 'APROBADA'), -- 7
(8, 'A', 15400.00, 3234.00, 18634.00, '2025-01-12 08:50:00', 'PENDIENTE'), -- 8
(9, 'C', 3900.00, 0.00, 3900.00, '2025-01-12 17:23:00', 'APROBADA'), -- 9 (C)
(10, 'B', 7600.00, 1596.00, 9196.00, '2025-01-13 12:10:00', 'APROBADA'), -- 10
(11, 'A', 50000.00, 10500.00, 60500.00, '2025-01-14 15:35:00', 'APROBADA'), -- 11
(12, 'C', 2100.00, 0.00, 2100.00, '2025-01-15 09:45:00', 'PENDIENTE'); -- 12 (C)
SELECT * FROM facturas;
Consideraciones impositivas:
- Factura A: el IVA se calcula y se discrimina sobre el neto.
- Factura B: el IVA se calcula internamente pero no se discrimina en la visualización al cliente.
- Factura C: no aplica IVA.
DELIMITER $$
CREATE PROCEDURE sp_crear_factura_simple (
IN p_cliente_id INT,
IN p_tipo VARCHAR(3),
IN p_neto DECIMAL(12,2)
)
BEGIN
DECLARE v_iva DECIMAL(12,2);
DECLARE v_total DECIMAL(12,2);
-- VALIDACIONES
IF NOT EXISTS (
SELECT 1
FROM clientes
WHERE id = p_cliente_id
) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Cliente inexistente';
END IF;
IF p_tipo NOT IN ('A','B','C') THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Tipo de factura inválido';
END IF;
-- CÁLCULOS
IF p_tipo IN ('A','B') THEN
SET v_iva = p_neto * 0.21;
ELSE
SET v_iva = 0;
END IF;
SET v_total = p_neto + v_iva;
-- INSERT
INSERT INTO facturas (cliente_id, tipo, neto, iva, total, fecha, estado)
VALUES (p_cliente_id, p_tipo, p_neto, v_iva, v_total, NOW(), 'PENDIENTE');
-- SALIDA
SELECT
LAST_INSERT_ID() AS id_factura,
p_cliente_id AS cliente,
p_tipo AS tipo,
p_neto AS neto,
v_iva AS iva,
v_total AS total,
'PENDIENTE (esperando envío a AFIP)' AS estado;
END $$
DELIMITER ;
-- Ver los SP
SELECT ROUTINE_NAME, ROUTINE_SCHEMA, CREATED, LAST_ALTERED
FROM information_schema.ROUTINES
WHERE ROUTINE_TYPE = 'PROCEDURE'
AND ROUTINE_SCHEMA = 'tu_base_de_datos';
¿Qué hace este SP?
- Recibe: cliente + tipo de factura + neto.
- Calcula IVA según reglas impositivas simplificadas.
- Calcula total.
- Guarda todo en la tabla facturas.
- La deja en estado PENDIENTE, lista para enviar a AFIP después.
- Devuelve un resumen claro de la factura generada.
CALL sp_crear_factura_simple(1, 'A', 10000);
CALL sp_crear_factura_simple(2, 'B', 5000);
CALL sp_crear_factura_simple(3, 'C', 7500);
id_factura | cliente | tipo | neto | iva | total | estado
---------------------------------------------------------------------------
15 | 1 | A | 10000 | 2100.00 | 12100.00| PENDIENTE (esperando envío a AFIP)
Nota:
Este stored procedure simula la generación de facturas previo a su validación y autorización por AFIP.
El cálculo impositivo se presenta de forma simplificada con fines demostrativos.
Triggers (Disparadores)
En muchas aplicaciones financieras, es necesario garantizar que ciertas reglas del negocio se cumplan de manera automática dentro de la base de datos.
Un ejemplo común es el control de saldos negativos en cuentas bancarias. Si un cliente queda en descubierto, se debe generar un registro de préstamo automáticamente.
Para implementar esta funcionalidad en MySQL, se utilizan los triggers, que son bloques de código que se ejecutan automáticamente cuando ocurre un evento sobre una tabla (INSERT, UPDATE o DELETE).
"Los triggers se usan hoy en día, pero ya no son la primera opción en muchos escenarios modernos."
SÍ se usan hoy y en entornos modernos siguen siendo muy útiles en:
1. Auditoría interna: Registrar automáticamente cambios sin depender de que la app lo haga, quién modificó un registro, cuándo lo hizo, antes y después del valor.
2. Reglas de negocio críticas: Que sí o sí deben cumplirse, sin importar la aplicación, evitar ventas con stock negativo, evitar precios por debajo de cierto margen, evitar borrar clientes con deudas.
3. Automatización simple dentro de la BD: Calcular campos derivados, actualizar totales, sincronizar tablas pequeñas.
No se recomiendan para: Lógica compleja, flujo principal del negocio, sistemas distribuidos.
Objetivo del caso de ejemplo:
Automatizar la creación de préstamos en la base de datos cada vez que una cuenta queda con saldo negativo, evitando inconsistencias y asegurando que la lógica del negocio se cumpla sin depender de la aplicación que interactúa con la base de datos.
Diseño de la base de datos.
Se utilizarán dos tablas principales: Cuentas y Prestamos
Almacena información de las cuentas bancarias y sus saldos.
CREATE TABLE cuentas (
id INT AUTO_INCREMENT PRIMARY KEY,
titular VARCHAR(100) NOT NULL,
saldo DECIMAL(10,2) NOT NULL
);
INSERT INTO cuentas (titular, saldo) VALUES
('Juan Pérez', 500.00),
('María Gómez', 1200.00),
('Carlos López', 150.00),
('Ana Fernández', 800.00),
('Pablo Ramírez', 300.00);
SELECT * FROM cuentas;
Registra los préstamos generados automáticamente por saldo negativo.
CREATE TABLE prestamos (
id INT AUTO_INCREMENT PRIMARY KEY,
id_cuenta INT NOT NULL,
monto DECIMAL(10,2) NOT NULL,
fecha_creacion DATETIME NOT NULL,
motivo VARCHAR(255),
FOREIGN KEY (id_cuenta) REFERENCES cuentas(id)
);
INSERT INTO prestamos (id_cuenta, monto, fecha_creacion, motivo) VALUES
(2, 500.00, '2025-10-15 09:30:00', 'Préstamo personal aprobado'),
(3, 1200.00, '2025-11-02 14:15:00', 'Refinanciación de tarjeta de crédito'),
(4, 300.00, '2025-12-01 11:45:00', 'Préstamo rápido por emergencia'),
(5, 750.00, '2025-12-05 16:20:00', 'Préstamo preaprobado por campaña comercial');
SELECT * FROM prestamos;
Se define un trigger que se activa después de actualizar la tabla cuentas (AFTER UPDATE).
DELIMITER $$
CREATE TRIGGER trg_crear_prestamo_por_descubierto
AFTER UPDATE ON cuentas
FOR EACH ROW
BEGIN
IF OLD.saldo >= 0 AND NEW.saldo < 0 THEN
INSERT INTO prestamos (id_cuenta, monto, fecha_creacion, motivo)
VALUES (
NEW.id,
ABS(NEW.saldo),
NOW(),
'Descubierto automático por saldo negativo'
);
END IF;
END $$
DELIMITER ;
-- Para ver el trigger creado en la base de datos de trabajo.
SELECT *
FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = 'tu_base_de_datos';
Explicación técnica:
- AFTER UPDATE ON cuentas: el trigger se ejecuta después de una actualización de la tabla cuentas.
- FOR EACH ROW: se aplica por cada fila afectada por el UPDATE.
- NEW.saldo: referencia el saldo actualizado de la cuenta.
- ABS(NEW.saldo): convierte el valor negativo en positivo para registrar correctamente el monto del préstamo.
- NOW(): fecha y hora de creación del préstamo.
- motivo: descripción del motivo del préstamo generado automáticamente.
Estado inicial
SELECT * FROM cuentas;
SELECT * FROM prestamos;
Supongamos que Juan Pérez realiza un pago de 600 unidades, dejando su saldo en negativo:
UPDATE cuentas
SET saldo = saldo - 600
WHERE id = 1;
Tabla cuentas y prestamos
SELECT * FROM cuentas;
SELECT * FROM prestamos;
- Ejecución automática: los triggers se ejecutan en la base de datos, sin necesidad de intervención de la aplicación.
- Impacto en rendimiento: si se aplican a tablas muy grandes o con operaciones masivas, pueden afectar la velocidad de actualización.
- Cadenas de triggers: si un trigger modifica otra tabla que también tiene triggers, puede generarse ejecución encadenada.
- Depuración y documentación: es fundamental mantener un registro de triggers y documentar su funcionamiento para evitar confusiones.
- Simplificación del modelo: se asume un único préstamo por evento de descubierto.
Nota:
En un sistema real, este trigger estaría acompañado por transacciones para garantizar la atomicidad entre la actualización del saldo y la generación del préstamo.
Los triggers en MySQL permiten automatizar reglas de negocio críticas y mantener la integridad de los datos. En este caso, garantizan que cualquier saldo negativo en una cuenta genere automáticamente un préstamo correspondiente. Este enfoque evita errores, mantiene consistencia y centraliza la lógica directamente en la base de datos, haciendo el sistema más confiable
Sector: Retail + Ecommerce omnicanal (tipo Frávega, Musimundo, Coto, Farmacity, Dexter, Garbarino, Open Sports).
La consulta calcula la facturación total de cada mes, sumando el valor económico de todas las ventas realizadas en ese período.
Para eso:
- Convierte la fecha a formato año-mes para agrupar las ventas por mes.
- Multiplica cantidad × precio_unitario para obtener el monto de cada venta.
- Suma el total del mes con SUM().
- Ordena los resultados cronológicamente.
Este KPI se usa en todas las empresas para analizar:
- Tendencias de ventas.
- Estacionalidad (meses fuertes y meses bajos).
- Evolución comercial.
- Efecto de campañas, promociones o eventos estacionales.
Es uno de los KPIs más importantes de cualquier área de Data Analytics, BI, Comercial y Finanzas.
SELECT
DATE_FORMAT(fecha, '%Y-%m') AS mes,
SUM(cantidad * precio_unitario) AS venta_total
FROM ventas
GROUP BY mes
ORDER BY mes;
Este KPI mide cuánto gasta en promedio un cliente por compra.
La consulta:
- Calcula el valor de cada venta usando cantidad × precio_unitario.
- Obtiene el promedio de todos esos montos mediante AVG().
Es un indicador clave en cualquier empresa porque permite analizar:
- Poder adquisitivo del cliente.
- Efectividad de promociones o combos.
- Cambios en el comportamiento de compra.
- Impacto de precios y estrategias comerciales.
El Ticket Promedio ayuda a entender si los clientes están comprando más o menos en cada visita o transacción, y es uno de los KPIs más monitoreados en Retail, E-commerce y Servicios.
SELECT
AVG(cantidad * precio_unitario) AS ticket_promedio
FROM ventas;
Este KPI compara el rendimiento comercial entre canales de venta (por ejemplo: Tienda Física vs E-commerce vs Marketplace).
La consulta SQL:
- Agrupa las ventas por canal.
- Calcula la facturación (SUM(cantidad * precio_unitario)).
- Cuenta cuántos clientes únicos compraron en cada canal (COUNT(DISTINCT cliente_id)).
- Suma la cantidad total de unidades vendidas (SUM(cantidad)).
Este análisis permite entender:
- Qué canal vende más (presencial, online, marketplace).
- En qué canal hay más clientes únicos (adquisición).
- Diferencias en volumen y comportamiento entre canales.
- Impacto de estrategias omnicanal (retail + ecommerce).
Es un KPI clave hoy en Argentina para Retail, E-commerce, Supermercados, Moda y Tecnología, donde la integración omnicanal es fundamental.
SELECT
canal,
SUM(cantidad * precio_unitario) AS facturacion,
COUNT(DISTINCT cliente_id) AS clientes,
SUM(cantidad) AS unidades
FROM ventas
GROUP BY canal;
Este KPI identifica qué productos generan la mayor parte de la facturación, aplicando el principio de Pareto (también conocido como regla 80/20, aunque en Argentina muchas empresas usan el corte del 70%).
La consulta SQL realiza:
- Suma de ventas por producto (cantidad × precio_unitario).
- Ordena los productos desde el que más factura al que menos.
- Calcula la venta acumulada usando una window function (SUM() OVER).
Determina qué productos componen el 70% de la facturación total, clasificándolos como:
- "PARETO – CONTRIBUYE AL 70%"
- "RESTO"
Este KPI permite a las empresas:
- Identificar los productos estrella (los que sostienen el negocio).
- Optimizar stock y reposición.
- Enfocar campañas comerciales en lo que más vende.
- Reducir complejidad operativa y mejorar márgenes.
Es extremadamente usado en Retail, Supermercados, E-commerce y cualquier negocio con catálogo amplio.
WITH productos AS (
SELECT
producto_id,
SUM(cantidad * precio_unitario) AS ventas
FROM ventas
GROUP BY producto_id
),
acumulado AS (
SELECT
producto_id,
ventas,
SUM(ventas) OVER (ORDER BY ventas DESC) AS acumulado
FROM productos
)
SELECT
producto_id,
ventas,
acumulado,
CASE
WHEN acumulado <= (SELECT SUM(ventas) * 0.7 FROM productos)
THEN 'PARETO – CONTRIBUYE AL 70%'
ELSE 'RESTO'
END AS clasificacion
FROM acumulado;
Este KPI forma parte del modelo RFM (Recencia, Frecuencia, Monetary), muy usado en Marketing, CRM, E-commerce y BI.
La consulta calcula:
- La última fecha de compra de cada cliente (MAX(fecha)).
- Los días que pasaron desde esa compra hasta hoy (DATEDIFF(CURDATE(), MAX(fecha))).
- Esto genera un indicador llamado recencia_dias, que mide cuán “activo” o “inactivo” está cada cliente.
Este KPI permite:
- Detectar clientes por riesgo de abandono (churn).
- Activar campañas de recuperación.
- Segmentar por antigüedad de compra (0–30, 30–90, 90–180+ días).
- Identificar clientes fieles vs. clientes perdidos.
Es uno de los indicadores más valiosos en empresas argentinas de Retail, E-commerce, Moda, Cosmética, Electro y Servicios de suscripción.
SELECT
cliente_id,
DATEDIFF(CURDATE(), MAX(fecha)) AS recencia_dias
FROM ventas
GROUP BY cliente_id;
Este KPI mide cuántas veces compró cada cliente dentro del período analizado.
Es el segundo componente del modelo RFM (Recencia, Frecuencia, Monetary).
La consulta:
- Agrupa las ventas por cliente.
- Cuenta la cantidad de compras realizadas (COUNT(*)).
- Devuelve un indicador llamado frecuencia_compras.
Este KPI permite evaluar:
- Clientes altamente recurrentes (alta frecuencia).
- Clientes de compra ocasional.
- Engagement y fidelidad.
- Segmentación de clientes para campañas de retención.
- Identificación de los clientes más valiosos en el largo plazo.
En empresas argentinas de Retail, E-commerce, Banca Digital y Servicios, este KPI es clave para entender comportamiento, lealtad y potencial de cada cliente.
SELECT
cliente_id,
COUNT(*) AS frecuencia_compras
FROM ventas
GROUP BY cliente_id;
Este KPI mide cuánto dinero generó cada cliente en total, sumando todas sus compras.
Es el componente M (Monetary) del modelo RFM, uno de los más importantes en CRM, Marketing, E-commerce y BI.
La consulta:
- Agrupa las ventas por cliente.
- Calcula la facturación total aportada por cada uno mediante SUM(cantidad * precio_unitario).
El resultado es el indicador valor_monetario, que representa:
- El valor económico real de cada cliente.
- Cuánto aporta al negocio.
- Qué clientes son “top clientes” o “alto valor”.
- Base para construir estrategias como Customer Lifetime Value (CLV).
Este KPI es fundamental para identificar qué clientes sostienen la facturación y dónde concentrar esfuerzos comerciales.
SELECT
cliente_id,
SUM(cantidad * precio_unitario) AS valor_monetario
FROM ventas
GROUP BY cliente_id;
Este KPI identifica productos cuyo stock está por debajo del nivel mínimo recomendado, señalando posibles riesgos operativos como quiebres de stock, pérdida de ventas o mala disponibilidad.
La consulta SQL:
- Trae el stock actual y el stock mínimo por producto y sucursal.
- Evalúa si el stock actual es menor o igual al mínimo.
Clasifica cada caso como:
- CRÍTICO → requiere reposición inmediata.
- OK → stock suficiente.
Este KPI permite:
- Detectar productos que necesitan reposición urgente.
- Evitar quiebres de stock y pérdidas de ventas.
- Mejorar planificación de compras y logística.
- Coordinar abastecimiento entre sucursales.
Es un KPI clave para Retail, Supermercados, Farmacias, Electro, Moda y cualquier negocio con múltiples sucursales o alto movimiento de inventario.
SELECT
producto_id,
sucursal,
stock_actual,
stock_minimo,
CASE
WHEN stock_actual <= stock_minimo THEN 'CRÍTICO'
ELSE 'OK'
END AS estado_stock
FROM stock;
El Margen Bruto mide cuánto gana la empresa por producto antes de gastos operativos, considerando la diferencia entre el precio de venta y el costo del producto.
En este ejemplo, se asume que el costo representa el 68% del precio de venta, una práctica común en análisis rápidos o cuando no existe una tabla de costos detallada.
La consulta SQL realiza:
- Calcula el margen por unidad precio_unitario - (precio_unitario * 0.68) → equivale al 32% del precio como margen.
- Suma el margen total generado por cada producto con SUM().
- Agrupa por producto_id, para ver cuál es más rentable.
Este KPI se utiliza para:
- Identificar productos con mayor rentabilidad.
- Comparar margen vs. volumen de ventas (productos estrella).
- Tomar decisiones de precios, descuentos y promociones.
- Evaluar contribución por categoría o línea de producto.
En Argentina, este KPI es clave en Retail, E-commerce, Moda, Cosmética, Supermercados y Electro, donde la rentabilidad varía mucho por producto.
SELECT
producto_id,
SUM((precio_unitario - (precio_unitario * 0.68))) AS margen_bruto
FROM ventas
GROUP BY producto_id;
(0.68 se usa como costo estimado para ejemplo; en retail argentino se maneja entre 55% y 75% de costo)
Este KPI permite analizar cuánta facturación genera cada provincia, combinando información de ventas con datos demográficos de los clientes.
Es fundamental para entender el desempeño geográfico del negocio.
La consulta SQL realiza:
- Une las ventas con los clientes mediante un JOIN por cliente_id.
- Agrupa por provincia del cliente.
- Suma la facturación total (cantidad × precio_unitario).
- Ordena las provincias de mayor a menor facturación.
Este KPI permite:
- Identificar regiones más rentables y de mayor demanda.
- Detectar provincias con oportunidad de crecimiento.
- Optimizar campañas comerciales por región.
- Mejorar la asignación de recursos (stock, logística, locales).
En Argentina, este análisis es muy usado en Retail, Ecommerce, Servicios, Banca Digital y cualquier empresa con presencia nacional.
SELECT
c.provincia,
SUM(v.cantidad * v.precio_unitario) AS facturacion
FROM ventas v
JOIN clientes c ON v.cliente_id = c.cliente_id
GROUP BY c.provincia
ORDER BY facturacion DESC;
Para mapas en Power BI.
Este KPI detecta qué sucursales están facturando por debajo del desempeño promedio de todas las sucursales.
Es clave para identificar puntos débiles, zonas que requieren inversión, capacitación del personal o revisión de estrategias comerciales.
La lógica trabaja en dos etapas:
- Calcular la facturación total por sucursal.
- Obtener el promedio general y comparar.
- Si una sucursal factura menos que el promedio global, se considera BAJO RENDIMIENTO.
¿Qué devuelve este KPI?
- sucursal: Nombre o código de la sucursal evaluada.
- total: Monto total facturado por esa sucursal.
estado:
- BAJO RENDIMIENTO: Factura por debajo del promedio general.
- OK: Factura en el promedio o por encima.
Aplicación en BI / Dashboard
- Mostrar un mapa o tabla comparativa de sucursales.
- Aplicar alertas visuales (rojo = bajo rendimiento).
- Priorizar mejoras operativas o comerciales.
- Identificar patrones por región.
WITH facturacion_sucursal AS (
SELECT sucursal, SUM(cantidad * precio_unitario) AS total
FROM ventas
GROUP BY sucursal
),
stats AS (
SELECT AVG(total) AS promedio
FROM facturacion_sucursal
)
SELECT
f.sucursal,
f.total,
CASE
WHEN f.total < s.promedio THEN 'BAJO RENDIMIENTO'
ELSE 'OK'
END AS estado
FROM facturacion_sucursal f
CROSS JOIN stats s;
Comparación contra promedio nacional.
Este KPI analiza el comportamiento de los clientes agrupándolos según el mes en que se dieron de alta.
Permite ver cómo evolucionan sus compras a lo largo del tiempo, medir retención, lealtad y frecuencia de compra.
Es un análisis fundamental en Customer Analytics, Growth, Retention y eCommerce.
¿Qué mide exactamente?
- Cohorte: Grupo de clientes que ingresaron (alta) el mismo mes.
- Mes de compra: Mes en el que efectivamente realizaron compras.
- Clientes activos: Cantidad de clientes únicos de esa cohorte que hicieron al menos una compra en ese mes.
Este análisis permite responder preguntas como:
- ¿Los clientes siguen comprando después de su primer mes?
- ¿Qué cohortes tienen mayor retención?
- ¿Qué mes de alta generó clientes más valiosos?
Resultados que devuelve:
- cohorte: Mes en el que se dieron de alta los clientes (ej. 2024-03).
- mes_compra: Mes en el que realmente realizaron compras (puede ser igual, posterior, o NULL si nunca compraron).
- clientes_activos: Número de clientes distintos que compraron en ese mes.
¿Para qué se usa en BI?
- Construir matrices de cohortes tipo heatmap (mes de alta vs mes de compra).
- Identificar caídas de retención entre meses.
- Medir engagement, frecuencia, y valor de vida (LTV) por cohorte.
Comparar cohortes para detectar:
- Mejor mes de onboarding,
- Impacto de campañas,
- Cambios en comportamiento.
SELECT
DATE_FORMAT(c.fecha_alta, '%Y-%m') AS cohorte,
DATE_FORMAT(v.fecha, '%Y-%m') AS mes_compra,
COUNT(DISTINCT v.cliente_id) AS clientes_activos
FROM clientes c
LEFT JOIN ventas v ON c.cliente_id = v.cliente_id
GROUP BY cohorte, mes_compra
ORDER BY cohorte, mes_compra;
Los KPIs analizados permiten construir un ecosistema de inteligencia de negocios completo, que combina:
- Visión económica (facturación, ticket, margen);
- Visión comercial (top productos, provincias, canales);
- Visión del cliente (RFM + cohortes);
- Visión operativa (stock, sucursales);
- Visión estratégica (retención, rentabilidad, expansión).
En conjunto, estos indicadores permiten tomar decisiones basadas en datos, mejorar la rentabilidad, optimizar la operación, aumentar la retención de clientes y fortalecer la estrategia de crecimiento del negocio.
Cadena de retail con venta presencial y online en Buenos Aires, CABA y Córdoba. Vende electrodomésticos, hogar, tecnología y artículos personales.
Problema de negocio:
En los últimos meses, RetailMax observa:
- Caída en ventas en CABA y GBA.
- Crecimiento irregular de ventas online.
- Quejas de clientes por tiempos de entrega.
- Disminución del índice de recomendación (NPS).
Objetivo del análisis:
Identificar qué variables explican la caída de ventas y qué factores influyen en la satisfacción del cliente.
CREATE DATABASE IF NOT EXISTS retailmax;
USE retailmax;
-- CLIENTES
CREATE TABLE clientes (
cliente_id INT AUTO_INCREMENT PRIMARY KEY,
edad INT,
sexo ENUM('M','F','X'),
provincia VARCHAR(50),
ingresos DECIMAL(12,2),
fecha_registro DATE
);
-- PRODUCTOS
CREATE TABLE productos (
producto_id INT AUTO_INCREMENT PRIMARY KEY,
categoria VARCHAR(50),
precio DECIMAL(10,2)
);
-- VENTAS
CREATE TABLE ventas (
venta_id INT AUTO_INCREMENT PRIMARY KEY,
cliente_id INT,
producto_id INT,
cantidad INT,
total DECIMAL(12,2),
fecha DATE,
canal ENUM('online','presencial'),
FOREIGN KEY (cliente_id) REFERENCES clientes(cliente_id),
FOREIGN KEY (producto_id) REFERENCES productos(producto_id)
);
-- ENVIOS (solo ventas online)
CREATE TABLE envios (
envio_id INT AUTO_INCREMENT PRIMARY KEY,
venta_id INT,
demora_dias INT,
provincia_envio VARCHAR(50),
FOREIGN KEY (venta_id) REFERENCES ventas(venta_id)
);
-- ENCUESTAS
CREATE TABLE encuestas (
encuesta_id INT AUTO_INCREMENT PRIMARY KEY,
cliente_id INT,
satisfaccion INT, -- 1..10
recomendaria BOOLEAN,
comentario TEXT,
fecha DATE,
FOREIGN KEY (cliente_id) REFERENCES clientes(cliente_id)
);
-- Insertar productos
INSERT INTO productos(categoria,precio) VALUES
('Electrodomésticos',199999.90),
('Electrodomésticos',159999.50),
('Celulares',250000.00),
('Celulares',150000.00),
('Hogar',29999.00),
('Hogar',15999.00),
('Belleza',5999.00),
('Informática',350000.00),
('Informática',250000.00);
-- Insertar clientes (60)
INSERT INTO clientes(edad,sexo,provincia,ingresos,fecha_registro) VALUES
(34,'M','CABA',450000,'2023-01-11'),
(29,'F','CABA',380000,'2023-02-15'),
(41,'M','Buenos Aires',520000,'2023-04-10'),
(50,'F','Buenos Aires',650000,'2023-03-20'),
(23,'M','Córdoba',300000,'2023-05-02'),
(38,'F','Córdoba',420000,'2023-01-14'),
(55,'F','CABA',700000,'2023-02-10'),
(44,'M','Buenos Aires',480000,'2023-02-17'),
(32,'F','Córdoba',350000,'2023-03-08'),
(46,'M','Buenos Aires',560000,'2023-04-03');
-- Insertar ventas (online y presencial)
INSERT INTO ventas(cliente_id,producto_id,cantidad,total,fecha,canal) VALUES
(1,1,1,199999.90,'2023-07-10','online'),
(2,3,1,250000.00,'2023-07-11','online'),
(3,5,2,59998.00,'2023-07-15','presencial'),
(1,8,1,350000.00,'2023-07-20','online'),
(4,2,1,159999.50,'2023-07-21','presencial'),
(5,7,1,5999.00,'2023-07-22','online'),
(6,3,1,250000.00,'2023-08-02','online'),
(7,4,1,150000.00,'2023-08-05','online'),
(8,9,1,250000.00,'2023-08-06','presencial'),
(9,6,2,31998.00,'2023-08-07','online'),
(10,8,1,350000.00,'2023-08-10','online');
-- Envíos
INSERT INTO envios(venta_id,demora_dias,provincia_envio) VALUES
(1,7,'CABA'),
(2,12,'CABA'),
(4,15,'Buenos Aires'),
(6,3,'Córdoba'),
(7,10,'Córdoba'),
(8,14,'CABA'),
(10,5,'Buenos Aires'),
(11,11,'Buenos Aires');
-- Encuestas
INSERT INTO encuestas(cliente_id,satisfaccion,recomendaria,comentario,fecha) VALUES
(1,5,0,'demora excesiva','2023-07-25'),
(2,4,0,'llegó golpeado','2023-07-27'),
(5,8,1,'todo ok','2023-07-30'),
(6,7,1,'conforme','2023-08-05'),
(7,3,0,'muy tarde','2023-08-06'),
(10,9,1,'excelente','2023-08-10');
Dataset pequeño pero 100% realista para análisis.
Medidas descriptivas.
Ventas totales, promedio y dispersión:
SELECT
COUNT(*) AS n_ventas,
AVG(total) AS promedio_venta,
STDDEV_SAMP(total) AS desvio_estandar,
VAR_SAMP(total) AS varianza,
MIN(total) AS minimo,
MAX(total) AS maximo,
(MAX(total) - MIN(total)) AS rango,
(STDDEV_SAMP(total) / AVG(total)) AS coef_variacion
FROM ventas;
Resultado obtenido:
a) n ventas = 11
b) promedio_venta ≈ $187.000 (valor típico de una venta)
c) Desvío estándar ≈ $118.418 (Este número te indica cuánta variabilidad hay entre los montos de venta)
d) Varianza = (Desvio Estándar)² = (118.418)² = 14.022.842.451,55
e) mínimo ≈ $5.999
f) máximo ≈ $350.000
g) rango = 350.000 − 5.999 = 344.001 (Enorme diferencia,confirma la variabilidad extrema)
h) Coeficiente de Variación CV = 118.418 / 187.090 = 0.63 (63%) (métrica que permite medir la dispersión en porcentaje.)
→ Alta dispersión (productos de alto y bajo valor).
Interpretación:
- 0–20% → baja dispersión
- 20–50% → moderada
- >50% → muy alta dispersión
63% = dispersión extremadamente alta.
Dispersión = qué tan distintos son los valores entre sí.
Si los valores cambian mucho, hay alta dispersión. Si son parecidos, hay baja dispersión.
Esto es típico en empresas donde: se venden productos premium y productos económicos, hay grandes diferencias de precios entre categorías.
Interpretación general:
El dataset muestra una altísima dispersión en los montos de venta, lo cual indica que los valores no son homogéneos. Existen ventas extremadamente bajas (≈ $6.000) y muy altas (≈ $350.000), lo que genera un coeficiente de variación del 63%, considerado muy alto.
Esto sugiere que la empresa maneja productos de ticket muy diverso, probablemente de categorías diferentes.
Para un análisis más robusto se recomienda segmentar por categoría, tipo de producto o canal de venta para identificar patrones.
Evalua el desempeño de la empresa según el canal de venta, es decir, el medio por el cual los clientes realizan sus compras.
Este análisis permite identificar cuáles canales son más rentables, más usados, más estables o más variables, y detectar oportunidades de mejora.
SELECT canal, COUNT(*) ventas, AVG(total) promedio
FROM ventas GROUP BY canal;
Conclusión:
| Canal | Ventas | Importe promedio |
| ---------- | ------------- | ---------------- |
| Online | mayor volumen | mayor ticket |
| Presencial | menor volumen | ticket medio |
| Canal | Ventas | Promedio por venta |
| ---------- | ------ | ------------------ |
| online | 8 | $198.499,61 |
| presencial | 3 | $156.665,83 |
Online concentra la mayor parte de las operaciones (8 de 11 ventas → 73%).
Presencial representa solo 3 ventas (27%).
⇒ Las ventas online dominan el negocio, pero son también donde aparecen más problemas.
Las ventas online no solo son más frecuentes, sino que además tienen un ticket promedio más alto.
Esto significa que los clientes que compran online gastan más que los que compran presencialmente.
Relación entre demoras y satisfacción – Correlación (Pearson) medida que indica qué tan relacionada está una variable con otra.
Lo que se hace es analizar si existe una relación estadística entre:
- Tiempo de demora (variable numérica, por ejemplo en minutos u horas)
- Nivel de satisfacción (variable numérica, por ejemplo de 1 a 10, o de 1 a 5)
SELECT
(SUM((demora_dias - avg_d)*(satisfaccion - avg_s)) /
(SQRT(SUM(POWER(demora_dias - avg_d,2)))*SQRT(SUM(POWER(satisfaccion - avg_s,2))))) AS correlacion
FROM (
SELECT e.demora_dias, c.satisfaccion,
AVG(e.demora_dias) OVER() avg_d,
AVG(c.satisfaccion) OVER() avg_s
FROM envios e
JOIN ventas v ON e.venta_id = v.venta_id
JOIN encuestas c ON c.cliente_id = v.cliente_id
) t;
Resultado obtenido:
Correlación de Pearson = r ≈ -0.525
Interpretación:
Más demora → menos satisfacción.
La relación es moderada, negativa y consistente.
Una correlación de -0.53 indica:
Relación negativa Moderada entre demoras y satisfacción.
Esto significa que:
- Cuando aumentan las demoras,
- la satisfacción del cliente disminuye de manera marcada.
Según criterios estándar:
| Valor | Interpretación |
| ------------- | -------------- |
| 0.0 – 0.3 | Débil |
| 0.3 – 0.6 | Moderada |
| 0.6 – 0.8 | Fuerte |
| > 0.8 | Muy fuerte |
Interpretación de negocio:
Relación entre demoras y satisfacción (Correlación de Pearson):
El análisis muestra una correlación negativa moderada (r = -0.53) entre el tiempo de demora y el nivel de satisfacción del cliente. Este resultado indica que, a medida que aumentan las demoras, la satisfacción disminuye de forma consistente, evidenciando que los tiempos de entrega son un factor relevante en la experiencia del cliente.
El análisis de demoras consiste en estudiar el tiempo que transcurre entre el inicio y la finalización de un proceso (entrega, atención, resolución, despacho, etc.), con el objetivo de evaluar eficiencia operativa, estabilidad del proceso e impacto en el cliente.
-- Promedio de demora
SELECT
AVG(demora_dias) AS promedio_demora
FROM envios;
-- Mediana de demora
SELECT
AVG(demora_dias) AS mediana_demora
FROM (
SELECT
demora_dias,
ROW_NUMBER() OVER (ORDER BY demora_dias) AS rn,
COUNT(*) OVER () AS total
FROM envios
) t
WHERE rn IN (
FLOOR((total + 1) / 2),
CEIL((total + 1) / 2)
);
-- Tercer cuartil (Q3 – 75%)
SELECT demora_dias AS q3_demora
FROM (
SELECT
demora_dias,
NTILE(4) OVER (ORDER BY demora_dias) AS cuartil
FROM envios
) t
WHERE cuartil = 3
ORDER BY demora_dias DESC
LIMIT 1;
Resultados del análisis de demoras:
- Promedio de demora ≈ 9,6 días.
- Mediana ≈ 10,5 días.
- Q3 ≈ 12 días.
El 25% de los envíos tarda más de 12 días.
Interpretación de negocio:
En el contexto del e-commerce en Argentina, los tiempos de entrega competitivos suelen ubicarse entre 48 horas y 5 días.
El análisis realizado muestra un promedio de demora de 9,6 días, una mediana de ≈ 10,5 días y un tercer cuartil (Q3) de 12 días, lo que indica que el 25% de los envíos presenta demoras superiores a 12 días.
Estos valores se encuentran muy por encima del estándar del mercado, evidenciando ineficiencias en el proceso logístico que impactan negativamente en la satisfacción del cliente y en la competitividad del negocio.
Valores atípicos, son datos que se alejan de forma significativa del comportamiento normal del conjunto de datos.
El análisis de outliers en demoras tiene como objetivo identificar envíos con tiempos de entrega anormalmente altos, que se desvían del comportamiento habitual del proceso logístico y pueden indicar incidentes operativos, cuellos de botella o fallas puntuales.
SELECT demora_dias
FROM envios
WHERE demora_dias > 14;
Resultado obtenido:
Se identificaron envíos con demoras de 15 días, los cuales superan el umbral definido por el tercer cuartil (Q3 ≈ 12 días) y exceden ampliamente los tiempos aceptables para el mercado argentino de e-commerce.
Las demoras superiores a 14 días pueden considerarse outliers operativos, ya que:
- se ubican fuera del rango habitual del proceso,
- afectan de forma directa la experiencia del cliente,
- incrementan reclamos, cancelaciones y costos logísticos.
La presencia de estos casos indica que el proceso no está completamente bajo control, sino que presenta eventos excepcionales que requieren análisis específico (zona, operador logístico, tipo de producto, fecha).
El análisis de satisfacción del cliente tiene como objetivo evaluar la percepción general del servicio brindado, identificar el nivel de consistencia de la experiencia y detectar posibles problemas operativos que impactan directamente en la fidelización y recomendación del cliente.
SELECT
AVG(satisfaccion) AS sat_promedio,
STDDEV_SAMP(satisfaccion) AS dispersion,
AVG(recomendaria) AS tasa_recomendacion
FROM encuestas;
Resultados obtenidos:
- Satisfacción promedio: 6,0
- Desvío estándar (dispersión): 2,37
- Tasa de recomendación: 0,50 (50%)
Interpretación estadística:
Satisfacción promedio = 6,0
El valor promedio indica una experiencia apenas aceptable.
En escalas de 1 a 10, un nivel saludable de satisfacción suele ubicarse por encima de 8.
Conclusión: los clientes no están conformes, solo toleran el servicio.
Dispersión (Desvío estándar) = 2,37
Este valor es alto en relación al promedio.
Tasa de recomendación = 50%
Solo la mitad de los clientes recomendaría la empresa.
En términos de negocio:
< 60% → riesgo reputacional
60–80% → aceptable
80% → excelente
Conclusión: el negocio no genera promotores sólidos.
Relación con la logística (visión integral):
Combinando este análisis con lo visto antes:
- Demoras promedio elevadas.
- Outliers logísticos (>14 días)
- Correlación negativa moderada entre demora y satisfacción (≈ -0,53)
La evidencia estadística es consistente:
Los problemas logísticos impactan directamente en la percepción del cliente y en su intención de recomendar.
Conclusión de negocio final:
La empresa presenta un nivel de satisfacción bajo (6,0), con alta variabilidad (desvío 2,37) y una tasa de recomendación limitada (50%). Estos indicadores confirman una experiencia de cliente inestable y poco competitiva, asociada principalmente a fallas en el proceso logístico.
Variables consideradas:
Demora del envío.
Canal de compra.
Ingreso del cliente.
Importe de la compra.
Hipótesis:
La demora es la variable más influyente en la satisfacción.
En SQL se obtiene la pendiente de la regresión lineal simple demora → satisfacción.
SELECT
(
SUM(e.demora_dias * c.satisfaccion)
- COUNT(*) * AVG(e.demora_dias) * AVG(c.satisfaccion)
)
/
(
SUM(e.demora_dias * e.demora_dias)
- COUNT(*) * AVG(e.demora_dias) * AVG(e.demora_dias)
) AS pendiente
FROM envios e
JOIN ventas v ON e.venta_id = v.venta_id
JOIN encuestas c ON v.cliente_id = c.cliente_id;
Resultado obtenido:
Pendiente = −0,2776
Esto significa:
Por cada día adicional de demora en el envío, la satisfacción promedio baja ~0,28 puntos
(en una escala de 1 a 10).
Es exactamente lo que esperaríamos desde el negocio.
Interpretación estadística (clara y defendible):
- La pendiente es negativa.
- La magnitud no es trivial.
- Confirma la hipótesis planteada.
Conclusión:
La demora del envío tiene un impacto negativo significativo en la satisfacción del cliente.
Hipótesis confirmada.
Calcula el tiempo promedio de entrega por provincia de destino, permitiendo identificar diferencias logísticas regionales.
SELECT provincia_envio, AVG(demora_dias) demora_prom
FROM envios
GROUP BY provincia_envio;
Interpretación del resultado:
Los tiempos de entrega varían significativamente según la provincia:
- CABA presenta la mayor demora promedio, superando los 11 días.
- Buenos Aires mantiene una demora elevada y muy cercana a CABA (10,33 días).
- Córdoba se destaca con una demora considerablemente menor (6,5 días).
Esto implica que un envío a CABA tarda en promedio casi el doble que uno a Córdoba.
⇒ La distribución logística está desbalanceada.
Cruce de ingresos del cliente vs monto gastado:
SELECT c.ingresos, SUM(v.total) gasto
FROM clientes c
JOIN ventas v ON v.cliente_id = c.cliente_id
GROUP BY c.cliente_id;
| Ingresos | Gasto total |
| -------- | ----------- |
| 450.000 | 549.999 |
| 250.000 | 250.000 |
| 520.000 | 59.998 |
| 650.000 | 159.999 |
| 300.000 | 5.999 |
| 700.000 | 150.000 |
| 350.000 | 31.998 |
| 560.000 | 350.000 |
Conclusión:
Si bien el poder adquisitivo influye en el consumo, el gasto del cliente muestra una alta dispersión, evidenciando que factores como hábitos de compra y mix de productos tienen mayor peso que el ingreso.
Correlación:
SELECT
(
SUM( (t.ingresos - avg_vals.avg_ingresos) * (t.gasto - avg_vals.avg_gasto) )
)
/
(
SQRT(
SUM( POW(t.ingresos - avg_vals.avg_ingresos, 2) ) *
SUM( POW(t.gasto - avg_vals.avg_gasto, 2) )
)
) AS correlacion
FROM (
SELECT
c.cliente_id,
c.ingresos,
SUM(v.total) AS gasto
FROM clientes c
JOIN ventas v ON v.cliente_id = c.cliente_id
GROUP BY c.cliente_id, c.ingresos
) t
CROSS JOIN (
SELECT
AVG(ingresos) AS avg_ingresos,
AVG(gasto) AS avg_gasto
FROM (
SELECT
c.ingresos,
SUM(v.total) AS gasto
FROM clientes c
JOIN ventas v ON v.cliente_id = c.cliente_id
GROUP BY c.cliente_id, c.ingresos
) x
) avg_vals;
Resultado: Correlación = r ≈ 0,141
Una correlación de 0,14 indica una: relación positiva débil entre ingresos del cliente y gasto total.
Esto significa que:
- A mayor ingreso hay una leve tendencia a gastar más.
- Pero el efecto es muy bajo.
- El ingreso no explica de forma significativa el comportamiento de gasto.
Qué nos dicen los datos:
El valor confirma lo que ya se observaba visualmente:
- Clientes con ingresos medios presentan gastos altos.
- Clientes con ingresos altos no necesariamente gastan más.
- Existe alta dispersión del gasto para un mismo nivel de ingreso.
El consumo no depende linealmente del poder adquisitivo.
Conclusión clave: El nivel de ingresos del cliente tiene una influencia limitada sobre el gasto total. La baja correlación observada indica que otros factores, como hábitos de compra, frecuencia y tipo de producto, tienen mayor peso en la decisión de consumo.
Si se realiza el gráfico de dispersión va a mostrar una relación débil entre los ingresos del cliente y el gasto total. A pesar de una leve tendencia positiva, se observa una alta dispersión, lo que indica que el nivel de ingresos no determina el comportamiento de consumo.
Hallazgos principales:
- La caída de ventas se explica en gran medida por ineficiencias logísticas, con una demora promedio de entrega cercana a 11 días, alcanzando picos de hasta 12 días en determinadas regiones.
- Se identificó una relación negativa significativa entre la demora del envío y la satisfacción del cliente, evidenciada mediante una regresión lineal simple, donde cada día adicional de demora reduce la satisfacción promedio en aproximadamente 0,28 puntos.
- El canal online representa más del 70% de las ventas y presenta el ticket promedio más alto ($198.499), pero concentra la totalidad de las quejas, asociadas principalmente a problemas de entrega.
El análisis geográfico muestra un desbalance logístico marcado:
- CABA (11 días) y Buenos Aires (10,3 días) registran las mayores demoras, afectando aproximadamente el 70% de la facturación online.
- Córdoba (6,5 días) presenta el mejor desempeño operativo, funcionando como referencia de eficiencia.
El análisis de precios y poder adquisitivo evidencia una alta dispersión en el gasto, con un ticket promedio elevado pero coexistencia de productos de bajo valor, lo que indica una segmentación comercial poco optimizada.
La relación entre ingresos del cliente y gasto total es débil (correlación r = 0,14), lo que sugiere que el poder adquisitivo no determina directamente el consumo. No obstante, el segmento de ingresos altos (>$450.000) representa una oportunidad estratégica para acciones personalizadas.
El canal presencial presenta menor volumen y menor ticket promedio ($156.666), con menor nivel de fricción operativa, pero menor impacto en la facturación total.
Implicancias estratégicas:
- La logística es el principal factor crítico que impacta en la experiencia del cliente y en la sostenibilidad del canal más rentable.
- Mejoras operativas en CABA y GBA tendrían un alto retorno inmediato, al proteger el canal online y mejorar la satisfacción general.
- Las estrategias basadas únicamente en precios o nivel de ingresos resultan insuficientes; la experiencia y el comportamiento de compra son los verdaderos drivers del negocio.
Corto plazo (0–30 días):
- Revisión urgente del proveedor logístico para CABA y GBA.
- Definición y control de SLA contractuales ≤ 5 días.
- Implementación de notificaciones automáticas proactivas para pedidos demorados.
Mediano plazo (30–90 días):
- Implementación de un modelo de distribución local por microzonas.
- Expansión de puntos de retiro (pick-up stores) para reducir dependencia de la última milla.
- Priorización operativa del canal online por su mayor ticket y volumen.
Largo plazo (6+ meses):
- Desarrollo de un modelo predictivo de demoras para optimizar la asignación de transporte.
- Implementación de segmentación RFM para personalizar ofertas según comportamiento de compra e ingresos.
- Integración de métricas de experiencia del cliente en la toma de decisiones operativas.
Conclusión final:
El desempeño del negocio no está limitado por la demanda ni por el poder adquisitivo del cliente, sino por la eficiencia operativa. Optimizar la logística, especialmente en el canal online y en las regiones críticas, es la acción con mayor impacto potencial sobre la satisfacción del cliente y la recuperación de las ventas.
Caso Real:
Una empresa argentina de ventas mayoristas detecta que los reportes de facturación por provincia no coinciden con la facturación total.
El modelo existe hace tiempo, los datos parecen correctos y la query “funciona”, pero los números no cierran.
La tarea es analizar, detectar el problema y proponer una solución.
CREATE TABLE clientes (
cliente_id INT PRIMARY KEY,
nombre VARCHAR(100),
provincia VARCHAR(50)
);
CREATE TABLE ventas (
venta_id INT PRIMARY KEY,
cliente_id INT
);
CREATE TABLE productos (
producto_id INT PRIMARY KEY,
nombre VARCHAR(100),
precio DECIMAL(10,2)
);
-- Tabla intermedia.
CREATE TABLE ventas_detalle (
venta_id INT,
producto_id INT,
cantidad INT
);
La tabla intermedia resuelve la relación muchos a muchos entre ventas y productos, y representa el hecho de negocio.
INSERT INTO clientes VALUES
(1, 'Kiosco Centro', 'Buenos Aires'),
(2, 'Almacén Norte', 'Córdoba');
INSERT INTO ventas VALUES
(100, 1),
(101, 2);
INSERT INTO productos VALUES
(10, 'Gaseosa 2L', 1200),
(11, 'Aceite 1L', 4500);
INSERT INTO ventas_detalle VALUES
(100, 10, 2),
(100, 11, 1),
(101, 10, 3);
El ejemplo utiliza pocos registros para simplificar la lectura. En escenarios reales con miles o millones de filas, este mismo error genera desvíos mucho más significativos.
SELECT * FROM clientes LIMIT 10;
SELECT * FROM ventas LIMIT 10;
SELECT * FROM productos LIMIT 10;
SELECT * FROM ventas_detalle LIMIT 10;
SELECT
c.provincia,
SUM(p.precio) AS facturacion
FROM ventas v
JOIN clientes c ON v.cliente_id = c.cliente_id
JOIN productos p ON 1 = 1
GROUP BY c.provincia;
Total facturación 11.400
Garantizar que la suma de los reportes parciales (total por provincia) no supere ni contradiga el total general.
"La suma de las partes nunca puede ser mayor que el todo."
-- Auditoria
SELECT
SUM(vd.cantidad * p.precio) AS total_real
FROM ventas_detalle vd
JOIN productos p ON vd.producto_id = p.producto_id;
-- Auditoria
SELECT
SUM(facturacion) AS total_provincias
FROM (
SELECT
c.provincia,
SUM(vd.cantidad * p.precio) AS facturacion
FROM ventas v
JOIN clientes c ON v.cliente_id = c.cliente_id
JOIN ventas_detalle vd ON v.venta_id = vd.venta_id
JOIN productos p ON vd.producto_id = p.producto_id
GROUP BY c.provincia
) t;
La auditoría NO replica la query de producción.
La auditoría calcula el negocio correctamente para validar si el reporte en producción es confiable.
Por lo tanto:
- Está bien que los totales auditados den iguales.
- Eso no invalida la auditoría.
- Justamente demuestra que la query de producción está mal.
total_producción > total_real
Donde total_produccion es el total obtenido a partir de la query actualmente en producción.
Si el total obtenido por la query en producción supera el total real calculado mediante auditoría, existe un uso incorrecto del modelo lógico.
en este ejemplo 11.400 > 10.500
SELECT
c.provincia,
SUM(p.precio) AS facturacion
FROM ventas v
JOIN clientes c ON v.cliente_id = c.cliente_id
JOIN productos p ON 1 = 1
GROUP BY c.provincia;
La query genera un producto cartesiano porque el JOIN a productos se hace con ON 1 = 1, ignorando la relación real entre ventas y productos. Esto provoca que se sumen productos que no fueron vendidos. El error no es técnico, es lógico: el modelo existe, pero está siendo mal utilizado.
La query con ON 1 = 1 es un error grave, porque salta la tabla intermedia.
Además, la query ignora la tabla de hechos (ventas_detalle), perdiendo la granularidad real del negocio.
SELECT
c.provincia,
SUM(vd.cantidad * p.precio) AS facturacion
FROM ventas v
JOIN clientes c ON v.cliente_id = c.cliente_id
JOIN ventas_detalle vd ON v.venta_id = vd.venta_id
JOIN productos p ON vd.producto_id = p.producto_id
GROUP BY c.provincia;
Esta versión:
- Respeta el modelo lógico.
- Utiliza la tabla de hechos (ventas_detalle).
- Calcula la facturación a nivel de línea de venta.
- Garantiza que los totales por provincia sean consistentes con el total real del negocio.
Este ejercicio evalúa la capacidad de detectar errores lógicos en consultas SQL que, aun siendo sintácticamente correctas, producen resultados incorrectos por un uso inapropiado del modelo de datos.
Se presenta un caso realista donde una query de facturación utiliza un JOIN ON 1 = 1, generando un producto cartesiano y omitiendo la tabla intermedia que representa el hecho de negocio. El resultado es una inflación de métricas que no puede reconciliarse con la realidad del negocio.
La clave está en: Entender la granularidad del dato, respetar la tabla de hechos, y validar resultados mediante auditorías de control de totales.
El caso diferencia claramente entre:
- consultas que “funcionan” técnicamente.
- consultas que son correctas desde el punto de vista lógico y de negocio.
El análisis no se limita a escribir SQL válido, sino que implica RAZONAR sobre el modelo de datos, DETECTAR inconsistencias y VALIDAR que los resultados tengan sentido desde el punto de vista del negocio.