Volver al Hub

LABORATORIO 01

Misión

Arquitectura & Ingesta

Fase 1: Ingesta

Domando el Caos:
De CSV Sucio a Producción

La mayoría de los proyectos de datos fallan en la ingesta. En este laboratorio, aprenderás la técnica profesional: ELT (Extract, Load, Transform) usando tablas Staging de alta velocidad.

Arquitectura del Laboratorio

graph LR RAW[CSV Sucio] -->|Python| DISK[Disco Local] DISK -->|BULK INSERT| STG[(SQL Staging)] STG -->|TRY_CAST & Filter| PROD[(SQL Producción)] style RAW fill:#f1f5f9,stroke:#64748b,stroke-dasharray: 5 5 style DISK fill:#e2e8f0,stroke:#64748b style STG fill:#fef9c3,stroke:#eab308,stroke-width:2px style PROD fill:#dcfce7,stroke:#16a34a,stroke-width:2px
1

Generar "Datos Sucios"

Para simular un escenario real y difícil, no usaremos un archivo limpio. Usaremos Python para descargar datos oficiales y convertirlos a un CSV sin cabeceras y sin tipos definidos. Así es como llegan los datos de sistemas Legacy.

00_fetch_data.py
import pandas as pd
import os

# 1. Fuente Oficial: NYC Taxi & Limousine Commission (Enero 2023)
url = "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2023-01.parquet"

print(f"Descargando datos desde {url}...")
# Pandas lee Parquet nativamente (mucho más eficiente que CSV)
df = pd.read_parquet(url)

# 2. Muestreo: Tomamos 1 millón de filas para el ejercicio
df = df.sample(n=1000000, random_state=42)

print("Corrompiendo formato para simular Legacy CSV...")
# Guardamos SIN index y SIN header. Esto obliga a definir la estructura manualmente en SQL.
output_file = "C:\\Data\\taxi_dirty.csv" # Asegúrate que esta carpeta exista
df.to_csv(output_file, index=False, header=False)

print(f"¡Hecho! Archivo generado en: {output_file}")
print(f"Filas: {len(df)}")
2

La Estrategia Staging

Regla de Oro de Ingeniería

Nunca intentes transformar datos durante la carga. Si defines una columna como INT y llega una letra "A", la carga de 1 millón de filas fallará por completo.
Solución: Carga todo como VARCHAR en una tabla Staging (Heap) y limpia después.

01_bulk_load.sql
CREATE DATABASE NYC_Taxi_Lab;
GO
USE NYC_Taxi_Lab;

-- 1. Tabla STAGING (Heap Table, sin Clustered Index)
-- Todo es VARCHAR para máxima permisividad.
CREATE TABLE Staging_Trips (
    VendorID VARCHAR(50),
    tpep_pickup_datetime VARCHAR(50),
    tpep_dropoff_datetime VARCHAR(50),
    passenger_count VARCHAR(50),
    trip_distance VARCHAR(50),
    RatecodeID VARCHAR(50),
    store_and_fwd_flag VARCHAR(50),
    PULocationID VARCHAR(50),
    DOLocationID VARCHAR(50),
    payment_type VARCHAR(50),
    fare_amount VARCHAR(50),
    extra VARCHAR(50),
    mta_tax VARCHAR(50),
    tip_amount VARCHAR(50),
    tolls_amount VARCHAR(50),
    improvement_surcharge VARCHAR(50),
    total_amount VARCHAR(50),
    congestion_surcharge VARCHAR(50),
    airport_fee VARCHAR(50)
);

-- 2. Carga de Alta Velocidad (Minimal Logging)
BULK INSERT Staging_Trips
FROM 'C:\Data\taxi_dirty.csv'
WITH (
    FIELDTERMINATOR = ',',  -- Separador de columnas CSV
    ROWTERMINATOR = '\n',   -- Salto de línea
    FIRSTROW = 1,           -- No hay headers
    TABLOCK                 -- IMPORTANTE: Permite carga paralela y minimiza log
);

-- Verificación rápida
SELECT TOP 5 * FROM Staging_Trips;
3

Anatomía del Desastre

Antes de limpiar, analicemos el costo de un mal diseño. ¿Cuánto espacio desperdicia usar VARCHAR para todo?

Diseño Staging

Pickup (Fecha) VARCHAR(50) ~ 25 bytes
Passengers VARCHAR(50) ~ 15 bytes
Total / Fila ~200 Bytes
Consecuencia: Caben pocas filas por página de 8KB. Lectura lenta.

Diseño Producción

Pickup (Fecha) DATETIME2(0) = 8 bytes
Passengers TINYINT = 1 byte
Total / Fila ~40 Bytes
Mejora: ¡500% más densidad de datos! Lectura ultra-rápida.
4

Transformación y Limpieza

Migramos de Staging a Producción. Aquí usamos TRY_CAST para manejar datos corruptos sin detener el proceso, y definimos un Clustered Index secuencial.

02_clean_architecture.sql
-- 1. Tabla Producción (Optimizada)
CREATE TABLE Trips (
    TripID INT IDENTITY(1,1) NOT NULL, -- Surrogate Key (Secuencial)
    VendorID TINYINT,
    Pickup DATETIME2(0),      -- Precisión de segundo
    Dropoff DATETIME2(0),
    PassengerCount TINYINT,   -- 1 byte (0-255)
    TripDistance DECIMAL(10,2),
    TotalAmount DECIMAL(10,2),
    
    -- El Clustered Index define el orden físico en disco.
    -- Usar IDENTITY evita la fragmentación (Page Splits).
    CONSTRAINT PK_Trips PRIMARY KEY CLUSTERED (TripID)
);

-- 2. ETL Intra-Base (Transformación)
INSERT INTO Trips (VendorID, Pickup, Dropoff, PassengerCount, TripDistance, TotalAmount)
SELECT 
    TRY_CAST(VendorID AS TINYINT),
    TRY_CAST(tpep_pickup_datetime AS DATETIME2(0)),
    TRY_CAST(tpep_dropoff_datetime AS DATETIME2(0)),
    -- Lógica de negocio: Si pasajeros es nulo/basura, asumir 1
    COALESCE(TRY_CAST(passenger_count AS TINYINT), 1),
    TRY_CAST(trip_distance AS DECIMAL(10,2)),
    TRY_CAST(total_amount AS DECIMAL(10,2))
FROM Staging_Trips
WHERE TRY_CAST(trip_distance AS DECIMAL(10,2)) > 0; -- Filtramos viajes inválidos

-- 3. La Prueba Final (Space Used)
EXEC sp_spaceused 'Staging_Trips';
EXEC sp_spaceused 'Trips'; 
-- ¡Deberías ver una reducción drástica en el tamaño 'Data'!

Has completado la Fase 1.

Datos cargados y optimizados.

Siguiente Misión

Estrategia de Índices