← Volver a la serie

MANUAL 02

Teradata

Carga masiva, consultas analíticas y un dataset realista.

Capítulo 1: Arquitectura de Teradata

1.1 ¿Qué es Teradata?

Teradata es un sistema de gestión de bases de datos relacionales (RDBMS) diseñado específicamente para el procesamiento analítico de grandes volúmenes de datos (OLAP). Su arquitectura de procesamiento masivamente paralelo (MPP) lo distingue de otros motores de bases de datos y lo convierte en la solución preferida para entornos de Data Warehouse empresariales.

1.2 Componentes Principales

ComponenteDescripciónRol
Parsing Engine (PE)Motor de análisis sintáctico y semánticoRecibe las consultas SQL del usuario
BYNETRed de interconexión interna de alta velocidadComunica PEs con AMPs
Access Module Processor (AMP)Procesador de acceso a datosLee/escribe datos en los discos
Virtual Disk (vDisk)Almacenamiento virtual asignado a cada AMPAlmacena los datos físicamente
CliqueGrupo de nodos que comparten discosGarantiza alta disponibilidad

1.3 Distribución de Datos: Hash de Fila

Teradata distribuye las filas entre los AMPs usando un algoritmo de hash aplicado al Índice Primario (PI) de cada tabla. Este mecanismo es fundamental para entender la performance del sistema.

Flujo de distribución de una fila INSERT

Flujo de distribución de una fila INSERT

  1. Usuario ejecuta: INSERT INTO ventas VALUES (101, ‘Buenos Aires’, 50000)
  2. El PE calcula: HASH( valor_PI = 101 ) → 0xA3F7…
  3. El BYNET enruta: hash % N_AMPs → AMP #7
  4. AMP #7 escribe: la fila en su vDisk

1.4 Índice Primario (PI)

El Índice Primario es la columna o combinación de columnas que determina en qué AMP se almacenará cada fila. Su elección es la decisión de diseño más importante en Teradata.

Tipo de PICaracterísticaCuándo Usarlo
UPI – Unique Primary IndexValores únicos por filaClaves de negocio únicas (ID de cliente)
NUPI – Non-Unique PIPermite duplicadosColumnas de alta cardinalidad, pero no únicas
No PI (NoPI Table)Sin índice primarioTablas de staging para carga masiva
⚠ Importante

Un PI mal elegido puede concentrar datos en pocos AMPs (skew), degradando drásticamente el rendimiento. Verificar siempre con HELP STATS y las vistas de sistema.

Capítulo 2: Tipos de Datos en Teradata

2.1 Tipos de Datos Numéricos

TipoTamañoRango / PrecisiónUso Típico
BYTEINT1 byte-128 a 127Flags, indicadores pequeños
SMALLINT2 bytes-32,768 a 32,767Códigos de estado
INTEGER / INT4 bytes-2.1B a 2.1BIDs, contadores
BIGINT8 bytes±9.2 × 10^18IDs de transacciones masivas
DECIMAL(p,s)variableHasta 38 dígitosMontos, precios (evita redondeo)
FLOAT / REAL8 bytesIEEE 754 doble precisiónCálculos científicos
NUMBER(p,s)variableCompatibilidad ANSIMigración desde Oracle

2.2 Tipos de Datos de Caracteres

TipoDescripciónMáximoNotas
CHAR(n)Longitud fija, rellena con espacios64,000 bytesPara códigos de longitud fija
VARCHAR(n)Longitud variable64,000 bytesTexto de longitud variable
CLOBCharacter Large Object2 GBDocumentos, texto largo
CHAR VARYINGSinónimo de VARCHAR-Compatibilidad ANSI

2.3 Tipos de Datos de Fecha y Hora

TipoFormato InternoEjemploNotas
DATEINTEGER (YYYYMMDD)2025-06-15Almacenado como entero, muy eficiente
TIMEHH:MI:SS.nnnnnn14:30:00.000000Con o sin zona horaria
TIMESTAMPDATE + TIME2025-06-15 14:30:00Estándar para auditorías
INTERVALDuración relativaINTERVAL ‘3’ MONTHPara cálculos de períodos

Ejemplo: Creación de tabla con tipos de datos correctos

CREATE TABLE clientes (
cliente_id INTEGER NOT NULL,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(200),
fecha_alta DATE FORMAT 'YYYY-MM-DD',
saldo DECIMAL(15,2) DEFAULT 0,
activo BYTEINT DEFAULT 1
)
PRIMARY INDEX (cliente_id);

Capítulo 3: SQL en Teradata – Comandos Esenciales

3.1 DDL – Data Definition Language

Los comandos DDL permiten crear, modificar y eliminar objetos de la base de datos.

CREATE TABLE – Sintaxis completa con opciones Teradata

CREATE [SET | MULTISET] TABLE nombre_tabla,
[NO] FALLBACK [PROTECTION],
[NO] BEFORE JOURNAL,
[NO] AFTER JOURNAL,
CHECKSUM = DEFAULT
(
col1 INTEGER NOT NULL,
col2 VARCHAR(100),
col3 DATE FORMAT 'YYYY-MM-DD'
)
PRIMARY INDEX nombre_pi (col1)
[PARTITION BY RANGE_N(col3 BETWEEN DATE '2020-01-01' AND DATE '2025-12-31' EACH INTERVAL '1' MONTH)];
OpciónSETMULTISET
Filas duplicadasNo permite (descarta silenciosamente)Permite duplicados
Comportamiento PIÚnico por defecto lógicoNo unicidad implícita
Uso recomendadoTablas de hechos con PI únicoStaging, tablas temporales

3.2 DML – Data Manipulation Language

INSERT / INSERT-SELECT

-- Inserción simple
INSERT INTO ventas (venta_id, cliente_id, monto, fecha)
VALUES (1001, 500, 15000.00, DATE '2025-06-15');

-- Insert-Select (ETL típico)
INSERT INTO ventas_historico
SELECT * FROM ventas_staging
WHERE fecha < DATE - 365;

UPDATE y DELETE con condiciones

-- UPDATE con subconsulta correlacionada
UPDATE v
FROM ventas v
SET monto = monto * 1.10
WHERE EXISTS (
SELECT 1 FROM clientes_premium cp
WHERE cp.cliente_id = v.cliente_id
);

-- DELETE con JOIN
DELETE FROM ventas_staging
WHERE (venta_id, fecha) IN (
SELECT venta_id, fecha FROM ventas
);

3.3 Opciones UPSERT: MERGE INTO

MERGE INTO – Insertar o actualizar en una sola instrucción

MERGE INTO clientes AS tgt
USING clientes_staging AS src
ON tgt.cliente_id = src.cliente_id
WHEN MATCHED THEN
UPDATE SET nombre = src.nombre,
email = src.email
WHEN NOT MATCHED THEN
INSERT (cliente_id, nombre, email)
VALUES (src.cliente_id, src.nombre, src.email);

Capítulo 4: Funciones Analíticas (OLAP)

Las funciones analíticas (también llamadas funciones de ventana) son una de las características más poderosas y diferenciales de Teradata. Permiten calcular valores acumulados, rankings, comparaciones con filas anteriores y posteriores, todo dentro de una sola consulta SQL sin subconsultas complejas.

4.1 Sintaxis General de Funciones de Ventana

Estructura OVER() – clave de las funciones analíticas

Estructura OVER() – clave de las funciones analíticas FUNCIÓN_ANALÍTICA(columna)

OVER ( [PARTITION BY col_agrupación] [ORDER BY col_orden ASC|DESC] [ROWS|RANGE BETWEEN inicio AND fin] )

4.2 Funciones de Ranking

FunciónDescripciónManejo de Empates
RANK()Ranking con saltos ante empateEmpates comparten rango, el siguiente salta
DENSE_RANK()Ranking sin saltosEmpates comparten rango, el siguiente es consecutivo
ROW_NUMBER()Número de fila únicoSin empates, siempre único
PERCENT_RANK()Percentil del ranking(rank-1)/(total_filas-1)
CUME_DIST()Distribución acumuladaFracción de filas <= valor actual
NTILE(n)Divide en N grupos igualesDistribuye uniformemente

Ejemplo práctico – Ranking de ventas por región

SELECT
region,
vendedor,
monto_venta,
RANK() OVER (PARTITION BY region ORDER BY monto_venta DESC) AS rank_region,
DENSE_RANK() OVER (PARTITION BY region ORDER BY monto_venta DESC) AS drank_region,
ROW_NUMBER() OVER (PARTITION BY region ORDER BY monto_venta DESC) AS nro_fila,
NTILE(4) OVER (PARTITION BY region ORDER BY monto_venta DESC) AS cuartil
FROM ventas
WHERE fecha BETWEEN DATE '2025-01-01' AND DATE '2025-12-31';
✔ Tip

RANK y DENSE_RANK son iguales salvo en los saltos. Usar DENSE_RANK cuando se quiera numerar posiciones consecutivas (ej: podio 1°, 2°, 3°). Usar RANK cuando se quiera respetar el salto (ej: dos 2°, entonces el siguiente es 4°).

4.3 Funciones de Ventana Deslizante

SUM, AVG, MIN, MAX con ROWS BETWEEN

SELECT
fecha,
monto,
-- Suma acumulada desde el inicio hasta la fila actual
SUM(monto) OVER (ORDER BY fecha ROWS UNBOUNDED PRECEDING) AS suma_acum,

