Módulo 4: Slowly Changing Dimensions

SCD tipo 3 y otras variantes, brevemente

Descripción

Las lecciones anteriores cubrieron los dos tipos de SCD que resuelven el 95% de los casos reales: tipo 1 (sobrescribe) y tipo 2 (historiza con filas nuevas). Esta lección cierra el vocabulario, brevemente, con SCD tipo 3 —que Kimball define como el tipo que "agrega un atributo nuevo en la dimensión para preservar el valor viejo del atributo; el valor nuevo sobrescribe el atributo principal, como en un cambio tipo 1"— y con dos nombres más que vale la pena reconocer aunque esta guía no los implemente a fondo: tipo 4 (mini-dimensión) y tipo 6 (híbrido). Vas a construir un ejemplo pequeño y ejecutado de tipo 3 sobre el mismo cambio de P002, y vas a terminar la lección sabiendo nombrar, con precisión, las cinco variantes del vocabulario Kimball de SCD — no solo las dos que implementaste a fondo.

Conexión con el módulo. Esta lección es deliberadamente breve —el diseño de esta guía la llama "brevemente" por una razón: tipo 3 tiene un uso mucho más limitado que tipo 1 y tipo 2 en la práctica ("se usa con relativa poca frecuencia", en palabras de la propia documentación de Kimball), y tipo 4 y tipo 6 son extensiones que esta guía nombra pero no construye—. La lección 8 no depende de nada de lo que aprendas aquí sobre tipo 3, 4 o 6; depende únicamente de dim_product_scd tal como la dejó la lección 6.

Una analogía: la libreta con una sola línea de "antes"

Piensa en un formulario de cambio de dirección con dos casillas: "dirección actual" y "dirección anterior". A diferencia del historial completo de un documento de identidad —que registra cada mudanza, sin límite—, este formulario solo tiene espacio para una dirección anterior. Si la persona se muda una segunda vez, la "dirección anterior" que había en la casilla se sobrescribe con la que era, hasta hace un momento, la "dirección actual" — y la mudanza de hace dos direcciones desaparece para siempre, sin dejar ningún rastro.

SCD tipo 3 es exactamente ese formulario de dos casillas. A diferencia de tipo 2, que agrega una fila nueva cada vez que algo cambia —sin límite de cuántas versiones puede acumular—, tipo 3 agrega una columna nueva (previous_category, por ejemplo) que guarda un solo valor anterior, en la misma fila. Es más simple de consultar que tipo 2 —nunca necesitas un JOIN ni un rango de fechas para saber "cuál era el valor anterior", está ahí mismo, en la fila—, pero paga ese costo con memoria limitada: solo recuerda un paso atrás, nunca la historia completa.

Ejemplo trabajado: SCD tipo 3 sobre P002

Construye dim_product_type3 con dos columnas adicionales —previous_category y previous_unit_cost— que guardan, exclusivamente, el valor inmediatamente anterior de cada atributo, más una columna changed_on que registra cuándo ocurrió ese único cambio recordado:

# scd_type3.py
import duckdb

con = duckdb.connect()
con.execute("""
    CREATE TABLE dim_product_type3 (
        product_id          VARCHAR,
        category            VARCHAR,
        previous_category   VARCHAR,
        unit_cost           DOUBLE,
        previous_unit_cost  DOUBLE,
        changed_on          DATE
    )
""")
con.execute("""
    INSERT INTO dim_product_type3 VALUES
        ('P001', 'beverages',   NULL, 0.40, NULL, NULL),
        ('P002', 'snacks',      NULL, 0.60, NULL, NULL),
        ('P003', 'beverages',   NULL, 0.35, NULL, NULL),
        ('P004', 'electronics', NULL, 2.10, NULL, NULL)
""")

print("=== dim_product_type3, ANTES del cambio (P002) ===")
print(con.sql("SELECT * FROM dim_product_type3 WHERE product_id = 'P002'"))

