Módulo 4: Slowly Changing Dimensions

Presentación del módulo: cuando una dimensión cambia con el tiempo

Por qué existe este módulo

El módulo 3 cerró con una frase que quedó pendiente a propósito, en la última línea de su propio proyecto: "el módulo 4 necesita, como punto de partida, la tabla dim_product en su forma star —con category como columna de texto, product_key como llave sustituta— exactamente como quedó en el módulo 2 y sin cambios en este módulo". Esa tabla —cuatro productos, cuatro filas, ninguna versión histórica todavía— ya existe. Este módulo no la reemplaza. Le hace la pregunta que ningún módulo anterior de esta guía respondió todavía: ¿qué pasa el día que el precio o la categoría de un producto cambian de verdad?

Hasta ahora, esta guía trató dim_product como si fuera un dato fijo: cuatro filas que no cambian, útiles para unir contra fact_orders, comparar contra una versión normalizada (módulo 3) o denormalizada (también módulo 3), pero siempre las mismas cuatro filas. Esa suposición fue correcta mientras la estuviste usando —ningún producto de Kiosko cambió de precio ni de categoría en los tres módulos anteriores—, pero es, a la vez, la suposición menos realista de las que esta guía sostuvo hasta ahora. En un negocio real, los productos cambian: un proveedor renegocia el costo, un equipo de mercadeo reclasifica un producto en una categoría distinta, un catálogo se corrige. Y cuando eso pasa, la pregunta que un modelador dimensional tiene que responder no es "¿cuál es el valor correcto de este producto?" —esa pregunta tiene una respuesta obvia: el más reciente—, sino una mucho más difícil: "¿cuál era el valor correcto de este producto el día que se vendió?".

Este módulo responde esa pregunta con dos técnicas complementarias, ambas con nombre formal en el vocabulario de Ralph Kimball: SCD tipo 1 (Slowly Changing Dimension tipo 1), que sobrescribe el valor viejo con el nuevo y deliberadamente pierde la historia, porque a veces perderla es exactamente lo correcto; y SCD tipo 2, que historiza el cambio agregando una fila nueva a la dimensión, marcada con un rango de fechas de vigencia (valid_from, valid_to) y un indicador de cuál versión es la actual (is_current), preservando ambas versiones —la vieja y la nueva— como filas separadas, para siempre. Vas a implementar SCD-2 de verdad, con el statement MERGE INTO de DuckDB, sobre dos instantáneas reales del catálogo de productos de Kiosko —una antes del cambio, otra después—, y vas a terminar el módulo sabiendo, con criterio y no por costumbre, cuándo cada tipo de historización es la decisión correcta para una columna específica.

Conexión con el módulo. Este módulo no toca fact_orders —eso es la lección 5 del módulo 5, cuando ya exista una dimensión historizada contra la cual unir el hecho correctamente—. Lo que construye es una tabla nueva, dim_product_scd, que convive con dim_product (la versión star, sin cambios, que el resto de la guía sigue usando cuando no hace falta historia) y demuestra, con un solo producto real de Kiosko cambiando de precio y de categoría el mismo día, la diferencia completa entre sobrescribir y historizar.

Una analogía: el historial de direcciones de un documento de identidad

Piensa en un documento de identidad que registra tu dirección —una cédula, un DNI, una licencia de conducir—. Cuando te mudas de casa, ¿qué hace la institución que emite ese documento? No borra tu dirección anterior y la reemplaza en silencio, como si nunca hubieras vivido ahí. Lo que hace, en el mejor de los sistemas administrativos, es exactamente lo contrario: marca la dirección anterior como vencida, con una fecha exacta de hasta cuándo fue válida, y registra la dirección nueva con su propia fecha de inicio de vigencia. Si alguna autoridad necesita saber dónde vivías el 15 de marzo del año pasado —para una notificación legal, para una auditoría, para reconstruir un historial—, el sistema puede responder con precisión, porque nunca sobrescribió el dato: lo historizó.