-- Media móvil de los últimos 7 días
AVG(monto) OVER (ORDER BY fecha ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS media_7d,

-- Máximo en ventana de 30 días
MAX(monto) OVER (ORDER BY fecha ROWS BETWEEN 29 PRECEDING AND CURRENT ROW) AS max_30d
FROM ventas_diarias
ORDER BY fecha;

4.4 Funciones LAG y LEAD – Comparar con Filas Adyacentes

LAG/LEAD – Variación respecto al período anterior/siguiente

SELECT
fecha,
monto,
-- Valor del período anterior (offset=1, default=0 si no existe)
LAG(monto, 1, 0) OVER (ORDER BY fecha) AS monto_anterior,

-- Valor del período siguiente
LEAD(monto, 1, 0) OVER (ORDER BY fecha) AS monto_siguiente,

-- Variación porcentual respecto al mes anterior
CAST((monto - LAG(monto,1) OVER (ORDER BY fecha))/ NULLIFZERO(LAG(monto,1) OVER (ORDER BY fecha))* 100 AS DECIMAL(10,2)) AS var_pct
FROM ventas_mensuales
ORDER BY fecha;

4.5 FIRST_VALUE y LAST_VALUE

Obtener el primer o último valor de una partición

SELECT
cliente_id,
fecha_compra,
monto,
-- Primera compra del cliente
FIRST_VALUE(fecha_compra) OVER (
PARTITION BY cliente_id
ORDER BY fecha_compra
ROWS UNBOUNDED PRECEDING
) AS primera_compra,

-- Monto de la última compra
LAST_VALUE(monto) OVER (
PARTITION BY cliente_id
ORDER BY fecha_compra
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS ultima_compra_monto
FROM compras
ORDER BY cliente_id, fecha_compra;

Capítulo 5: Funciones de Fecha y Hora

5.1 Operaciones con DATE

Función / ExpresiónEjemploResultado
DATE (fecha actual)SELECT DATE2025-06-15
DATE + nDATE + 30Fecha + 30 días
DATE - DATEDATE ‘2025-12-31’ - DATE ‘2025-01-01’364 (días entre fechas)
EXTRACT(parte FROM fecha)EXTRACT(YEAR FROM DATE ‘2025-06-15’)2025
ADD_MONTHS(fecha, n)ADD_MONTHS(DATE, 3)Suma 3 meses exactos
LAST_DAY(fecha)LAST_DAY(DATE ‘2025-06-01’)2025-06-30
NEXT_DAY(fecha, dia)NEXT_DAY(DATE, ‘Monday’)Próximo lunes
FORMAT fecha (YYYY-MM-DD)(DATE)(FORMAT ‘YYYY-MM-DD’)Convierte a string formateado

Ejemplo completo – Análisis temporal de ventas

SELECT
EXTRACT(YEAR FROM fecha) AS anio,
EXTRACT(MONTH FROM fecha) AS mes,
EXTRACT(DAY FROM fecha) AS dia,
-- Día de la semana (1=Domingo ... 7=Sábado en TD)
TD_DAY_OF_WEEK(fecha) AS dia_semana,
-- Inicio y fin del mes
fecha - (EXTRACT(DAY FROM fecha) - 1) AS primer_dia_mes,
LAST_DAY(fecha) AS ultimo_dia_mes,
-- Diferencia en meses con respecto a hoy
MONTHS_BETWEEN(DATE, fecha) AS meses_transcurridos,
COUNT(*) AS total_ventas,
SUM(monto) AS total_monto
FROM ventas
GROUP BY 1,2,3,4,5,6,7
ORDER BY 1,2,3;

5.2 TIMESTAMP y Zonas Horarias

Manejo de TIMESTAMP con zona horaria

-- TIMESTAMP actual del sistema
SELECT CURRENT_TIMESTAMP;
SELECT CURRENT_TIMESTAMP(6); -- Con microsegundos

-- Conversión entre zonas horarias
SELECT CURRENT_TIMESTAMP AT 'America/Buenos_Aires';
SELECT CURRENT_TIMESTAMP AT TIME ZONE INTERVAL '-03:00' HOUR TO MINUTE;

-- Diferencia entre TIMESTAMPS (resultado en días)
SELECT CAST(
(CAST('2025-12-31 23:59:59' AS TIMESTAMP) - CAST('2025-01-01 00:00:00' AS TIMESTAMP)
) SECOND(4) AS DECIMAL(15,2)) / 86400 AS dias_diferencia;

5.3 Funciones TD_SYSFNLIB Específicas de Teradata

FunciónDescripciónEjemplo de Uso
TD_DAY_OF_WEEK(d)Día de la semana (0=Dom)TD_DAY_OF_WEEK(fecha) = 0 (Domingo)
TD_DAY_OF_YEAR(d)Día del año (1-366)TD_DAY_OF_YEAR(DATE ‘2025-06-15’) = 166
TD_WEEK_OF_YEAR(d)Semana ISO del añoPara análisis semanal
TD_FISCAL_QUARTER(d,m)Trimestre fiscalTD_FISCAL_QUARTER(fecha, 4) – año fiscal desde abril
TD_SYSDATETIME()Fecha-hora con precisión de nsPara logs de auditoría

Capítulo 6: Funciones de String y Conversión

6.1 Funciones de Manipulación de Texto

FunciónSintaxisEjemplo
TRIMTRIM([LEADING|TRAILING|BOTH] char FROM expr)TRIM(BOTH ’ ’ FROM ’ Hola ’) → ‘Hola’
SUBSTR / SUBSTRINGSUBSTR(str, inicio, largo)SUBSTR(‘Teradata’,1,4) → ‘Tera’
CHARACTERS / CHAR_LENGTHCHARACTERS(str)CHARACTERS(‘hola’) → 4
INDEX / INSTRINDEX(str, buscar)INDEX(‘Buenos Aires’,‘Aires’) → 8
UPPER / LOWERUPPER(str)UPPER(‘teradata’) → ‘TERADATA’
CONCAT / ||str1 || str2’Tera’ || ‘data’ → ‘Teradata’
TRANSLATETRANSLATE(str USING LATIN_TO_UNICODE)Conversión de juego de caracteres
REGEXP_SUBSTRREGEXP_SUBSTR(str, patrón)Extrae con expresión regular
REGEXP_REPLACEREGEXP_REPLACE(str, pat, reemplazo)Reemplaza con regex
STRTOKSTRTOK(str, delim, n)STRTOK(‘a;b;c’,’;‘,2) → ‘b’
OREPLACEOREPLACE(str, buscar, reempl)OREPLACE(‘Hola Mundo’,‘Mundo’,‘TD’) → ‘Hola TD’

Ejemplo práctico – Limpieza y parsing de datos de texto

SELECT
-- Normalizar nombre
TRIM(UPPER(nombre)) AS nombre_limpio,
-- Extraer dominio del email
SUBSTR(email, INDEX(email,'@') + 1) AS dominio,
-- Separar código de área de teléfono '(011) 4567-8901'
STRTOK(OREPLACE(OREPLACE(telefono,'(',''),')',''), ' ', 1) AS cod_area,
-- Enmascarar email
SUBSTR(email,1,3) || '***' ||
SUBSTR(email, INDEX(email,'@')) AS email_mascara,
-- Validar formato con regex
CASE WHEN REGEXP_SIMILAR(email,'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\[A-Za-z]{2,}$') = 1
THEN 'VÁLIDO' ELSE 'INVÁLIDO' END AS email_estado
FROM clientes;

6.2 Conversiones de Tipo: CAST y TO_CHAR / TO_DATE

CAST – conversión explícita de tipos

-- Numérico a texto
CAST(12345.67 AS VARCHAR(20)) → '12345.67'

-- Texto a fecha
CAST('2025-06-15' AS DATE FORMAT 'YYYY-MM-DD')

-- Texto a número
CAST('9876.54' AS DECIMAL(10,2)) → 9876.54

-- TIMESTAMP a DATE
CAST(CURRENT_TIMESTAMP AS DATE) → fecha actual

-- Con formato de salida
CAST(monto AS VARCHAR(20)) (FORMAT '$$$$,$$$,$$9.99')
✔ Teradata vs ANSI

Teradata acepta tanto CAST (ANSI estándar) como la sintaxis abreviada: (columna)(TIPO). Ejemplo: (fecha)(FORMAT ‘YYYYMMDD’). En ambientes mixtos preferir CAST para portabilidad.

Capítulo 7: Optimización y Performance

7.1 EXPLAIN – El Plan de Ejecución

El comando EXPLAIN muestra cómo el Optimizador de Teradata planifica ejecutar una consulta. Es la herramienta más importante para diagnosticar y optimizar consultas.

Cómo interpretar EXPLAIN

EXPLAIN
SELECT v.cliente_id, SUM(v.monto)
FROM ventas v
JOIN clientes c ON v.cliente_id = c.cliente_id
WHERE v.fecha >= DATE - 30
GROUP BY 1;

-- Términos clave en la salida del EXPLAIN:
-- 'We do an all-AMPs retrieve' → Full table scan (costoso)
-- 'We do a single-AMP retrieve' → Acceso por PI (ideal)
-- 'Redistributing rows' → Redistribución de datos entre AMPs
-- 'Duplicating rows' → Duplicación para JOIN (ineficiente)
-- 'Spool' → Archivo temporal en disco
-- 'Confidence level' → Confianza en estadísticas (high=bueno)

7.2 Estadísticas – COLLECT STATISTICS

El Optimizador de Teradata depende de estadísticas actualizadas para generar buenos planes de ejecución. Sin estadísticas, el Optimizador usa estimaciones que pueden ser muy incorrectas.

Comandos de estadísticas

-- Recolectar estadísticas de una columna individual
COLLECT STATISTICS ON ventas COLUMN (cliente_id);

-- Estadísticas de índice primario
COLLECT STATISTICS ON ventas INDEX (venta_id);

-- Estadísticas de múltiples columnas (histograma conjunto)
COLLECT STATISTICS ON ventas COLUMN (fecha, region);

-- Ver estadísticas existentes
HELP STATS ventas;

-- Ver detalle de una estadística
SHOW STATS VALUES ON ventas;

-- Actualizar solo si han cambiado > umbral (recomendado en producción)
COLLECT STATISTICS USING THRESHOLD 10 PERCENT ON ventas COLUMN (fecha);

7.3 Estrategias para Evitar el Skew

ProblemaCausaSolución
Skew de PIPI con baja cardinalidad o nulosElegir un PI con alta cardinalidad y sin nulos
Producto CartesianoJOIN sin condición o con constanteVerificar ON clause, usar EXPLAIN
Spool excesivoSELECT * sin filtros en tablas grandesAplicar filtros tempranos, usar vistas
Redistribución en JOINsColumnas de JOIN distintas al PICrear PI/SI en columnas de JOIN frecuentes
Lock contentionMuchas transacciones en las mismas filasUsar ACCESS LOCK en consultas analíticas

7.4 Índices Secundarios y PPI

Tipos de índices adicionales

-- Índice Secundario Único (USI) – como un UPI alternativo
CREATE UNIQUE INDEX (email) ON clientes;

-- Índice Secundario No Único (NUSI) – para columnas de filtro frecuente
CREATE INDEX (region, fecha) ON ventas;

-- Partition Primary Index (PPI) – particionamiento por rango de fecha
CREATE TABLE ventas_ppi (
venta_id INTEGER NOT NULL,
fecha DATE NOT NULL,
monto DECIMAL(15,2)
)
PRIMARY INDEX (venta_id)
PARTITION BY RANGE_N(
fecha BETWEEN DATE '2020-01-01' AND DATE '2025-12-31'
EACH INTERVAL '1' MONTH
);

ℹ Info: Con PPI, Teradata puede eliminar particiones completas al aplicar filtros de fecha (partition elimination), reduciendo dramáticamente el I/O en tablas históricas grandes.

Capítulo 8: Ejercicios Prácticos Resueltos

Los siguientes ejercicios están basados en un modelo de datos de ventas. Se asume la existencia de las tablas: ventas, clientes, productos, regiones.

Ejercicio 1 – Top 3 Productos por Región (Ventanas)

Objetivo: Obtener los 3 productos con mayor volumen de ventas por cada región, mostrando también el porcentaje que representan del total regional.

Solución con RANK y porcentaje regional

WITH ventas_regionales AS (
SELECT
r.nombre_region,
p.nombre_producto,
SUM(v.monto) AS total_venta,
COUNT(*) AS cant_transacciones
FROM ventas v
JOIN clientes c ON v.cliente_id = c.cliente_id
JOIN regiones r ON c.region_id = r.region_id
JOIN productos p ON v.producto_id = p.producto_id
WHERE v.fecha BETWEEN DATE '2025-01-01' AND DATE '2025-12-31'
GROUP BY 1, 2
),
ranking AS (
SELECT
nombre_region,
nombre_producto,
total_venta,
cant_transacciones,
RANK() OVER (PARTITION BY nombre_region ORDER BY total_venta DESC) AS rank_prod,
SUM(total_venta) OVER (PARTITION BY nombre_region) AS total_region,
CAST(total_venta * 100.0 / SUM(total_venta) OVER (PARTITION BY nombre_region)
AS DECIMAL(5,2)) AS pct_region
FROM ventas_regionales
)
SELECT * FROM ranking
WHERE rank_prod <= 3
ORDER BY nombre_region, rank_prod;

Ejercicio 2 – Detección de Gaps en Series Temporales

Objetivo: Identificar días sin ventas (gaps) en una tabla de ventas diarias, útil para auditorías y controles de calidad de datos.

Solución con LAG y generación de calendario

WITH serie_ventas AS (
SELECT
fecha,
SUM(monto) AS total_dia,
LAG(fecha, 1) OVER (ORDER BY fecha) AS fecha_anterior
FROM ventas
WHERE fecha BETWEEN DATE '2025-01-01' AND DATE '2025-12-31'
GROUP BY fecha
)
SELECT
fecha_anterior + 1 AS gap_inicio,
fecha - 1 AS gap_fin,
(fecha - fecha_anterior - 1) AS dias_sin_ventas
FROM serie_ventas
WHERE (fecha - fecha_anterior) > 1 -- Hay un gap
ORDER BY gap_inicio;

Ejercicio 3 – Cohorte de Clientes por Mes de Primera Compra

Objetivo: Analizar la retención de clientes agrupándolos por el mes de su primera compra (cohorte) y medir cuántos siguen comprando en meses posteriores.

Análisis de cohortes con FIRST_VALUE y GROUP BY

WITH primera_compra AS (
SELECT
cliente_id,
CAST(FIRST_VALUE(fecha) OVER (
PARTITION BY cliente_id
ORDER BY fecha
ROWS UNBOUNDED PRECEDING
) AS CHAR(7)) AS cohorte_mes -- 'YYYY-MM'
FROM ventas
),
actividad AS (
SELECT
v.cliente_id,
CAST(v.fecha AS CHAR(7)) AS mes_actividad,
pc.cohorte_mes
FROM ventas v
JOIN primera_compra pc ON v.cliente_id = pc.cliente_id
GROUP BY 1, 2, 3
)
SELECT
cohorte_mes,
mes_actividad,
COUNT(DISTINCT cliente_id) AS clientes_activos,
-- Diferencia en meses desde el inicio de la cohorte
(CAST(mes_actividad AS DATE FORMAT 'YYYY-MM') -
CAST(cohorte_mes AS DATE FORMAT 'YYYY-MM')) MONTH AS mes_numero
FROM actividad
GROUP BY 1, 2
ORDER BY 1, 4;

Ejercicio 4 – Cálculo de Percentiles con QUANTILE

QUANTILE – función exclusiva de Teradata

-- QUANTILE divide el resultado en N grupos de igual tamaño
-- Similar a NTILE pero con semántica estadística más precisa
SELECT
cliente_id,
total_compras,
-- Deciles (0 al 9)
QUANTILE(10, total_compras OVER ()) AS decil,
-- Percentil 25, 50, 75 (cuartiles)
QUANTILE(100, total_compras OVER ()) AS percentil
FROM (
SELECT cliente_id, SUM(monto) AS total_compras
FROM ventas
GROUP BY cliente_id
) sub
ORDER BY decil DESC, total_compras DESC;

Capítulo 9: Vistas, Macros y Procedimientos Almacenados

9.1 Vistas (VIEWS)

CREATE VIEW – abstracción de consultas complejas

-- Vista simple de clientes activos
REPLACE VIEW vw_clientes_activos AS
SELECT
c.cliente_id,
c.nombre,
c.email,
MAX(v.fecha) AS ultima_compra,
SUM(v.monto) AS total_historico
FROM clientes c
JOIN ventas v ON c.cliente_id = v.cliente_id
WHERE c.activo = 1
GROUP BY 1,2,3;

-- Uso de la vista
SELECT * FROM vw_clientes_activos
WHERE ultima_compra >= DATE - 90;

9.2 Macros

Las macros en Teradata son como procedimientos parametrizados pero más simples. Se compilan una vez y se ejecutan con EXEC. Son ideales para consultas recurrentes con parámetros.

CREATE MACRO con parámetros

REPLACE MACRO mac_ventas_cliente (
p_cliente_id INTEGER,
p_fecha_desde DATE,
p_fecha_hasta DATE
) AS (
SELECT
fecha,
producto_id,
monto
FROM ventas
WHERE cliente_id = :p_cliente_id
AND fecha BETWEEN :p_fecha_desde AND :p_fecha_hasta
ORDER BY fecha;
);

-- Ejecución
EXEC mac_ventas_cliente (500, DATE '2025-01-01', DATE '2025-06-30');

9.3 Procedimientos Almacenados (Stored Procedures)

CREATE PROCEDURE con lógica condicional y cursores

REPLACE PROCEDURE sp_actualizar_segmento (
IN p_fecha_corte DATE,
OUT p_total_actualizados INTEGER
)
BEGIN
DECLARE v_cliente_id INTEGER;
DECLARE v_total DECIMAL(15,2);
DECLARE SQLSTATE VARCHAR(6) DEFAULT '00000';

-- Cursor para iterar clientes
DECLARE cur_clientes CURSOR FOR
SELECT cliente_id, SUM(monto) AS total_anio
FROM ventas
WHERE fecha BETWEEN p_fecha_corte - 365 AND p_fecha_corte
GROUP BY cliente_id;

SET p_total_actualizados = 0;

OPEN cur_clientes;
FETCH cur_clientes INTO v_cliente_id, v_total;

WHILE SQLSTATE = '00000' DO
UPDATE clientes SET
segmento = CASE
WHEN v_total >= 100000 THEN 'PLATINUM'
WHEN v_total >= 50000 THEN 'GOLD'
WHEN v_total >= 10000 THEN 'SILVER'
ELSE 'STANDARD'
END
WHERE cliente_id = v_cliente_id;

SET p_total_actualizados = p_total_actualizados + 1;
FETCH cur_clientes INTO v_cliente_id, v_total;
END WHILE;

CLOSE cur_clientes;
END;

Capítulo 10: Referencia Rápida y Diferencias con Otros Motores

10.1 Diferencias Clave: Teradata vs SQL Estándar / Oracle / SQL Server

ConceptoTeradataOracle / SQL Server
Fecha actualDATESYSDATE / GETDATE()
Auto-incrementoNo nativo – usar IDENTITY o secuenciasSEQUENCE / IDENTITY
Top N filasSAMPLE n o TOP n (BTEQ)ROWNUM / TOP n / FETCH FIRST
Concatenar strings|| o CONCAT()+ (SQL Server) / || (Oracle)
Manejo de nulos en aritméticaNULL propaga / NULLIFZERO()NVL / ISNULL
Tabla dualNo existe – SELECT 1 directamenteDUAL (Oracle)
UPSERTMERGE INTO … WHEN MATCHEDMERGE INTO (similar)
Tabla temporal de sesiónCREATE VOLATILE TABLECREATE #temp (SQL Server)
Índice de acceso rápidoÍndice Primario (PI) distribuidoClustered Index / IOT

10.2 Comandos de Administración Frecuentes

Comandos de utilidad en el día a día

-- Ver estructura de una tabla
SHOW TABLE nombre_tabla;
HELP TABLE nombre_tabla;
HELP COLUMN nombre_tabla.*;

-- Ver espacio utilizado
SELECT * FROM DBC.TableSizeV WHERE DatabaseName = 'mi_base';

-- Ver sesiones activas
SELECT * FROM DBC.SessionInfoV ORDER BY SpoolUsage DESC;

-- Ver locks activos
SELECT * FROM DBC.LockLogV;

-- Ver objetos de una base de datos
SELECT TableName, TableKind FROM DBC.TablesV
WHERE DatabaseName = 'mi_base' ORDER BY TableKind, TableName;

-- Ver dependencias de una vista
SELECT * FROM DBC.ObjectUsageV WHERE ObjectDatabaseName = 'mi_base';

10.3 Tablas Volátiles y Tablas Derivadas (CTEs)

Tabla Volátil vs CTE vs Tabla Derivada

-- 1. TABLA VOLÁTIL: persiste durante la sesión, con estadísticas
CREATE VOLATILE TABLE tmp_resumen AS (
SELECT cliente_id, SUM(monto) AS total
FROM ventas WHERE fecha >= DATE - 90
GROUP BY cliente_id
) WITH DATA PRIMARY INDEX (cliente_id) ON COMMIT PRESERVE ROWS;
COLLECT STATISTICS ON tmp_resumen COLUMN (cliente_id);

-- 2. CTE (WITH): más legible, no materializada físicamente
WITH resumen AS (
SELECT cliente_id, SUM(monto) AS total
FROM ventas WHERE fecha >= DATE - 90
GROUP BY cliente_id
)
SELECT c.nombre, r.total FROM resumen r JOIN clientes c ON r.cliente_id = c.cliente_id;

-- 3. TABLA DERIVADA: subconsulta en el FROM
SELECT c.nombre, r.total
FROM clientes c
JOIN (SELECT cliente_id, SUM(monto) AS total FROM ventas GROUP BY 1) r
ON c.cliente_id = r.cliente_id;

⚠ Performance: Las tablas volátiles son ideales cuando la subconsulta se reutiliza múltiples veces o cuando el optimizador no la resuelve bien como CTE. Siempre recolectar estadísticas sobre las columnas de JOIN de las tablas volátiles.

Capítulo 11: FastLoad y MultiLoad – Carga Masiva de Datos

Teradata ofrece utilidades de carga masiva diseñadas para superar las limitaciones del SQL convencional cuando se trabaja con millones de registros. FastLoad y MultiLoad son las herramientas fundamentales del ecosistema ETL de Teradata.

11.1 Comparativa: FastLoad vs MultiLoad vs BTEQ

CaracterísticaFastLoadMultiLoadBTEQ (INSERT)
Velocidad de cargaMáxima (todos los AMPs)Muy altaLenta (secuencial)
Tabla destinoDebe estar vacíaCon o sin datosCon o sin datos
Operaciones soportadasSolo INSERTINSERT/UPDATE/DELETE/UPSERTTodas las DML
Acceso durante cargaTabla NO disponibleTabla NO disponibleTabla disponible
Tabla de errores automáticaSí (2 tablas)Sí (2 tablas)No
Rollback automáticoSí (con restart)Sí (con checkpoint)No por defecto
Volumen ideal> 100.000 filas> 50.000 filas mixtas< 10.000 filas
ParalelismoTotal: todos los AMPsTotal: todos los AMPsSecuencial
Usa SpoolNoWork tables internasSí
Regla clave

FastLoad exige tabla VACÍA y sin índices secundarios activos durante la carga. MultiLoad tolera datos existentes pero no permite acceso concurrente durante el proceso.

11.2 FastLoad – Carga Inicial Masiva

FastLoad opera en dos fases: Acquisition Phase (distribuye datos a los AMPs) y Application Phase (escribe en los discos). Puede cargar millones de filas en minutos sobre una tabla vacía.

Paso 1 – Preparación del entorno

Crear tablas de destino y limpiar errores previos

-- Tabla destino (debe estar vacía)
CREATE MULTISET TABLE ecomm.pedidos_stg,
NO FALLBACK, NO BEFORE JOURNAL, NO AFTER JOURNAL
(
pedido_id INTEGER NOT NULL,
cliente_id INTEGER NOT NULL,
producto_id INTEGER NOT NULL,
fecha_pedido DATE FORMAT 'YYYY-MM-DD',
cantidad SMALLINT,
precio_unitario DECIMAL(10,2),
monto_total DECIMAL(12,2),
estado VARCHAR(20),
canal VARCHAR(30),
region VARCHAR(50)
)
PRIMARY INDEX (pedido_id);

-- Limpiar tablas de error previas si existieran
DROP TABLE ecomm.pedidos_stg_ET; -- Error Table 1: errores de conversión
DROP TABLE ecomm.pedidos_stg_UV; -- Error Table 2: violaciones de unicidad

Paso 2 – Script FastLoad completo

fastload_pedidos.fl – Script de carga 1.000.000 filas

/*================================================
FastLoad: Carga inicial de PEDIDOS
Archivo: pedidos_1M.csv (1.000.000 filas)
Destino: ecomm.pedidos_stg
================================================*/

SESSIONS 8; /* Un thread por AMP/CPU aproximadamente */
ERRLIMIT 100; /* Abortar si supera 100 errores */

LOGON tdserver/usuario,contraseña;

BEGIN LOADING ecomm.pedidos_stg
ERRORFILES ecomm.pedidos_stg_ET, ecomm.pedidos_stg_UV
CHECKPOINT 50000; /* Restart cada 50.000 filas */

DEFINE
pedido_id (CHAR(10)),
cliente_id (CHAR(10)),
producto_id (CHAR(10)),
fecha_pedido (CHAR(10)),
cantidad (CHAR(5)),
precio_unitario (CHAR(12)),
monto_total (CHAR(14)),
estado (CHAR(20)),
canal (CHAR(30)),
region (CHAR(50))
FILE = '/data/pedidos_1M.csv'
SKIP 1; /* Saltar línea de encabezado */

INSERT INTO ecomm.pedidos_stg (
pedido_id, cliente_id, producto_id,
fecha_pedido, cantidad, precio_unitario,
monto_total, estado, canal, region
)
VALUES (
:pedido_id (INTEGER),
:cliente_id (INTEGER),
:producto_id (INTEGER),
:fecha_pedido (DATE, FORMAT 'YYYY-MM-DD'),
:cantidad (SMALLINT),
:precio_unitario (DECIMAL(10,2)),
:monto_total (DECIMAL(12,2)),
:estado,
:canal,
:region
);

END LOADING;

SELECT COUNT(*) FROM ecomm.pedidos_stg;
SELECT COUNT(*) FROM ecomm.pedidos_stg_ET;
SELECT COUNT(*) FROM ecomm.pedidos_stg_UV;

LOGOFF;

Paso 3 – Monitoreo y troubleshooting

Diagnóstico de una carga FastLoad activa o fallida

-- Ver sesiones activas de utilidades de carga
SELECT UserName, ClientAddr, Workload
FROM DBC.SessionInfoV
WHERE Workload = 'FASTLOAD';

-- Errores más frecuentes
SELECT ErrorCode, TRIM(ErrorMsg) AS descripcion, COUNT(*) AS cant
FROM ecomm.pedidos_stg_ET
GROUP BY 1, 2
ORDER BY 3 DESC;

-- Ver una fila con error para diagnóstico
SELECT * FROM ecomm.pedidos_stg_ET
WHERE ErrorCode = 2679 /* 2679 = error de conversión de fecha */
SAMPLE 5;

11.3 MultiLoad – Carga Incremental con Upsert y Delete

MultiLoad permite mezclar INSERT, UPDATE, DELETE y UPSERT en una misma ejecución sobre tablas que ya contienen datos. Es ideal para cargas incrementales diarias y sincronización de datos.

multiload_pedidos_delta.ml – Delta diario con UPSERT y DELETE

/*================================================
MultiLoad: Delta diario de PEDIDOS
Lógica: UPSERT nuevos/modificados + DELETE cancelados
Archivo: pedidos_delta_YYYYMMDD.csv
Campo tipo_operacion: I=Insert, U=Update, D=Delete
================================================*/

SESSIONS 4;
ERRLIMIT 50;

LOGON tdserver/usuario,contraseña;

.BEGIN MLOAD TABLES ecomm.pedidos
WORKTABLES ecomm.pedidos_wt1, ecomm.pedidos_wt2
ERRORTABLES ecomm.pedidos_ET1, ecomm.pedidos_ET2
CHECKPOINT 25000;

.LAYOUT delta_pedidos;
.FIELD pedido_id * CHAR(10);
.FIELD cliente_id * CHAR(10);
.FIELD producto_id * CHAR(10);
.FIELD fecha_pedido * CHAR(10);
.FIELD cantidad * CHAR(5);
.FIELD precio_unitario * CHAR(12);
.FIELD monto_total * CHAR(14);
.FIELD estado * CHAR(20);
.FIELD canal * CHAR(30);
.FIELD region * CHAR(50);
.FIELD tipo_op * CHAR(1);

.DML LABEL upsert_pedidos IGNORE DUPLICATE ROWS;

UPDATE ecomm.pedidos SET
cantidad = :cantidad (SMALLINT),
precio_unitario = :precio_unitario (DECIMAL(10,2)),
monto_total = :monto_total (DECIMAL(12,2)),
estado = :estado
WHERE pedido_id = :pedido_id (INTEGER)
AND :tipo_op = 'U';

INSERT INTO ecomm.pedidos
SELECT :pedido_id (INTEGER), :cliente_id (INTEGER),
:producto_id (INTEGER),
:fecha_pedido (DATE, FORMAT 'YYYY-MM-DD'),
:cantidad (SMALLINT), :precio_unitario (DECIMAL(10,2)),
:monto_total (DECIMAL(12,2)), :estado, :canal, :region
WHERE :tipo_op = 'I';

.DML LABEL delete_pedidos;
DELETE FROM ecomm.pedidos
WHERE pedido_id = :pedido_id (INTEGER)
AND :tipo_op = 'D';

.IMPORT INFILE /data/pedidos_delta.csv
LAYOUT delta_pedidos
APPLY upsert_pedidos WHERE tipo_op IN ('I','U')
APPLY delete_pedidos WHERE tipo_op = 'D'
SKIP 1;

.END MLOAD;

SELECT COUNT(*) AS total_final FROM ecomm.pedidos;
SELECT COUNT(*) AS errores_ET1 FROM ecomm.pedidos_ET1;
SELECT COUNT(*) AS errores_ET2 FROM ecomm.pedidos_ET2;

LOGOFF;

11.4 TPT – Teradata Parallel Transporter (Estándar Moderno)

TPT unifica FastLoad, MultiLoad y otros operadores en un framework único. Es el estándar recomendado para nuevas implementaciones desde Teradata 13.x en adelante.

Script TPT equivalente a FastLoad

DEFINE JOB carga_pedidos_tpt
DESCRIPTION 'Carga masiva de pedidos via TPT'
(
DEFINE SCHEMA pedidos_schema
(
pedido_id VARCHAR(10),
cliente_id VARCHAR(10),
fecha_pedido VARCHAR(10),
monto_total VARCHAR(14),
estado VARCHAR(20)
);

DEFINE OPERATOR file_reader TYPE DATACONNECTOR PRODUCER
SCHEMA pedidos_schema
ATTRIBUTES
(
VARCHAR FileName = '/data/pedidos_1M.csv',
VARCHAR Format = 'Delimited',
VARCHAR TextDelimiter = ',',
VARCHAR SkipRows = '1'
);

DEFINE OPERATOR td_loader TYPE LOAD
SCHEMA pedidos_schema
ATTRIBUTES
(
VARCHAR TdpId = 'tdserver',
VARCHAR UserName = 'usuario',
VARCHAR UserPassword = 'contraseña',
VARCHAR TargetTable = 'ecomm.pedidos_stg',
VARCHAR ErrorTable1 = 'ecomm.pedidos_stg_ET',
VARCHAR ErrorTable2 = 'ecomm.pedidos_stg_UV',
INTEGER Sessions = 8
);

STEP cargar_datos
(
APPLY
('INSERT INTO ecomm.pedidos_stg VALUES (
:pedido_id (INTEGER), :cliente_id (INTEGER),
:fecha_pedido (DATE, FORMAT ''YYYY-MM-DD''),
:monto_total (DECIMAL(12,2)), :estado);'
)
TO OPERATOR (td_loader)
SELECT * FROM OPERATOR (file_reader);
);
);

Capítulo 12: Dataset E-Commerce – 1.000.000 Filas para Práctica

Este capítulo presenta el dataset completo de e-commerce para practicar todos los conceptos del manual. Incluye modelo de datos, scripts de generación sintética en Python, procedimiento de carga y 14 consultas analíticas de complejidad progresiva.

12.1 Modelo de Datos – Esquema Estrella

TablaTipoFilas aprox.Descripción
ecomm.clientesDimensión50.000Datos maestros de clientes con segmento y región
ecomm.productosDimensión5.000Catálogo con categoría, marca y precio base
ecomm.pedidosHecho1.000.000Tabla central: una fila por línea de pedido
ecomm.pagosHecho950.000Pagos de pedidos (excluye cancelados)
ecomm.devolucionesHecho80.000Devoluciones (~8% de los pedidos entregados)

12.2 DDL Completo – Creación de Tablas

ecomm.clientes – Dimensión principal

CREATE SET TABLE ecomm.clientes,
NO FALLBACK, NO BEFORE JOURNAL, NO AFTER JOURNAL
(
cliente_id INTEGER NOT NULL,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(150),
pais VARCHAR(50) DEFAULT 'Argentina',
region VARCHAR(50),
ciudad VARCHAR(80),
segmento VARCHAR(20), /* STANDARD|SILVER|GOLD|PLATINUM */
fecha_alta DATE FORMAT 'YYYY-MM-DD',
fecha_nacimiento DATE FORMAT 'YYYY-MM-DD',
genero CHAR(1), /* M|F|X */
activo BYTEINT DEFAULT 1
)
PRIMARY INDEX (cliente_id);
CREATE INDEX (region) ON ecomm.clientes;

ecomm.productos – Dimensión catálogo

CREATE SET TABLE ecomm.productos,
NO FALLBACK, NO BEFORE JOURNAL, NO AFTER JOURNAL
(
producto_id INTEGER NOT NULL,
nombre VARCHAR(200) NOT NULL,
categoria VARCHAR(60),
subcategoria VARCHAR(60),
marca VARCHAR(60),
precio_base DECIMAL(10,2),
costo DECIMAL(10,2),
activo BYTEINT DEFAULT 1
)
PRIMARY INDEX (producto_id);
CREATE INDEX (categoria) ON ecomm.productos;

ecomm.pedidos – Tabla de Hechos con PPI mensual

CREATE MULTISET TABLE ecomm.pedidos,
NO FALLBACK, NO BEFORE JOURNAL, NO AFTER JOURNAL
(
pedido_id INTEGER NOT NULL,
cliente_id INTEGER NOT NULL,
producto_id INTEGER NOT NULL,
fecha_pedido DATE NOT NULL FORMAT 'YYYY-MM-DD',
cantidad SMALLINT DEFAULT 1,
precio_unitario DECIMAL(10,2),
descuento_pct DECIMAL(5,2) DEFAULT 0,
monto_bruto DECIMAL(12,2),
monto_descuento DECIMAL(12,2) DEFAULT 0,
monto_total DECIMAL(12,2),
estado VARCHAR(20), /* PENDIENTE|ENVIADO|ENTREGADO|CANCELADO */
canal VARCHAR(30), /* WEB|APP|MKTPLACE|TELEVENTAS */
region VARCHAR(50),
tiempo_entrega SMALLINT /* días hasta entrega, 0 si no entregado */
)
PRIMARY INDEX (pedido_id)
PARTITION BY RANGE_N(
fecha_pedido BETWEEN DATE '2022-01-01' AND DATE '2025-12-31'
EACH INTERVAL '1' MONTH
);
CREATE INDEX (cliente_id) ON ecomm.pedidos;

ecomm.pagos y ecomm.devoluciones

CREATE MULTISET TABLE ecomm.pagos,
NO FALLBACK, NO BEFORE JOURNAL, NO AFTER JOURNAL
(
pago_id INTEGER NOT NULL,
pedido_id INTEGER NOT NULL,
cliente_id INTEGER NOT NULL,
fecha_pago DATE FORMAT 'YYYY-MM-DD',
monto_pagado DECIMAL(12,2),
metodo_pago VARCHAR(30), /* TARJETA|TRANSFERENCIA|MP|CRYPTO */
cuotas BYTEINT DEFAULT 1,
estado_pago VARCHAR(20) /* APROBADO|RECHAZADO|PENDIENTE */
)
PRIMARY INDEX (pago_id);
CREATE INDEX (pedido_id) ON ecomm.pagos;

CREATE MULTISET TABLE ecomm.devoluciones,
NO FALLBACK, NO BEFORE JOURNAL, NO AFTER JOURNAL
(
devolucion_id INTEGER NOT NULL,
pedido_id INTEGER NOT NULL,
cliente_id INTEGER NOT NULL,
fecha_devol DATE FORMAT 'YYYY-MM-DD',
motivo VARCHAR(100),
monto_devuelto DECIMAL(12,2),
estado_devol VARCHAR(20) /* APROBADA|RECHAZADA|EN_PROCESO */
)
PRIMARY INDEX (devolucion_id);

12.3 Script Python – Generación de 1.000.000 Filas Sintéticas

generar_dataset_ecomm.py - Se descaga el script y dataset, adjunto con los Ejercicios

# generar_dataset_ecomm.py
# Manual Teradata -- Capitulo 12
# Descripcion: Genera los archivos CSV del dataset de e-commerce.
#
# PRODUCTOS: usa el catalogo real de gondola argentina
#            con precios, marcas y descripciones reales.
#            Si no encuentra el archivo, usa datos sinteticos de respaldo.
#
# Archivos generados:
#   clientes.csv   -->  50.000 filas
#   productos.csv  -->   5.000 filas (sample del catalogo real)
#   pedidos_1M.csv --> 1.000.000 filas
#   pagos.csv      --> ~950.000 filas (excluye cancelados)
#
# Uso:
#   python generar_dataset_ecomm.py
#
# Requisitos: Python 3.8+, solo stdlib

import csv
import random
import datetime
import sys
import os
from pathlib import Path

random.seed(42)
.................
......................
..........................

12.4 Secuencia de Carga y Post-Carga

Orden recomendado: dimensiones → hechos → estadísticas

-- PASO 1: Dimensiones (primero siempre)
-- fastload < fastload_clientes.fl
-- fastload < fastload_productos.fl

-- PASO 2: Tabla de hechos principal
-- fastload < fastload_pedidos.fl

-- PASO 3: Hechos secundarios
-- fastload < fastload_pagos.fl

-- PASO 4: Recolectar estadísticas (OBLIGATORIO post-carga)
COLLECT STATISTICS ON ecomm.pedidos COLUMN (pedido_id);
COLLECT STATISTICS ON ecomm.pedidos COLUMN (cliente_id);
COLLECT STATISTICS ON ecomm.pedidos COLUMN (fecha_pedido);
COLLECT STATISTICS ON ecomm.pedidos COLUMN (estado);
COLLECT STATISTICS ON ecomm.pedidos COLUMN (canal);
COLLECT STATISTICS ON ecomm.clientes COLUMN (cliente_id);
COLLECT STATISTICS ON ecomm.clientes COLUMN (region);
COLLECT STATISTICS ON ecomm.clientes COLUMN (segmento);
COLLECT STATISTICS ON ecomm.productos COLUMN (producto_id);
COLLECT STATISTICS ON ecomm.productos COLUMN (categoria);

-- PASO 5: Verificación final
SELECT 'clientes' AS tabla, COUNT(*) AS filas FROM ecomm.clientes
UNION ALL SELECT 'productos', COUNT(*) FROM ecomm.productos
UNION ALL SELECT 'pedidos', COUNT(*) FROM ecomm.pedidos
UNION ALL SELECT 'pagos', COUNT(*) FROM ecomm.pagos;

12.5 Consultas Analíticas sobre el Dataset

Las siguientes consultas están diseñadas para practicar todos los conceptos del manual sobre el dataset real, de menor a mayor complejidad.

NIVEL 1 – Consultas Básicas de Exploración

Q01 – Resumen general del dataset

SELECT
COUNT(DISTINCT cliente_id) AS clientes_activos,
COUNT(*) AS total_pedidos,
SUM(monto_total) AS facturacion_total,
AVG(monto_total) AS ticket_promedio,
MIN(fecha_pedido) AS primer_pedido,
MAX(fecha_pedido) AS ultimo_pedido
FROM ecomm.pedidos
WHERE estado <> 'CANCELADO';

Q02 – Ventas por canal y estado con tiempo de entrega

SELECT
    canal,
    estado,
    COUNT(*)              AS cantidad,
    SUM(monto_total)      AS monto_total,
    AVG(monto_total)      AS ticket_prom,
    AVG(tiempo_entrega)   AS dias_entrega_prom
FROM ecomm.pedidos
GROUP BY canal, estado
ORDER BY canal, 4 DESC;

Q03 – Top categorías: facturación, descuentos y margen estimado

SELECT
pr.categoria,
COUNT(*) AS cant_pedidos,
SUM(p.monto_total) AS facturacion,
SUM(p.monto_descuento) AS total_descuentos,
CAST(SUM(p.monto_descuento)*100.0
/ NULLIFZERO(SUM(p.monto_bruto)) AS DECIMAL(5,2)) AS pct_desc,
SUM((p.precio_unitario - pr.costo)
* p.cantidad) AS margen_estimado
FROM ecomm.pedidos p
JOIN ecomm.productos pr ON p.producto_id = pr.producto_id
WHERE p.estado <> 'CANCELADO'
GROUP BY pr.categoria
ORDER BY facturacion DESC;

NIVEL 2 – Funciones Analíticas y Temporales

Q04 – Evolución mensual con variación % y acumulado anual

WITH mensual AS (
SELECT
EXTRACT(YEAR FROM fecha_pedido) AS anio,
EXTRACT(MONTH FROM fecha_pedido) AS mes,
SUM(monto_total) AS facturacion,
COUNT(*) AS pedidos,
COUNT(DISTINCT cliente_id) AS clientes_unicos
FROM ecomm.pedidos
WHERE estado <> 'CANCELADO'
GROUP BY 1, 2
)
SELECT
anio, mes, facturacion, pedidos, clientes_unicos,
LAG(facturacion,1) OVER (ORDER BY anio, mes) AS facturacion_mes_ant,
CAST(
(facturacion - LAG(facturacion,1) OVER (ORDER BY anio, mes))*100.0
/ NULLIFZERO(LAG(facturacion,1) OVER (ORDER BY anio, mes))
AS DECIMAL(6,2)) AS var_pct_mom,
SUM(facturacion) OVER (PARTITION BY anio
ORDER BY mes ROWS UNBOUNDED PRECEDING) AS acum_anual
FROM mensual
ORDER BY anio, mes;

Q05 – Top 3 productos por categoría con porcentaje de participación

WITH ventas_prod AS (
SELECT
pr.categoria,
pr.nombre AS producto,
SUM(p.monto_total) AS facturacion,
COUNT(*) AS cant_pedidos
FROM ecomm.pedidos p
JOIN ecomm.productos pr ON p.producto_id = pr.producto_id
WHERE p.estado <> 'CANCELADO'
GROUP BY 1, 2
)
SELECT * FROM (
SELECT
categoria, producto, facturacion, cant_pedidos,
RANK() OVER (PARTITION BY categoria
ORDER BY facturacion DESC) AS rank_cat,
CAST(facturacion*100.0
/ SUM(facturacion) OVER (PARTITION BY categoria)
AS DECIMAL(5,2)) AS pct_categoria
FROM ventas_prod
) r
WHERE rank_cat <= 3
ORDER BY categoria, rank_cat;

Q06 – Media móvil 7 días y detección de días atípicos

WITH diario AS (
SELECT fecha_pedido,
COUNT(*) AS pedidos, SUM(monto_total) AS monto
FROM ecomm.pedidos
WHERE estado <> 'CANCELADO'
AND fecha_pedido BETWEEN DATE '2024-01-01' AND DATE '2024-12-31'
GROUP BY fecha_pedido
),
con_media AS (
SELECT
fecha_pedido, pedidos, monto,
AVG(monto) OVER (ORDER BY fecha_pedido
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS mm7d,
STDDEV_POP(monto) OVER (ORDER BY fecha_pedido
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS std7d
FROM diario
)
SELECT
fecha_pedido, pedidos, monto, mm7d,
CASE WHEN ABS(monto - mm7d) > 2 * NULLIFZERO(std7d)
THEN 'ANOMALIA' ELSE 'NORMAL' END AS clasificacion
FROM con_media
ORDER BY fecha_pedido;

NIVEL 3 – Análisis de Clientes y Segmentación

Q07 – Segmentación RFM completa

WITH rfm_base AS (
    SELECT
        cliente_id,
        ZEROIFNULL((MAX(fecha_pedido) - DATE) * -1) AS recencia,
        COUNT(*)                                     AS frecuencia,
        SUM(monto_total)                             AS monetario
    FROM ecomm.pedidos
    WHERE estado <> 'CANCELADO'
    GROUP BY cliente_id
),
scores AS (
    SELECT
        cliente_id,
        recencia,
        frecuencia,
        monetario,
        RANK() OVER (ORDER BY recencia  ASC)  AS r_score,
        RANK() OVER (ORDER BY frecuencia DESC) AS f_score,
        RANK() OVER (ORDER BY monetario  DESC) AS m_score
    FROM rfm_base
)
SELECT
    cliente_id, recencia, frecuencia, monetario,
    r_score, f_score, m_score,
    r_score + f_score + m_score AS rfm_total,
    CASE
        WHEN r_score >= 4 AND f_score >= 4  THEN 'CHAMPION'
        WHEN r_score >= 3 AND f_score >= 3  THEN 'LOYAL'
        WHEN r_score >= 4 AND f_score <= 2  THEN 'NEW_CUSTOMER'
        WHEN r_score <= 2 AND f_score >= 3  THEN 'AT_RISK'
        WHEN r_score = 1  AND f_score = 1   THEN 'LOST'
        ELSE 'NEEDS_ATTENTION'
    END AS segmento_rfm
FROM scores
ORDER BY 8 DESC;

Q08 – Análisis de cohortes: retención mensual

WITH primera AS (
SELECT cliente_id,
EXTRACT(YEAR FROM MIN(fecha_pedido))*100
+ EXTRACT(MONTH FROM MIN(fecha_pedido)) AS cohorte
FROM ecomm.pedidos WHERE estado <> 'CANCELADO'
GROUP BY 1
),
act AS (
SELECT DISTINCT p.cliente_id, pr.cohorte,
EXTRACT(YEAR FROM p.fecha_pedido)*100
+ EXTRACT(MONTH FROM p.fecha_pedido) AS mes_act
FROM ecomm.pedidos p
JOIN primera pr ON p.cliente_id = pr.cliente_id
WHERE p.estado <> 'CANCELADO'
)
SELECT
cohorte,
mes_act,
COUNT(DISTINCT cliente_id) AS clientes,
(mes_act/100*12 + MOD(mes_act,100))
- (cohorte/100*12 + MOD(cohorte,100)) AS mes_numero
FROM act
GROUP BY 1, 2
ORDER BY 1, 4;

Q09 – Detección de clientes en riesgo de churn (GOLD y PLATINUM)

WITH act AS (
SELECT cliente_id,
DATE - MAX(fecha_pedido) AS dias_inactivo,
COUNT(*) AS total_pedidos,
SUM(CASE WHEN fecha_pedido >= DATE-180 THEN monto_total ELSE 0 END) AS m_ult6m,
SUM(CASE WHEN fecha_pedido BETWEEN DATE-360 AND DATE-181
THEN monto_total ELSE 0 END) AS m_ant6m
FROM ecomm.pedidos WHERE estado <> 'CANCELADO'
GROUP BY 1
)
SELECT
a.cliente_id, c.nombre, c.segmento, c.region,
dias_inactivo, total_pedidos, m_ult6m, m_ant6m,
CAST((m_ult6m-m_ant6m)*100.0/NULLIFZERO(m_ant6m) AS DECIMAL(6,2)) AS var_gasto_pct,
CASE
WHEN dias_inactivo > 180 THEN 'CHURN_ALTO'
WHEN dias_inactivo > 90 AND m_ult6m < m_ant6m*0.5 THEN 'CHURN_MEDIO'
ELSE 'CHURN_BAJO'
END AS riesgo_churn
FROM act a
JOIN ecomm.clientes c ON a.cliente_id = c.cliente_id
WHERE dias_inactivo > 60
AND c.segmento IN ('GOLD','PLATINUM')
ORDER BY dias_inactivo DESC;

NIVEL 4 – Análisis Avanzado Multi-tabla

Q10 – Tasa de devolución por categoría y región

SELECT
pr.categoria,
p.region,
COUNT(DISTINCT p.pedido_id) AS total_entregados,
COUNT(DISTINCT d.pedido_id) AS devueltos,
CAST(COUNT(DISTINCT d.pedido_id)*100.0
/ NULLIFZERO(COUNT(DISTINCT p.pedido_id)) AS DECIMAL(5,2)) AS tasa_devol_pct,
SUM(d.monto_devuelto) AS monto_devuelto
FROM ecomm.pedidos p
JOIN ecomm.productos pr ON p.producto_id = pr.producto_id
LEFT JOIN ecomm.devoluciones d ON p.pedido_id = d.pedido_id
WHERE p.estado = 'ENTREGADO'
GROUP BY 1, 2
ORDER BY tasa_devol_pct DESC;

Q11 – LTV proyectado a 12 meses por segmento y región

WITH metricas AS (
SELECT p.cliente_id, c.segmento, c.region,
COUNT(*) AS freq,
SUM(p.monto_total) AS ltv_hist,
AVG(p.monto_total) AS aov,
COUNT(*)*1.0/NULLIFZERO(
MONTHS_BETWEEN(DATE,MIN(p.fecha_pedido))) AS freq_mensual
FROM ecomm.pedidos p
JOIN ecomm.clientes c ON p.cliente_id=c.cliente_id
WHERE p.estado <> 'CANCELADO'
GROUP BY 1,2,3
)
SELECT segmento, region,
COUNT(*) AS clientes,
AVG(ltv_hist) AS ltv_prom,
AVG(aov) AS aov_prom,
CAST(AVG(aov)*AVG(freq_mensual)*12 AS DECIMAL(15,2)) AS ltv_12m,
CAST(AVG(aov)*AVG(freq_mensual)*12*0.55 AS DECIMAL(15,2)) AS margen_12m
FROM metricas
GROUP BY 1,2
ORDER BY ltv_12m DESC;

Q12 – Análisis de métodos de pago y tasa de aprobación

SELECT
metodo_pago, cuotas,
COUNT(*) AS total,
SUM(CASE WHEN estado_pago='APROBADO' THEN 1 ELSE 0 END) AS aprobados,
SUM(CASE WHEN estado_pago='RECHAZADO' THEN 1 ELSE 0 END) AS rechazados,
CAST(SUM(CASE WHEN estado_pago='APROBADO' THEN 1 ELSE 0 END)*100.0
/ NULLIFZERO(COUNT(*)) AS DECIMAL(5,2)) AS tasa_aprobacion_pct,
AVG(monto_pagado) AS ticket_prom,
SUM(monto_pagado) AS monto_total
FROM ecomm.pagos
GROUP BY 1, 2
ORDER BY metodo_pago, cuotas;

Q13 – Percentil 90 de tiempo de entrega por región y canal

SELECT
region, canal,
COUNT(*) AS pedidos_entregados,
AVG(tiempo_entrega) AS dias_prom,
MIN(tiempo_entrega) AS dias_min,
MAX(tiempo_entrega) AS dias_max,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY tiempo_entrega) AS mediana,
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY tiempo_entrega) AS p90
FROM ecomm.pedidos
WHERE estado = 'ENTREGADO' AND tiempo_entrega > 0
GROUP BY 1, 2
ORDER BY p90 DESC;

Q14 – Canasta de productos: pares comprados juntos en el mismo mes

WITH compras AS (
SELECT cliente_id, producto_id,
EXTRACT(YEAR FROM fecha_pedido)*100
+ EXTRACT(MONTH FROM fecha_pedido) AS mes
FROM ecomm.pedidos WHERE estado <> 'CANCELADO'
GROUP BY 1,2,3
)
SELECT
a.producto_id AS prod_a, pa.nombre AS nombre_a,
b.producto_id AS prod_b, pb.nombre AS nombre_b,
COUNT(*) AS compras_juntas
FROM compras a
JOIN compras b
ON a.cliente_id = b.cliente_id
AND a.mes = b.mes
AND a.producto_id < b.producto_id
JOIN ecomm.productos pa ON a.producto_id = pa.producto_id
JOIN ecomm.productos pb ON b.producto_id = pb.producto_id
GROUP BY 1,2,3,4
HAVING compras_juntas >= 50
ORDER BY compras_juntas DESC;
Siguiente iteración del manual

Se agregarán las consultas Q15-Q25 cubriendo: detección de fraude por patrones de compra, forecast con regresión lineal en SQL, análisis de estacionalidad y segmentación geográfica avanzada.

Capítulo 13: Consultas Avanzadas – Fraude, Forecast y Estacionalidad

Este capítulo completa la serie de consultas analíticas sobre el dataset de e-commerce. Las consultas Q15 a Q25 abordan tres grandes áreas: detección de patrones anómalos y fraude, proyecciones de ventas con regresión lineal en SQL puro, y análisis de estacionalidad. Todas ejecutables directamente sobre las tablas del Capítulo 12.

SECCIÓN A — Detección de Fraude y Anomalías (Q15–Q18)

Q15 – Clientes con velocidad de compra anormal (burst de pedidos)

Detecta clientes que realizaron una cantidad inusualmente alta de pedidos en una ventana corta de tiempo (1 hora), un patrón típico de fraude automatizado o uso indebido de cuentas.

Q15 – Detección de ráfagas de pedidos en ventana de 1 hora

WITH pedidos_ts AS (
/* Agregar timestamp sintético combinando fecha + hora aleatoria */
/* En un entorno real, usar la columna timestamp del pedido */
SELECT
pedido_id,
cliente_id,
fecha_pedido,
monto_total,
canal,
-- Número de pedido del cliente en el día
ROW_NUMBER() OVER (
PARTITION BY cliente_id, fecha_pedido
ORDER BY pedido_id
) AS nro_pedido_dia,
-- Total de pedidos del cliente ese día
COUNT(*) OVER (
PARTITION BY cliente_id, fecha_pedido
) AS total_pedidos_dia,
-- Monto acumulado del cliente ese día
SUM(monto_total) OVER (
PARTITION BY cliente_id, fecha_pedido
ORDER BY pedido_id
ROWS UNBOUNDED PRECEDING
) AS monto_acum_dia
FROM ecomm.pedidos
WHERE estado <> 'CANCELADO'
),
estadisticas_globales AS (
SELECT
AVG(total_pedidos_dia) AS media_pedidos_dia,
STDDEV_POP(total_pedidos_dia) AS std_pedidos_dia
FROM (
SELECT cliente_id, fecha_pedido, COUNT(*) AS total_pedidos_dia
FROM ecomm.pedidos
GROUP BY cliente_id, fecha_pedido
) sub
)
SELECT DISTINCT
p.cliente_id,
c.nombre,
c.segmento,
p.fecha_pedido,
p.total_pedidos_dia,
p.monto_acum_dia,
eg.media_pedidos_dia,
eg.std_pedidos_dia,
/* Z-Score: cuántas desviaciones estándar sobre la media */
CAST(
(p.total_pedidos_dia - eg.media_pedidos_dia)
/ NULLIFZERO(eg.std_pedidos_dia)
AS DECIMAL(8,2)) AS z_score,
CASE
WHEN (p.total_pedidos_dia - eg.media_pedidos_dia)
/ NULLIFZERO(eg.std_pedidos_dia) > 3 THEN 'ALERTA_ALTA'
WHEN (p.total_pedidos_dia - eg.media_pedidos_dia)
/ NULLIFZERO(eg.std_pedidos_dia) > 2 THEN 'ALERTA_MEDIA'
ELSE 'NORMAL'
END AS nivel_alerta
FROM pedidos_ts p
CROSS JOIN estadisticas_globales eg
JOIN ecomm.clientes c ON p.cliente_id = c.cliente_id
WHERE p.total_pedidos_dia > eg.media_pedidos_dia + 2 * eg.std_pedidos_dia
ORDER BY z_score DESC, monto_acum_dia DESC;

Q16 – Detección de importes redondos sospechosos

En fraude financiero, los montos perfectamente redondos (ej: 10000.00, 50000.00) son indicadores de transacciones sintéticas. Esta consulta identifica concentraciones anómalas de importes redondos por cliente.

Q16 – Clientes con alta proporción de montos redondos

WITH analisis_redondos AS (
SELECT
cliente_id,
COUNT(*) AS total_pedidos,
/* Monto redondo: sin centavos y múltiplo de 100 */
SUM(CASE
WHEN MOD(CAST(monto_total AS INTEGER), 100) = 0
AND monto_total = CAST(monto_total AS INTEGER)
THEN 1 ELSE 0
END) AS pedidos_redondos,
SUM(monto_total) AS monto_total_cliente,
SUM(CASE
WHEN MOD(CAST(monto_total AS INTEGER), 100) = 0
AND monto_total = CAST(monto_total AS INTEGER)
THEN monto_total ELSE 0
END) AS monto_redondos
FROM ecomm.pedidos
WHERE estado <> 'CANCELADO'
GROUP BY cliente_id
HAVING total_pedidos >= 5 /* Solo clientes con historial suficiente */
)
SELECT
ar.cliente_id,
c.nombre,
c.segmento,
c.region,
total_pedidos,
pedidos_redondos,
monto_total_cliente,
monto_redondos,
CAST(pedidos_redondos * 100.0 / total_pedidos AS DECIMAL(5,2))
AS pct_redondos,
CAST(monto_redondos * 100.0 / NULLIFZERO(monto_total_cliente)
AS DECIMAL(5,2)) AS pct_monto_redondo
FROM analisis_redondos ar
JOIN ecomm.clientes c ON ar.cliente_id = c.cliente_id
WHERE pedidos_redondos * 100.0 / total_pedidos > 80 /* > 80% redondos */
ORDER BY pct_redondos DESC, monto_redondos DESC;

Q17 – Triangulación: pedidos entregados pero con pago rechazado

Uno de los patrones de fraude más comunes en e-commerce: el pedido llega a estado ENTREGADO pero el pago asociado fue rechazado o nunca se procesó correctamente.

Q17 – Pedidos entregados sin pago aprobado

SELECT
p.pedido_id,
p.cliente_id,
c.nombre,
c.segmento,
c.region,
p.fecha_pedido,
p.monto_total,
p.canal,
pg.estado_pago,
pg.metodo_pago,
pg.monto_pagado,
p.monto_total - COALESCE(pg.monto_pagado, 0) AS diferencia,
CASE
WHEN pg.pedido_id IS NULL THEN 'SIN_PAGO_REGISTRADO'
WHEN pg.estado_pago = 'RECHAZADO' THEN 'PAGO_RECHAZADO'
WHEN pg.estado_pago = 'PENDIENTE' THEN 'PAGO_PENDIENTE'
WHEN pg.monto_pagado < p.monto_total * 0.95
THEN 'PAGO_INSUFICIENTE'
END AS tipo_anomalia
FROM ecomm.pedidos p
JOIN ecomm.clientes c ON p.cliente_id = c.cliente_id
LEFT JOIN ecomm.pagos pg ON p.pedido_id = pg.pedido_id
WHERE p.estado = 'ENTREGADO'
AND (
pg.pedido_id IS NULL
OR pg.estado_pago IN ('RECHAZADO', 'PENDIENTE')
OR pg.monto_pagado < p.monto_total * 0.95
)
ORDER BY p.monto_total DESC;

Q18 – Score de riesgo compuesto por cliente

Consolida múltiples señales de riesgo en un score único por cliente, combinando: devoluciones excesivas, pagos rechazados, pedidos cancelados y comportamiento de compra atípico.

Q18 – Score de riesgo multicriterio

WITH metricas AS (
SELECT
p.cliente_id,
COUNT(*) AS total_ped,
SUM(CASE WHEN p.estado='CANCELADO' THEN 1 ELSE 0 END) AS cancelados,
SUM(CASE WHEN p.estado='ENTREGADO' THEN 1 ELSE 0 END) AS entregados,
COUNT(DISTINCT d.devolucion_id) AS devoluciones,
SUM(CASE WHEN pg.estado_pago='RECHAZADO'
THEN 1 ELSE 0 END) AS pagos_rechazados,
SUM(p.monto_total) AS monto_total
FROM ecomm.pedidos p
LEFT JOIN ecomm.devoluciones d ON p.pedido_id = d.pedido_id
LEFT JOIN ecomm.pagos pg ON p.pedido_id = pg.pedido_id
GROUP BY p.cliente_id
HAVING total_ped >= 3
),
scores AS (
SELECT
cliente_id, total_ped, monto_total,
cancelados, devoluciones, pagos_rechazados,
/* Cada señal suma puntos de riesgo (0-100) */
CAST(cancelados * 100.0 / NULLIFZERO(total_ped) AS DECIMAL(5,1)) AS pct_cancel,
CAST(devoluciones* 100.0 / NULLIFZERO(entregados) AS DECIMAL(5,1)) AS pct_devol,
CAST(pagos_rechazados*100.0/NULLIFZERO(total_ped) AS DECIMAL(5,1)) AS pct_rechaz,
/* Score ponderado: cancelaciones 40% + devoluciones 35% + rechazos 25% */
CAST(
(cancelados *100.0/NULLIFZERO(total_ped)) * 0.40 +
(devoluciones*100.0/NULLIFZERO(entregados)) * 0.35 +
(pagos_rechazados*100.0/NULLIFZERO(total_ped))* 0.25
AS DECIMAL(6,2)) AS risk_score
FROM metricas
)
SELECT
s.cliente_id,
c.nombre,
c.segmento,
c.region,
total_ped,
monto_total,
pct_cancel,
pct_devol,
pct_rechaz,
risk_score,
CASE
WHEN risk_score >= 40 THEN 'RIESGO_ALTO'
WHEN risk_score >= 20 THEN 'RIESGO_MEDIO'
ELSE 'RIESGO_BAJO'
END AS nivel_riesgo
FROM scores s
JOIN ecomm.clientes c ON s.cliente_id = c.cliente_id
ORDER BY risk_score DESC;
Consideración

El score de riesgo es un modelo heurístico basado en reglas. En producción se combina con modelos de ML externalizados (Python/R) cuyos scores se cargan de vuelta a Teradata para enriquecimiento.

SECCIÓN B — Forecast con Regresión Lineal en SQL (Q19–Q22)

Teradata permite implementar regresión lineal simple directamente en SQL usando funciones de agregación estándar. Aunque no reemplaza a herramientas especializadas, es útil para forecasts rápidos sin salir del motor.

Q19 – Regresión lineal: tendencia de ventas mensuales

Calcula la recta de regresión Y = a + b*X sobre la serie mensual de ventas, donde X es el número de mes y Y es la facturación. Los coeficientes a (intercepto) y b (pendiente) permiten proyectar meses futuros.

Q19 – Coeficientes de regresión lineal sobre serie mensual

WITH serie_agrupada AS (
    -- Paso 1: agrupar por periodo
    SELECT
        EXTRACT(YEAR FROM fecha_pedido)*100
        + EXTRACT(MONTH FROM fecha_pedido) AS periodo,
        SUM(monto_total)                   AS y
    FROM ecomm.pedidos
    WHERE estado <> 'CANCELADO'
    GROUP BY 1
),
serie AS (
    -- Paso 2: asignar numero secuencial ya sin GROUP BY
    SELECT
        periodo,
        y,
        ROW_NUMBER() OVER (ORDER BY periodo) AS x
    FROM serie_agrupada
),
regresion AS (
    SELECT
        COUNT(*)   AS n,
        AVG(x)     AS media_x,
        AVG(y)     AS media_y,
        CAST(
            (SUM(x*y) - COUNT(*)*AVG(x)*AVG(y))
            / NULLIFZERO(SUM(x*x) - COUNT(*)*AVG(x)*AVG(x))
        AS DECIMAL(18,4)) AS pendiente_b,
        CAST(
            AVG(y) -
            (SUM(x*y) - COUNT(*)*AVG(x)*AVG(y))
            / NULLIFZERO(SUM(x*x) - COUNT(*)*AVG(x)*AVG(x))
            * AVG(x)
        AS DECIMAL(18,2)) AS intercepto_a
    FROM serie
)
SELECT
    s.periodo,
    s.x,
    s.y                                                      AS facturacion_real,
    CAST(r.intercepto_a + r.pendiente_b * s.x
         AS DECIMAL(18,2))                                   AS facturacion_ajustada,
    r.pendiente_b,
    r.intercepto_a,
    CAST(ABS(s.y - (r.intercepto_a + r.pendiente_b*s.x))*100.0
         / NULLIFZERO(s.y) AS DECIMAL(6,2))                  AS mape_pct
FROM serie s
CROSS JOIN regresion r
ORDER BY s.x;

Q20 – Proyección de ventas para los próximos 6 meses

Usando los coeficientes calculados en Q19, genera los valores proyectados para los 6 meses siguientes al último período con datos reales.

Q20 – Forecast 6 meses con intervalo de confianza simple

WITH serie_agrupada AS (
    SELECT
        EXTRACT(YEAR FROM fecha_pedido)*100
        + EXTRACT(MONTH FROM fecha_pedido) AS periodo,
        SUM(monto_total)                   AS y
    FROM ecomm.pedidos
    WHERE estado <> 'CANCELADO'
    GROUP BY 1
),
serie AS (
    SELECT
        periodo,
        y,
        ROW_NUMBER() OVER (ORDER BY periodo) AS x
    FROM serie_agrupada
),
regresion AS (
    SELECT
        MAX(x)        AS ultimo_x,
        MAX(periodo)  AS ultimo_periodo,
        CAST(
            (SUM(x*y) - COUNT(*)*AVG(x)*AVG(y))
            / NULLIFZERO(SUM(x*x) - COUNT(*)*AVG(x)*AVG(x))
        AS DECIMAL(18,4)) AS b,
        CAST(
            AVG(y) -
            (SUM(x*y) - COUNT(*)*AVG(x)*AVG(y))
            / NULLIFZERO(SUM(x*x) - COUNT(*)*AVG(x)*AVG(x))
            * AVG(x)
        AS DECIMAL(18,2)) AS a,
        STDDEV_POP(y)     AS std_y
    FROM serie
),
futuros AS (
    SELECT 1 AS offset FROM (SELECT 1 AS d) t
    UNION ALL SELECT 2 FROM (SELECT 1 AS d) t
    UNION ALL SELECT 3 FROM (SELECT 1 AS d) t
    UNION ALL SELECT 4 FROM (SELECT 1 AS d) t
    UNION ALL SELECT 5 FROM (SELECT 1 AS d) t
    UNION ALL SELECT 6 FROM (SELECT 1 AS d) t
)
SELECT
    r.ultimo_x + f.offset                                     AS x_futuro,
    /* Calcular YYYYMM del periodo proyectado */
    EXTRACT(YEAR FROM ADD_MONTHS(
        CAST(CAST(r.ultimo_periodo AS CHAR(6)) || '01'
             AS DATE FORMAT 'YYYYMMDD'),
        f.offset)) * 100
    + EXTRACT(MONTH FROM ADD_MONTHS(
        CAST(CAST(r.ultimo_periodo AS CHAR(6)) || '01'
             AS DATE FORMAT 'YYYYMMDD'),
        f.offset))                                            AS periodo_proyectado,
    CAST(r.a + r.b*(r.ultimo_x + f.offset) AS DECIMAL(18,2)) AS forecast,
    CAST(r.a + r.b*(r.ultimo_x+f.offset) - 1.645*r.std_y
         AS DECIMAL(18,2))                                    AS limite_inferior_90,
    CAST(r.a + r.b*(r.ultimo_x+f.offset) + 1.645*r.std_y
         AS DECIMAL(18,2))                                    AS limite_superior_90,
    r.b                                                       AS pendiente_mensual,
    CAST(r.b*100.0 / NULLIFZERO(r.a) AS DECIMAL(6,2))        AS crecimiento_pct_mensual
FROM futuros f
CROSS JOIN regresion r
ORDER BY f.offset;

Q21 – Regresión por categoría: ¿cuáles crecen más rápido?

Aplica la misma regresión lineal a cada categoría de productos en paralelo, usando PARTITION BY, para identificar cuáles tienen mayor pendiente de crecimiento.

Q21 – Pendiente de crecimiento por categoría (REGR_SLOPE)

/* Teradata soporta funciones de regresión ANSI directamente */
WITH serie_agrupada AS (
    -- Paso 1: agregar monto por categoria y periodo
    SELECT
        pr.categoria,
        EXTRACT(YEAR FROM p.fecha_pedido)*100
        + EXTRACT(MONTH FROM p.fecha_pedido)    AS periodo,
        SUM(p.monto_total)                      AS y
    FROM ecomm.pedidos p
    JOIN ecomm.productos pr ON p.producto_id = pr.producto_id
    WHERE p.estado <> 'CANCELADO'
    GROUP BY 1, 2
),
serie_cat AS (
    -- Paso 2: asignar numero de mes secuencial por categoria
    SELECT
        categoria,
        periodo,
        y,
        ROW_NUMBER() OVER (
            PARTITION BY categoria
            ORDER BY periodo
        ) AS x
    FROM serie_agrupada
)
SELECT
    categoria,
    COUNT(*)                                              AS meses_con_datos,
    AVG(y)                                               AS facturacion_prom_mensual,
    CAST(REGR_SLOPE(y, x)      AS DECIMAL(18,2))         AS pendiente_mensual,
    CAST(REGR_INTERCEPT(y, x)  AS DECIMAL(18,2))         AS intercepto,
    CAST(REGR_R2(y, x)         AS DECIMAL(8,4))          AS r_cuadrado,
    CAST(REGR_SLOPE(y, x) * 100.0
         / NULLIFZERO(AVG(y))  AS DECIMAL(6,2))          AS crecim_pct_mensual
FROM serie_cat
GROUP BY categoria
ORDER BY pendiente_mensual DESC;
Funciones ANSI

REGR_SLOPE(Y,X), REGR_INTERCEPT(Y,X) y REGR_R2(Y,X) son funciones de regresión incluidas en el estándar SQL:2003 y disponibles en Teradata sin necesidad de código adicional. Mucho más concisas que el cálculo manual de Q19.

Q22 – Forecast con descomposición de tendencia y ajuste estacional

Mejora el forecast puro de regresión agregando un índice estacional mensual. Primero calcula el promedio de cada mes del año, luego ajusta la proyección por ese factor.

Q22 – Forecast con ajuste estacional (trend × índice_mes)

WITH mensual_agrupado AS (
    SELECT
        EXTRACT(YEAR  FROM fecha_pedido) AS anio,
        EXTRACT(MONTH FROM fecha_pedido) AS mes,
        EXTRACT(YEAR FROM fecha_pedido)*100
        + EXTRACT(MONTH FROM fecha_pedido) AS periodo,
        SUM(monto_total)                   AS y
    FROM ecomm.pedidos
    WHERE estado <> 'CANCELADO'
    GROUP BY 1, 2
),
mensual AS (
    SELECT
        anio, mes, periodo, y,
        ROW_NUMBER() OVER (ORDER BY periodo) AS x
    FROM mensual_agrupado
),
estacional AS (
    SELECT
        mes,
        AVG(y)                                          AS prom_mes,
        CAST(AVG(y) / NULLIFZERO(SUM(AVG(y)) OVER ())
             AS DECIMAL(8,4))                           AS indice_estacional
    FROM mensual
    GROUP BY mes
),
regresion AS (
    SELECT
        CAST(REGR_SLOPE(y,x)     AS DECIMAL(18,4)) AS b,
        CAST(REGR_INTERCEPT(y,x) AS DECIMAL(18,2)) AS a,
        MAX(x)                                      AS ultimo_x,
        MAX(periodo)                                AS ultimo_periodo
    FROM mensual
),
futuros AS (
    SELECT 1 AS n FROM (SELECT 1 AS d) t
    UNION ALL SELECT 2 FROM (SELECT 1 AS d) t
    UNION ALL SELECT 3 FROM (SELECT 1 AS d) t
    UNION ALL SELECT 4 FROM (SELECT 1 AS d) t
    UNION ALL SELECT 5 FROM (SELECT 1 AS d) t
    UNION ALL SELECT 6 FROM (SELECT 1 AS d) t
)
SELECT
    r.ultimo_x + f.n                                          AS x,
    EXTRACT(YEAR FROM ADD_MONTHS(
        CAST(CAST(r.ultimo_periodo AS CHAR(6)) || '01'
             AS DATE FORMAT 'YYYYMMDD'), f.n)) * 100
    + EXTRACT(MONTH FROM ADD_MONTHS(
        CAST(CAST(r.ultimo_periodo AS CHAR(6)) || '01'
             AS DATE FORMAT 'YYYYMMDD'), f.n))                AS periodo,
    CAST(r.a + r.b*(r.ultimo_x+f.n) AS DECIMAL(18,2))        AS trend_puro,
    e.indice_estacional,
    CAST((r.a + r.b*(r.ultimo_x+f.n)) * e.indice_estacional
         AS DECIMAL(18,2))                                    AS forecast_ajustado
FROM futuros f
CROSS JOIN regresion r
JOIN estacional e
    ON e.mes = EXTRACT(MONTH FROM ADD_MONTHS(
        CAST(CAST(r.ultimo_periodo AS CHAR(6)) || '01'
             AS DATE FORMAT 'YYYYMMDD'), f.n))
ORDER BY f.n;

SECCIÓN C — Análisis de Estacionalidad (Q23–Q25)

Q23 – Índices estacionales por mes y día de semana

Calcula dos dimensiones de estacionalidad simultáneamente: el índice mensual (enero vs diciembre) y el índice por día de la semana (lunes vs domingo). Útil para planificación de stock y campañas.

Q23 – Doble índice estacional: mensual y semanal

/* ── Índice Estacional MENSUAL ── */
WITH por_mes AS (
SELECT
EXTRACT(MONTH FROM fecha_pedido) AS mes_num,
CASE EXTRACT(MONTH FROM fecha_pedido)
WHEN 1 THEN 'Enero' WHEN 2 THEN 'Febrero'
WHEN 3 THEN 'Marzo' WHEN 4 THEN 'Abril'
WHEN 5 THEN 'Mayo' WHEN 6 THEN 'Junio'
WHEN 7 THEN 'Julio' WHEN 8 THEN 'Agosto'
WHEN 9 THEN 'Septiembre' WHEN 10 THEN 'Octubre'
WHEN 11 THEN 'Noviembre' WHEN 12 THEN 'Diciembre'
END AS mes_nombre,
AVG(monto_total) AS ticket_prom_mes,
COUNT(*) AS pedidos_prom_mes
FROM ecomm.pedidos
WHERE estado <> 'CANCELADO'
GROUP BY 1, 2
)
SELECT
mes_num, mes_nombre,
ticket_prom_mes,
pedidos_prom_mes,
/* Índice > 1.0 = mes por encima del promedio anual */
CAST(ticket_prom_mes / AVG(ticket_prom_mes) OVER ()
AS DECIMAL(6,4)) AS indice_ticket,
CAST(pedidos_prom_mes / AVG(pedidos_prom_mes) OVER ()
AS DECIMAL(6,4)) AS indice_volumen
FROM por_mes
ORDER BY mes_num;

Q23b – Índice estacional por día de la semana

WITH por_dia AS (
SELECT
TD_DAY_OF_WEEK(fecha_pedido) AS dia_num, /* 0=Dom..6=Sab */
CASE TD_DAY_OF_WEEK(fecha_pedido)
WHEN 0 THEN 'Domingo' WHEN 1 THEN 'Lunes'
WHEN 2 THEN 'Martes' WHEN 3 THEN 'Miércoles'
WHEN 4 THEN 'Jueves' WHEN 5 THEN 'Viernes'
WHEN 6 THEN 'Sábado'
END AS dia_nombre,
COUNT(*) AS total_pedidos,
SUM(monto_total) AS monto_total,
AVG(monto_total) AS ticket_prom
FROM ecomm.pedidos
WHERE estado <> 'CANCELADO'
GROUP BY 1, 2
)
SELECT
dia_num, dia_nombre,
total_pedidos, monto_total, ticket_prom,
CAST(total_pedidos / AVG(total_pedidos) OVER ()
AS DECIMAL(6,4)) AS indice_volumen_dia,
CAST(ticket_prom / AVG(ticket_prom) OVER ()
AS DECIMAL(6,4)) AS indice_ticket_dia
FROM por_dia
ORDER BY indice_volumen_dia DESC;

Q24 – Mapa de calor: facturación por mes y día de semana (pivot)

Genera una tabla pivoteada que muestra la facturación promedio en la intersección de cada mes con cada día de la semana, creando un mapa de calor que revela los patrones de compra bidimensionales.

Q24 – Pivot: facturación promedio por MES x DÍA DE SEMANA

SELECT
CASE EXTRACT(MONTH FROM fecha_pedido)
WHEN 1 THEN 'Ene' WHEN 2 THEN 'Feb' WHEN 3 THEN 'Mar'
WHEN 4 THEN 'Abr' WHEN 5 THEN 'May' WHEN 6 THEN 'Jun'
WHEN 7 THEN 'Jul' WHEN 8 THEN 'Ago' WHEN 9 THEN 'Sep'
WHEN 10 THEN 'Oct' WHEN 11 THEN 'Nov' WHEN 12 THEN 'Dic'
END AS mes,
EXTRACT(MONTH FROM fecha_pedido) AS mes_num,
/* Una columna por día de semana */
CAST(AVG(CASE WHEN TD_DAY_OF_WEEK(fecha_pedido)=1
THEN monto_total END) AS DECIMAL(12,0)) AS lunes,
CAST(AVG(CASE WHEN TD_DAY_OF_WEEK(fecha_pedido)=2
THEN monto_total END) AS DECIMAL(12,0)) AS martes,
CAST(AVG(CASE WHEN TD_DAY_OF_WEEK(fecha_pedido)=3
THEN monto_total END) AS DECIMAL(12,0)) AS miercoles,
CAST(AVG(CASE WHEN TD_DAY_OF_WEEK(fecha_pedido)=4
THEN monto_total END) AS DECIMAL(12,0)) AS jueves,
CAST(AVG(CASE WHEN TD_DAY_OF_WEEK(fecha_pedido)=5
THEN monto_total END) AS DECIMAL(12,0)) AS viernes,
CAST(AVG(CASE WHEN TD_DAY_OF_WEEK(fecha_pedido)=6
THEN monto_total END) AS DECIMAL(12,0)) AS sabado,
CAST(AVG(CASE WHEN TD_DAY_OF_WEEK(fecha_pedido)=0
THEN monto_total END) AS DECIMAL(12,0)) AS domingo
FROM ecomm.pedidos
WHERE estado <> 'CANCELADO'
GROUP BY mes, mes_num
ORDER BY mes_num;

Q25 – Análisis de estacionalidad multi-año con descomposición STL manual

Descompone la serie temporal en tres componentes: tendencia (suavizado de media móvil 12 meses), estacionalidad (patrón repetitivo), y residuo (ruido). Esta es la base del método STL (Seasonal and Trend decomposition using Loess) implementada en SQL puro.

Q25 – Descomposición STL manual: tendencia + estacionalidad + residuo

WITH mensual AS (
SELECT
EXTRACT(YEAR FROM fecha_pedido) AS anio,
EXTRACT(MONTH FROM fecha_pedido) AS mes,
SUM(monto_total) AS y
FROM ecomm.pedidos
WHERE estado <> 'CANCELADO'
GROUP BY 1, 2
),
con_tendencia AS (
SELECT
anio, mes, y,
/* Tendencia = media móvil centrada de 12 meses (suaviza ciclo anual) */
AVG(y) OVER (
ORDER BY anio, mes
ROWS BETWEEN 5 PRECEDING AND 6 FOLLOWING
) AS tendencia_mm12
FROM mensual
),
/* Componente estacional = y desestacionalizado (y / tendencia) */
con_desestac AS (
SELECT
anio, mes, y, tendencia_mm12,
CAST(y / NULLIFZERO(tendencia_mm12) AS DECIMAL(8,4)) AS y_desestac
FROM con_tendencia
WHERE tendencia_mm12 IS NOT NULL
),
/* Índice estacional = promedio del ratio por mes (patrón anual) */
indice_estacional AS (
SELECT
mes,
CAST(AVG(y_desestac) AS DECIMAL(8,4)) AS indice_est
FROM con_desestac
GROUP BY mes
)
/* Resultado final: serie descompuesta */
SELECT
d.anio,
d.mes,
d.y AS serie_original,
CAST(d.tendencia_mm12 AS DECIMAL(18,2)) AS tendencia,
i.indice_est AS indice_estacional,
/* Componente estacional absoluta */
CAST(d.tendencia_mm12 * i.indice_est
AS DECIMAL(18,2)) AS componente_estacional,
/* Residuo = lo que no explican tendencia ni estacionalidad */
CAST(d.y - d.tendencia_mm12 * i.indice_est
AS DECIMAL(18,2)) AS residuo,
/* Residuo % sobre la serie original */
CAST((d.y - d.tendencia_mm12 * i.indice_est)
* 100.0 / NULLIFZERO(d.y)
AS DECIMAL(6,2)) AS residuo_pct
FROM con_desestac d
JOIN indice_estacional i ON d.mes = i.mes
ORDER BY d.anio, d.mes;
Interpretación

Residuos con valor absoluto > 15-20% indican eventos atípicos (promociones, cortes de stock, problemas operativos). Un indice_estacional de 1.30 para diciembre significa que ese mes vende 30% más que el promedio anual ajustado por tendencia.

Resumen: Catálogo Completo de Consultas Q01–Q25

N°ConsultaTécnicas UsadasNivel
Q01Resumen general del datasetCOUNT, SUM, AVG, MIN, MAXBásico
Q02Ventas por canal y estadoGROUP BY, agregacionesBásico
Q03Top categorías con margen estimadoJOIN, NULLIFZERO, cálculo de margenBásico
Q04Evolución mensual con variación %LAG, SUM acumulado, EXTRACTIntermedio
Q05Top 3 productos por categoríaRANK, PARTITION BY, pct. participaciónIntermedio
Q06Media móvil 7 días y anomalíasAVG/STDDEV ventana deslizante, CASEIntermedio
Q07Segmentación RFMNTILE, lógica de scoring multicriteriaIntermedio
Q08Análisis de cohortesFIRST_VALUE, GROUP BY temporalAvanzado
Q09Detección de churn (GOLD/PLATINUM)SUM condicional, comparación períodosAvanzado
Q10Tasa de devolución por cat. y regiónLEFT JOIN, NULLIFZERO, tasa calculadaAvanzado
Q11LTV proyectado 12 mesesMONTHS_BETWEEN, proyección por segmentoAvanzado
Q12Métodos de pago y aprobaciónSUM CASE, tasa de aprobaciónIntermedio
Q13Percentil 90 de tiempo de entregaPERCENTILE_CONT, agrupación multi-dimAvanzado
Q14Canasta de productos (Market Basket)Self-JOIN, HAVING, análisis de paresAvanzado
Q15Detección de ráfagas de pedidos (fraude)ROW_NUMBER, CROSS JOIN, Z-ScoreExpert
Q16Montos redondos sospechososMOD, CAST, detección por umbralExpert
Q17Entregados sin pago aprobadoLEFT JOIN, COALESCE, triangulaciónExpert
Q18Score de riesgo compuestoScore ponderado multi-CTEExpert
Q19Regresión lineal manual en SQLROW_NUMBER, fórmula b/a, CROSS JOINExpert
Q20Forecast 6 meses con IC del 90%Proyección + STDDEV, límites CIExpert
Q21Pendiente de crecimiento por categoríaREGR_SLOPE, REGR_R2, REGR_INTERCEPTExpert
Q22Forecast con ajuste estacionalADD_MONTHS, índice estacional, trend×est.Expert
Q23Índices estacionales mes y día semanaTD_DAY_OF_WEEK, doble índiceExpert
Q24Mapa de calor mes × día semana (pivot)CASE pivot, AVG condicionalExpert
Q25Descomposición STL: tend+estac+residuoMM12, índice estacional, residuo %Expert

Bibliografia y Referencias

Este manual fue construido tomando como base la documentacion oficial de Teradata, bibliografia academica de bases de datos relacionales y fuentes de referencia de la industria. A continuacion se detallan todas las fuentes utilizadas.

Documentacion Oficial Teradata

Toda la documentacion oficial se encuentra disponible en docs.teradata.com

Teradata Database SQL Reference. Referencia completa del lenguaje SQL de Teradata: funciones analiticas, tipos de datos, sintaxis DDL/DML y extensiones propietarias del motor.

Teradata FastLoad Reference Manual. Guia oficial de FastLoad: sintaxis del DEFINE, fases de carga (Acquisition y Application), manejo de tablas de error y opciones de sesion.

Teradata MultiLoad Reference Manual. Guia oficial de MultiLoad: layouts, operaciones DML masivas (INSERT/UPDATE/DELETE), work tables, error tables y checkpoints de restart.

Teradata Parallel Transporter User Guide. Documentacion de TPT como marco unificado moderno que reemplaza a FastLoad y MultiLoad a partir de Teradata 13.x.

Teradata Database Design. Guia de diseno: eleccion del Indice Primario (PI), PPI, distribucion de datos entre AMPs y estrategias de optimizacion.

Teradata Database Administration. Administracion del sistema: usuarios, roles, perfiles, espacios PERM/SPOOL/TEMP, monitoreo y vistas del diccionario DBC.

DBC Views Reference. Referencia de las vistas del diccionario de datos del sistema (DBC.TablesV, DBC.IndicesV, DBC.SessionInfoV, etc.).

Bibliografia Academica

Silberschatz, A.; Korth, H.; Sudarshan, S. Database System Concepts, 7ma edicion. McGraw-Hill Education, 2019. ISBN: 978-0078022159.

Silberschatz, A.; Korth, H.; Sudarshan, S. Fundamentos de Bases de Datos, 6ta edicion en espanol. McGraw-Hill / Interamericana, 2014. ISBN: 978-8448190330.

Kimball, R.; Ross, M. The Data Warehouse Toolkit, 3ra edicion. Wiley, 2013. ISBN: 978-1118530801. Referencia para modelado dimensional y esquemas estrella del Capitulo 12.

Inmon, W.H. Building the Data Warehouse, 4ta edicion. Wiley, 2005. ISBN: 978-0764599446. Fundamentos de arquitectura de Data Warehouse empresarial.

Ramakrishnan, R.; Gehrke, J. Database Management Systems, 3ra edicion. McGraw-Hill, 2002. ISBN: 978-0072465631.

Fuentes de Datos Utilizadas

Catálogo de Precios de Referencia de Supermercados Argentina. Archivo de precios de lista de productos de gondola del mercado argentino (~707.000 registros) con descripciones, marcas y unidades de medida. Utilizado en el Capitulo 12 para generar la tabla ecomm.productos con datos reales del mercado local.

Dataset sintetico e-commerce ecomm.*. Generado con el script generar_dataset_ecomm.py. Produce ~2.000.000 de registros en 4 tablas (clientes, productos, pedidos, pagos). Base de todos los ejercicios Q01-Q25.

Herramientas y Tecnologias

Teradata Database 17.x. Motor de base de datos sobre el que se desarrollaron y validaron todos los ejemplos SQL del manual.

Teradata Studio 17.x. Entorno de desarrollo y ejecucion de consultas SQL utilizado durante la construccion del manual.

Teradata Tools and Utilities (TTU). Conjunto de utilitarios que incluye FastLoad, MultiLoad y BTEQ. Los scripts de FastLoad fueron validados en un entorno Teradata 17 real.

Python 3.8+. Lenguaje utilizado para la generacion del dataset sintetico de e-commerce. Solo se utilizaron modulos de la biblioteca estandar.

Claude — Anthropic, 2025. Asistente de inteligencia artificial utilizado como co-autor en la redaccion, estructuracion y generacion de ejemplos del manual.

Nota sobre la Autoria

Este manual fue construido de forma colaborativa entre Javier, profesional de datos con experiencia avanzada en entornos Teradata, DataStage y Control-M, y Claude, el asistente de inteligencia artificial de Anthropic. La estructura, los contenidos tecnicos, los ejercicios practicos y el dataset de practica son el resultado de un proceso iterativo de construccion conjunta.

Todos los ejemplos de SQL fueron revisados para garantizar su compatibilidad con la sintaxis de Teradata. Los scripts de FastLoad fueron validados y corregidos directamente en un entorno Teradata 17 real durante la construccion del manual.

Aviso

Los precios del catalogo de productos argentino corresponden a valores de lista de referencia y pueden no reflejar precios actuales de mercado. Su uso en este manual es exclusivamente educativo y de practica de consultas SQL.

Archivos de práctica

Los scripts y recursos de este manual, para descargar y practicar en tu propio entorno.