# El mismo cambio de siempre: category y unit_cost de P002, el 2026-08-15.
# A diferencia de tipo 2, NO se inserta una fila nueva -- se sobrescribe el valor
# principal, pero antes de sobrescribirlo, se copia a la columna "previous_*".
con.execute("""
    UPDATE dim_product_type3
    SET previous_category  = category,
        previous_unit_cost = unit_cost,
        category            = 'health-snacks',
        unit_cost            = 0.68,
        changed_on           = DATE '2026-08-15'
    WHERE product_id = 'P002'
""")

print("\n=== dim_product_type3, DESPUES del cambio (P002, sigue siendo UNA fila) ===")
print(con.sql("SELECT * FROM dim_product_type3 WHERE product_id = 'P002'"))

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

=== dim_product_type3, ANTES del cambio (P002) ===
┌────────────┬──────────┬───────────────────┬───────────┬────────────────────┬────────────┐
│ product_id │ category │ previous_category │ unit_cost │ previous_unit_cost │ changed_on │
│  varchar   │ varchar  │      varchar      │  double   │       double       │    date    │
├────────────┼──────────┼───────────────────┼───────────┼────────────────────┼────────────┤
│ P002       │ snacks   │ NULL              │       0.6 │               NULL │ NULL       │
└────────────┴──────────┴───────────────────┴───────────┴────────────────────┴────────────┘

=== dim_product_type3, DESPUES del cambio (P002, sigue siendo UNA fila) ===
┌────────────┬───────────────┬───────────────────┬───────────┬────────────────────┬────────────┐
│ product_id │   category    │ previous_category │ unit_cost │ previous_unit_cost │ changed_on │
│  varchar   │    varchar    │      varchar      │  double   │       double       │    date    │
├────────────┼───────────────┼───────────────────┼───────────┼────────────────────┼────────────┤
│ P002       │ health-snacks │ snacks            │      0.68 │                0.6 │ 2026-08-15 │
└────────────┴───────────────┴───────────────────┴───────────┴────────────────────┴────────────┘

P002 sigue teniendo exactamente una fila —a diferencia de dim_product_scd en la lección 5, que tiene dos—, pero esa única fila conserva snacks y 0.6 en previous_category/previous_unit_cost, junto al valor actual. Cualquier consulta que necesite "el valor actual y el inmediatamente anterior, sin JOIN" puede leerlos directamente de esta fila. Lo que esta tabla no puede responder es la pregunta que sí resuelve tipo 2: si P002 cambiara una tercera vez, previous_category se sobrescribiría con health-snacks, y snacks —el valor original— desaparecería sin dejar rastro, exactamente el mismo límite que la analogía del formulario de dos casillas ya adelantó.

Diagrama: las cinco variantes del vocabulario Kimball, en una tabla

Tipo  Nombre                 Que hace                                    Cuantas versiones guarda
────  ─────────────────────  ──────────────────────────────────────────  ─────────────────────────
1     Overwrite              Sobrescribe el valor en la misma fila        1 (solo la actual)
2     Add new row            Agrega una fila nueva, con valid_from/       Todas, sin limite
                              valid_to/is_current
3     Add new attribute      Agrega una columna "previous_*" en la        2 (actual + 1 anterior)
                              misma fila
4     Mini-dimension         Separa atributos de cambio rapido en         Todas (en la mini-dimension,
                              una tabla aparte, con su propia llave       no en la dimension base)
6     Hybrid (1+2+3)         Combina las tres tecnicas anteriores en      Todas, MAS el valor actual
                              la misma fila                               embebido en cada version
flowchart LR
    A["Tipo 1\nsobrescribe\n1 version"] --- B["Tipo 3\n+ 1 columna previous_*\n2 versiones"]
    B --- C["Tipo 2\n+ fila nueva\ntodas las versiones"]
    C --- D["Tipo 6\nhibrido 1+2+3\ntodas + actual embebido"]
    E["Tipo 4\nmini-dimension\naparte"]