Eso es, exactamente, lo que SCD tipo 2 hace con una fila de dim_product. Cuando el costo o la categoría de un producto cambian, la fila vieja no desaparece —se marca como vencida, con una fecha de expiración (valid_to) y un indicador de que ya no es la versión vigente (is_current = false)—, y se agrega una fila nueva, con su propia fecha de inicio (valid_from) y marcada como la versión actual (is_current = true). Cualquier pregunta futura sobre "¿cuál era el costo de este producto en tal fecha?" tiene una respuesta exacta, verificable, exactamente como el historial de direcciones de un documento de identidad. SCD tipo 1, en cambio, es el sistema administrativo que sí borra la dirección anterior sin dejar rastro: rápido, simple, pero incapaz de responder cualquier pregunta sobre el pasado.

Ejemplo trabajado: el mapa de este módulo, antes de construirlo

Antes de tocar datos reales de Kiosko, vale la pena ver, de un vistazo, qué construye cada lección y cómo se relacionan entre sí —el mismo tipo de mapa que abrió el módulo 3 antes de comparar star, snowflake y OBT.

# scd_module_map.py
CONCEPTS = [
    ("SCD tipo 1", "Sobrescribe el valor en la misma fila. Rapido, pero pierde la historia por completo."),
    ("SCD tipo 2", "Agrega una fila nueva con valid_from/valid_to/is_current. Conserva cada version, para siempre."),
    ("MERGE INTO", "El statement de DuckDB que cierra la fila vieja y abre la fila nueva en un flujo repetible."),
]

LESSONS = [
    ("El problema: dim_product no es estatica", "Dos snapshots de products, el cambio real de P002 declarado"),
    ("SCD tipo 1: sobrescribe y pierde historia", "UPDATE en el lugar, EJECUTADO"),
    ("SCD tipo 2: historiza con valid_from/valid_to", "Dos statements manuales (UPDATE + INSERT), EJECUTADO"),
    ("Implementando SCD tipo 2 con MERGE INTO", "MERGE INTO real, corrido dos veces, EJECUTADO"),
    ("Eligiendo tipo 1 vs tipo 2 por columna", "product_name (tipo 1) vs category/unit_cost (tipo 2), EJECUTADO"),
    ("SCD tipo 3 y otras variantes, brevemente", "previous_category EJECUTADO; tipo 4 y tipo 6 nombrados"),
    ("Proyecto: dim_product historizada de Kiosko", "El pipeline completo de las 6 lecciones anteriores, EJECUTADO"),
]

print("=== Los tres conceptos centrales de este modulo ===\n")
for name, description in CONCEPTS:
    print(f"- {name}")
    print(f"  {description}\n")

print("=== Las siete lecciones que construyen sobre ellos ===\n")
for i, (name, description) in enumerate(LESSONS, start=2):
    print(f"L{i}. {name}")
    print(f"    {description}\n")

Qué esperar. Al correr python3 scd_module_map.py, la salida es exactamente esta:

=== Los tres conceptos centrales de este modulo ===

- SCD tipo 1
  Sobrescribe el valor en la misma fila. Rapido, pero pierde la historia por completo.

- SCD tipo 2
  Agrega una fila nueva con valid_from/valid_to/is_current. Conserva cada version, para siempre.

- MERGE INTO
  El statement de DuckDB que cierra la fila vieja y abre la fila nueva en un flujo repetible.

=== Las siete lecciones que construyen sobre ellos ===

L2. El problema: dim_product no es estatica
    Dos snapshots de products, el cambio real de P002 declarado

L3. SCD tipo 1: sobrescribe y pierde historia
    UPDATE en el lugar, EJECUTADO

L4. SCD tipo 2: historiza con valid_from/valid_to
    Dos statements manuales (UPDATE + INSERT), EJECUTADO

L5. Implementando SCD tipo 2 con MERGE INTO
    MERGE INTO real, corrido dos veces, EJECUTADO

L6. Eligiendo tipo 1 vs tipo 2 por columna
    product_name (tipo 1) vs category/unit_cost (tipo 2), EJECUTADO

L7. SCD tipo 3 y otras variantes, brevemente
    previous_category EJECUTADO; tipo 4 y tipo 6 nombrados

L8. Proyecto: dim_product historizada de Kiosko
    El pipeline completo de las 6 lecciones anteriores, EJECUTADO

Todavía ningún producto real de Kiosko cambió de precio en este mapa —eso empieza en la lección 2—. Pero fíjate en el orden: primero se plantea el problema con evidencia (lección 2), después se muestra la solución ingenua que falla (lección 3), después se construye la solución correcta a mano, statement por statement, para entender exactamente qué hace (lección 4), después se automatiza esa misma solución con la herramienta correcta para el trabajo (lección 5), después se agrega criterio —no todo necesita historizarse igual (lección 6)—, después se nombran brevemente las variantes que existen más allá de tipo 1 y tipo 2 (lección 7), y solo al final se integra todo en el proyecto de cierre (lección 8). Ninguna lección salta directo a MERGE INTO sin haber entendido primero, a mano, qué problema resuelve.

Diagrama: dónde estabas, dónde vas a estar

flowchart LR
    subgraph M3["Modulo 3 (ya escrito)"]
        A["dim_product star\nproduct_key, product_id,\nproduct_name, category, unit_cost\n4 filas, sin historia"]
    end

    subgraph M4["Este modulo (4 de 8)"]
        B["L2: El problema\nP002 cambia category y unit_cost"]
        C["L3: SCD tipo 1\nsobrescribe, EJECUTADO"]
        D["L4: SCD tipo 2 manual\nvalid_from/valid_to/is_current"]
        E["L5: MERGE INTO\nEJECUTADO 2 veces"]
        F["L6: Tipo 1 vs tipo 2\npor columna, EJECUTADO"]
        G["L7: Tipo 3 y variantes\nbrevemente"]
        H["L8: dim_product_scd\ncompleta, EJECUTADO"]
    end

    subgraph M5["Modulo 5 (siguiente)"]
        I["Join punto-en-el-tiempo\ncontra dim_product_scd"]
    end

    A --> B --> C --> D --> E --> F --> G --> H --> I

El mapa de este módulo

Leccion   Que construye
────────  ──────────────────────────────────────────────────────────────
L1        (esta) El mapa: los tres conceptos, antes de construirlos
L2        El problema: dim_product no es estatica, EJECUTADO
L3        SCD tipo 1: sobrescribe y pierde historia, EJECUTADO
L4        SCD tipo 2: historiza a mano, con UPDATE + INSERT, EJECUTADO
L5        SCD tipo 2 con MERGE INTO, corrido 2 veces, EJECUTADO
L6        Tipo 1 vs tipo 2 por columna, EJECUTADO
L7        Tipo 3 y otras variantes, brevemente, EJECUTADO en parte
L8        Proyecto: dim_product_scd completa de Kiosko, EJECUTADO

Las lecciones 3, 4 y 5 son la columna vertebral ejecutable del módulo: la misma pregunta —¿qué le pasa a dim_product cuando P002 cambia de precio y de categoría el 15 de agosto de 2026?— respondida tres veces, con tres herramientas distintas, para que entiendas no solo que MERGE INTO es la forma correcta de hacerlo, sino por qué las alternativas más simples no alcanzan. La lección 6 le da criterio a la lección 5: no toda columna necesita el mismo tratamiento. La lección 7 abre la puerta, brevemente, a lo que existe más allá de tipo 1 y tipo 2, sin construirlo a fondo. La lección 8 cierra con el proyecto: el pipeline completo, de punta a punta, verificado.

Profundización: por qué este módulo necesita el star ya construido

Podría parecer que la historización de una dimensión es un tema independiente, que podría enseñarse con cualquier tabla de ejemplo, sin depender de los tres módulos anteriores de esta guía. Este módulo resiste esa tentación por dos razones concretas.