Profundización: tipo 4 y tipo 6, nombrados sin implementar

SCD tipo 4 (mini-dimensión). Kimball lo describe así: "se usa cuando un grupo de atributos en una dimensión cambia rápidamente y se separa en una mini-dimensión". La idea: si dim_product_scd tuviera, además de category y unit_cost, un grupo de atributos que cambiaran constantemente —por ejemplo, un stock_level que se actualizara cada hora—, historizar esos atributos con tipo 2 haría crecer la dimensión principal sin control, una fila nueva por cada actualización de inventario. Tipo 4 resuelve esto separando ese grupo de atributos volátiles en una tabla aparte —una "mini-dimensión"—, con su propia llave sustituta, referenciada desde el hecho junto a la llave de la dimensión principal. Kiosko no tiene, en esta guía, ningún atributo tan volátil como para justificar una mini-dimensión —category y unit_cost cambian, como mucho, unas pocas veces— así que esta guía nombra tipo 4 sin construirlo.

SCD tipo 6 (híbrido). La definición de Kimball es precisa: "el tipo 6 construye sobre la técnica de tipo 2, embebiendo también versiones tipo 1 actuales de los mismos atributos en la fila de la dimensión, de forma que las filas del hecho puedan filtrarse o agruparse tanto por el valor del atributo tipo 2 vigente en el momento de la medición, como por el valor actual del atributo". En términos de Kiosko, esto significaría que cada fila histórica de dim_product_scd —incluyendo product_key = 2, la versión ya cerrada de P002— tendría, además de su propio category histórico (snacks), una columna adicional como current_category que siempre mostrara el valor más reciente (health-snacks), sin importar qué tan vieja sea esa fila. Esto permite responder dos preguntas distintas con la misma tabla: "¿qué categoría tenía este producto cuando se vendió?" (columna histórica) y "¿en qué categoría cae esta venta si la reclasifico con el catálogo de hoy?" (columna tipo 1 embebida) — el nombre "tipo 6" viene, según la convención informal de la industria, de que combina tipo 1 + tipo 2 + tipo 3 (1+2+3=6). Esta guía no lo implementa porque Kiosko, con un solo cambio de P002, no tiene todavía un caso de negocio que necesite ambas preguntas a la vez — pero vale la pena reconocerlo como el techo de complejidad de este vocabulario, para cuando el caso de negocio lo justifique.

La frontera con esta guía: el time travel nativo de un lakehouse. Todo lo que este módulo construyó —tipo 1, tipo 2, tipo 3— historiza a mano, con columnas que tú mismo diseñas y mantienes (valid_from, valid_to, is_current, previous_*). Existe una alternativa completamente distinta, que resuelve el mismo problema desde la capa de almacenamiento en vez de desde el modelo: los formatos de tabla de un lakehouse moderno —Apache Iceberg, Delta Lake— ofrecen time travel nativo: cada vez que una fila cambia, el propio formato de tabla conserva versiones anteriores del archivo completo, consultables con una sintaxis del motor (por ejemplo, SELECT * FROM tabla FOR TIMESTAMP AS OF '2026-08-10'), sin que el modelador tenga que diseñar ni una sola columna de vigencia. Es una forma genuinamente distinta de resolver el mismo problema —consultar el pasado sin perderlo—, no una implementación más del mismo patrón. Esta guía nombra esta alternativa una sola vez, aquí, sin implementarla: lakehouse-and-iceberg-guide, la guía hermana de este ecosistema, la construye a fondo.

Errores comunes