La primera es la llave sustituta. El módulo 2, lección 3, ya adelantó —sin poder demostrarlo todavía— por qué product_key se genera con ROW_NUMBER() OVER (ORDER BY product_id) en vez de un identificador aleatorio: "cuando dim_product empiece a versionar sus filas... product_id deja de identificar una única fila de la dimensión... product_key va a identificar una versión específica de un producto". Ese momento llegó. Sin haber entendido primero qué es una llave sustituta y por qué se separa de la llave natural, la primera pregunta que surge al ver dim_product_scd con dos filas para P002 sería genuinamente confusa: ¿por qué hay dos filas con el mismo product_id? El módulo 2 ya respondió esa pregunta, con dos módulos de anticipación.

La segunda razón es más práctica: este módulo necesita un catálogo de productos real, ya construido y verificado, sobre el cual aplicar el cambio. DIM_PRODUCT —cuatro productos, definidos desde el módulo 1, reutilizados sin cambios en los módulos 2 y 3— es exactamente esa base. No hay que inventar un catálogo nuevo para esta lección: el mismo P002 (Energy Bar, categoría snacks, costo 0.60) que ya conoces desde el primer módulo de esta guía es el producto que va a cambiar de categoría y de costo en este módulo. Nada nuevo que aprender sobre el catálogo — toda la novedad está en qué le pasa quando cambia.

Errores comunes

Pensar que este módulo modifica dim_product directamente. Qué pasa: alguien, al leer "historización de dim_product", asume que este módulo va a alterar la tabla dim_product del módulo 2 —agregarle columnas valid_from/valid_to/is_current, convertirla en la versión historizada—. Por qué pasa: el nombre del módulo (slowly-changing-dimensions) y el hecho de que el producto que cambia sea uno que ya conoces desde el módulo 1 sugieren, de forma razonable pero incorrecta, una modificación en el lugar. Cómo detectarlo: si al terminar este módulo esperas que dim_product (la tabla del módulo 2, sin sufijo) tenga columnas de vigencia, tienes esta confusión. Cómo corregirlo: este módulo construye una tabla nueva, dim_product_scd, separada de dim_product. dim_product sigue existiendo, sin cambios, como la versión star que el resto de la guía usa cuando no hace falta historia — exactamente la misma disciplina que el módulo 3 ya estableció con dim_product_normalized y mart_daily_sales_obt: estructuras nuevas y paralelas, no reemplazos del original.

Asumir que fact_orders también necesita historizarse. Qué pasa: alguien, al ver que una dimensión puede cambiar con el tiempo, se pregunta si fact_orders —el hecho— también necesita valid_from/valid_to. Por qué pasa: el vocabulario de "historizar" suena, a primera escucha, como algo que podría aplicarse a cualquier tabla. Cómo detectarlo: si te encuentras diseñando columnas de vigencia para fact_orders, perdiste la distinción central entre hecho y dimensión que el módulo 1, lección 6, ya estableció con precisión. Cómo corregirlo: un hecho registra un evento que ya ocurrió, en un instante fijo (order_ts) — no cambia después de registrado, y por lo tanto no necesita historizarse. Lo que cambia con el tiempo es el contexto descriptivo alrededor del evento —el costo o la categoría de un producto, no la venta en sí—, y ese contexto vive en las dimensiones. SCD tipo 1 y tipo 2 son técnicas de dimensión, exclusivamente.

Saltar directo a MERGE INTO sin entender el problema que resuelve. Qué pasa: alguien, impaciente por llegar al statement "real" de producción, quiere copiar la sintaxis de MERGE INTO de la lección 5 sin pasar por las lecciones 2, 3 y 4 que construyen el problema y la solución manual primero. Por qué pasa: MERGE INTO se ve como "la respuesta", y las lecciones anteriores se sienten como preámbulo prescindible. Cómo detectarlo: si en la lección 5 no puedes explicar, sin mirar, por qué la solución de la lección 3 (SCD tipo 1) pierde información que la lección 4 (SCD tipo 2 manual) preserva, te faltó el fundamento que hace que MERGE INTO tenga sentido. Cómo corregirlo: las lecciones 3 y 4 no son relleno — son la evidencia, construida a mano, de exactamente qué problema MERGE INTO automatiza en la lección 5. Sin haberlo hecho a mano una vez, la sintaxis de MERGE INTO es memorización sin comprensión.

Ejercicios

Ejercicio 1 — Recuerda la fila exacta del checklist que este módulo resuelve. Sin releer el proyecto del módulo 3, escribe de memoria el nombre exacto de la fila del checklist (introducido en el módulo 1, lección 2) que corresponde a este módulo.

Ver solución

La fila dice, textualmente: "Historización de una dimensión que cambia (SCD)". A diferencia del módulo 3 —que comparó tres formas de modelar sin cambiar ningún dato—, este módulo resuelve una fila que depende de que un valor real cambie: sin un cambio de precio o de categoría que historizar, no hay nada que demostrar. Por eso este módulo elige, de forma explícita, un producto y un cambio concreto —P002, categoría y costo, el 15 de agosto de 2026— en vez de razonar en abstracto.

Ejercicio 2 — Explica, en tus propias palabras, la diferencia entre dim_product y dim_product_scd. Sin mirar las lecciones siguientes, escribe 2-3 frases explicando qué tabla existe hoy en esta guía, qué tabla va a construir este módulo, y por qué van a convivir en vez de que una reemplace a la otra.

Ver solución

dim_product es la tabla que construyó el módulo 2 y que el módulo 3 usó sin cambios: cuatro productos, una fila cada uno, con product_key como llave sustituta pero sin ninguna noción de vigencia en el tiempo — es la versión "star" canónica que el resto de esta guía sigue usando cuando no hace falta historia. dim_product_scd es una tabla nueva que este módulo va a construir, con las mismas columnas de negocio (product_id, product_name, category, unit_cost) más tres columnas de historización (valid_from, valid_to, is_current), capaz de guardar más de una fila por product_id cuando un producto cambia. Las dos tablas conviven porque resuelven necesidades distintas: dim_product para cuando el reporte no necesita saber "cuál era el valor en tal fecha", dim_product_scd para cuando sí.

Ejercicio 3 — Predice qué pasaría si Kiosko nunca cambiara ningún producto. En 2-3 frases, explica si dim_product_scd, construida sobre un catálogo que nunca cambia, tendría alguna diferencia visible frente a dim_product.

Ver solución

Si ningún producto de Kiosko cambiara nunca, dim_product_scd tendría exactamente las mismas cuatro filas que dim_product —una por producto—, con valid_from fijo en la fecha de carga inicial, valid_to siempre NULL y is_current siempre true. La diferencia sería solo estructural (tres columnas adicionales, sin usar todavía), no de datos. Esto confirma que la historización no es un costo que se paga siempre: una dimensión que de verdad no cambia nunca "cuesta" tres columnas extra sin ningún beneficio visible — la ganancia real aparece, precisamente, el día que algo cambia, como le va a pasar a P002 en la lección 2.

Resumen y siguiente paso

Este módulo toma dim_product tal como la dejó el módulo 3 —cuatro productos, sin historia— y le hace la pregunta que ninguna lección anterior de esta guía respondió: ¿qué pasa cuando el precio o la categoría de un producto cambian de verdad? Vas a responderla dos veces —sobrescribiendo con SCD tipo 1, historizando con SCD tipo 2— y vas a terminar implementando SCD-2 de producción con MERGE INTO, sobre un cambio real y verificado de Kiosko: P002 (Energy Bar), categoría y costo, el 15 de agosto de 2026.

Antes de avanzar deberías poder: nombrar los tres conceptos centrales de este módulo (tipo 1, tipo 2, MERGE INTO) y qué hace cada uno; explicar por qué este módulo construye dim_product_scd como tabla nueva en vez de modificar dim_product; y decir de memoria cuál es la fila exacta del checklist del módulo 1 que este módulo resuelve.

La lección 2 abre el problema con evidencia: dos instantáneas reales del catálogo de Kiosko, products_v1 y products_v2, comparadas columna por columna para encontrar exactamente qué cambió, y en qué producto.

Recursos