Confundir tipo 3 con "una versión más simple de tipo 2". Qué pasa: alguien implementa tipo 3 pensando que es un atajo hacia tipo 2, con la idea de "ya después migro a agregar más columnas previous_previous_* si hace falta más historia". Por qué pasa: tipo 3, con solo una columna adicional, se ve como un paso intermedio natural hacia tipo 2. Cómo detectarlo: si tu diseño de tipo 3 asume que vas a poder "escalarlo" agregando más y más columnas previous_* para guardar más pasos de historia, tienes esta confusión — cada columna adicional solo agrega un paso más, nunca una historia ilimitada, y el número de columnas que necesitarías crece sin límite si el atributo cambia con frecuencia. Cómo corregirlo: tipo 3 y tipo 2 son técnicas con propósitos distintos, no niveles de la misma escala — tipo 3 es correcto cuando de verdad solo interesa "el valor actual y el inmediatamente anterior" (por ejemplo, para comparar "antes/después" de un cambio puntual), nunca como sustituto de tipo 2 cuando la historia completa importa.

Implementar tipo 4 o tipo 6 "porque suenan más completos", sin un caso de negocio que los justifique. Qué pasa: alguien, después de leer sobre las cinco variantes, decide implementar el patrón más sofisticado disponible —tipo 6, el híbrido— para dim_product_scd, sin que Kiosko tenga ninguna pregunta de negocio que realmente lo necesite. Por qué pasa: es tentador asumir que "más capacidad" es siempre mejor, sin medir el costo de mantenimiento adicional. Cómo detectarlo: si no puedes nombrar, con precisión, la pregunta de negocio específica que tipo 6 resolvería y que tipo 2 no —"reclasificar ventas históricas con la categoría de hoy", en el ejemplo de esta lección—, no tienes un caso real, solo una preferencia por la complejidad. Cómo corregirlo: la misma disciplina de la lección 6 aplica aquí, a nivel de tabla completa: elige la variante más simple que resuelva la pregunta de negocio real que tienes hoy, y sube de complejidad solo cuando una pregunta nueva y concreta lo exija — nunca por adelantado, sin evidencia.

Pensar que el time travel de un lakehouse hace innecesario aprender SCD a mano. Qué pasa: alguien, al enterarse de que Iceberg o Delta Lake resuelven el mismo problema con time travel nativo, concluye que aprender SCD tipo 1/2/3 a mano es un esfuerzo desperdiciado, algo que ya no se hace en la práctica. Por qué pasa: es razonable suponer que la herramienta más moderna reemplaza por completo a la técnica manual. Cómo detectarlo: si asumes que ninguna empresa historiza dimensiones a mano hoy en día, subestimas cuántos warehouses de producción todavía corren sobre motores sin time travel nativo —o sobre tablas que sí lo tienen, pero donde el equipo de datos, de todas formas, decide construir columnas explícitas de vigencia por razones de portabilidad entre motores—. Cómo corregirlo: el time travel de un lakehouse resuelve el mismo problema desde otra capa, pero el concepto que sostiene ambas soluciones —preservar el pasado sin perderlo, distinguir la versión vigente de las históricas— es el mismo que este módulo enseñó. Entender SCD a mano primero es lo que te permite reconocer, con criterio, qué problema resuelve el time travel nativo cuando lo veas en lakehouse-and-iceberg-guide.

Ejercicios

Ejercicio 1 — Simula un segundo cambio de P002 en dim_product_type3, y observa qué se pierde. Supón que, el 2026-08-25, P002 cambia de costo otra vez, a 0.72, sin cambiar de categoría. Aplica ese cambio sobre dim_product_type3 con la misma técnica de esta lección, y verifica qué pasó con el valor 0.6 (el costo original, anterior al primer cambio).

Ver solución
con.execute("""
    UPDATE dim_product_type3
    SET previous_unit_cost = unit_cost,
        unit_cost           = 0.72,
        changed_on           = DATE '2026-08-25'
    WHERE product_id = 'P002'
""")
print(con.sql("SELECT * FROM dim_product_type3 WHERE product_id = 'P002'"))

Salida esperada:

┌────────────┬───────────────┬───────────────────┬───────────┬────────────────────┬────────────┐
│ product_id │   category    │ previous_category │ unit_cost │ previous_unit_cost │ changed_on │
│  varchar   │    varchar    │      varchar      │  double   │       double       │    date    │
├────────────┼───────────────┼───────────────────┼───────────┼────────────────────┼────────────┤
│ P002       │ health-snacks │ snacks            │      0.72 │               0.68 │ 2026-08-25 │
└────────────┴───────────────┴───────────────────┴───────────┴────────────────────┴────────────┘

previous_unit_cost ahora es 0.68 —el valor que era "actual" justo antes de este segundo cambio—, y 0.6 —el costo original, de antes del primer cambio— desapareció por completo, sin dejar ningún rastro en esta tabla. Compáralo con dim_product_scd (tipo 2) de la lección 5: ahí, un segundo cambio de P002 agrega una tercera fila, preservando las tres versiones completas. Esta es, en código, la limitación exacta que la analogía del formulario de dos casillas ya adelantó: tipo 3 solo recuerda un paso atrás, sin importar cuántos cambios reales hayan ocurrido antes de ese.

Ejercicio 2 — Nombra, de memoria, las cinco variantes de SCD y su idea central en una frase cada una. Sin mirar el diagrama de esta lección, escribe los nombres de tipo 1, 2, 3, 4 y 6, con una frase que capture la idea central de cada uno.

Ver solución

Tipo 1 (overwrite): sobrescribe el valor, sin historia. Tipo 2 (add new row): agrega una fila nueva por cada cambio, con vigencia. Tipo 3 (add new attribute): agrega una columna para el valor inmediatamente anterior, en la misma fila. Tipo 4 (mini-dimension): separa los atributos de cambio rápido en una tabla aparte. Tipo 6 (hybrid): combina 1, 2 y 3 en la misma fila, embebiendo tanto la historia completa como el valor actual. Fíjate en que no existe un "tipo 5" en este vocabulario estándar — la numeración de Kimball salta directo de 4 a 6, porque 6 es, deliberadamente, la suma de 1+2+3.

Ejercicio 3 — Argumenta cuándo elegirías tipo 3 en vez de tipo 2 para unit_cost de Kiosko. En 2-3 frases, describe un escenario de negocio hipotético donde tipo 3 —no tipo 2— sería la elección correcta para unit_cost, distinto del escenario de esta guía.

Ver solución

Si el único uso de negocio para el costo histórico de P002 fuera comparar, en un solo reporte, "el margen antes de la última renegociación con el proveedor" contra "el margen después" —sin necesidad de reconstruir toda la línea de tiempo de cambios de costo, solo el par inmediato antes/después—, tipo 3 sería suficiente y más simple de consultar que tipo 2: unit_cost y previous_unit_cost en la misma fila, sin JOIN ni rango de fechas. La guía eligió tipo 2 para Kiosko porque el módulo 5 necesita poder reconstruir revenue histórico correcto para cualquier fecha pasada —no solo comparar un antes/después puntual—, una pregunta que solo tipo 2, con su historia completa, puede responder con precisión.

Resumen y siguiente paso

Esta lección cerró el vocabulario de SCD: tipo 3 (una columna previous_*, un solo paso de memoria, ejecutado sobre P002), y los nombres de tipo 4 (mini-dimensión, para atributos de cambio rápido) y tipo 6 (híbrido, tipo 1+2+3 combinados), reconocidos sin implementar. Nombraste también, una vez, la alternativa de fondo que existe fuera de esta guía: el time travel nativo de un lakehouse, que resuelve el mismo problema desde la capa de almacenamiento en vez de con columnas manuales.

Antes de avanzar deberías poder: explicar la diferencia entre tipo 3 (memoria de un paso) y tipo 2 (memoria completa); nombrar las cinco variantes del vocabulario Kimball de SCD, con una frase por cada una; y nombrar la alternativa de time travel nativo y en qué guía hermana se construye a fondo.

La lección 8 integra todo el módulo en un solo pipeline: dim_product_scd construida desde cero, historizada con MERGE INTO corrido dos veces, verificada con exactamente dos versiones de P002 — el proyecto de cierre de este módulo.

Recursos