Módulo 4: Slowly Changing Dimensions
SCD tipo 1: sobrescribe y pierde historia
Descripción
Esta lección construye la solución más simple posible al problema que declaró la lección 2: cuando P002 cambia de categoría y de costo, sobrescribe la fila existente con los valores nuevos. Esa es, formalmente, la técnica que Ralph Kimball llama SCD tipo 1 (Slowly Changing Dimension tipo 1): "el valor viejo del atributo en la fila de la dimensión se sobrescribe con el valor nuevo". Vas a implementarla con un solo UPDATE, vas a confirmar que funciona —dim_product_type1 refleja, correctamente, el estado actual de P002—, y vas a confirmar también su costo real: una vez aplicado el UPDATE, no existe ninguna consulta, por ingeniosa que sea, capaz de recuperar los valores snacks y 0.60.
Conexión con el módulo. Esta lección no es un error de diseño para evitar a toda costa — es una técnica legítima, con su propio lugar, y esta lección lo deja claro con evidencia: la SCD tipo 1 es exactamente correcta cuando el cambio es una corrección (un error de captura, un typo) y no un hecho de negocio que alguien vaya a necesitar auditar más adelante. La lección 4 va a construir, sobre el mismo cambio de P002, la alternativa que sí preserva la historia — y la lección 6 va a formalizar el criterio exacto para elegir entre ambas, columna por columna.
Una analogía: borrar el pizarrón
Piensa en un pizarrón donde alguien anota, cada semana, el precio vigente de un producto para que el equipo de ventas lo consulte rápido. Cuando el precio cambia, la persona encargada borra el número viejo y escribe el nuevo — nadie espera que el pizarrón conserve un historial de todos los precios que tuvo alguna vez; su único trabajo es mostrar el valor actual, de la forma más simple y rápida de actualizar posible. Si alguien pregunta "¿cuál era el precio hace un mes?", la respuesta honesta es "no tengo cómo saberlo" — y en muchos contextos, esa respuesta es perfectamente aceptable, porque nadie diseñó el pizarrón para responder esa pregunta.
SCD tipo 1 es exactamente ese pizarrón. Es rápido de actualizar —un solo UPDATE, sin filas nuevas, sin columnas de vigencia que mantener—, y es la elección correcta cuando el valor "actual" es todo lo que cualquier consumidor del dato necesita. El costo, igual de real, es que el pizarrón borrado no puede reconstruirse: una vez que el valor viejo desaparece, desaparece para siempre.
Ejemplo trabajado: sobrescribiendo P002 con SCD tipo 1
Construye dim_product_type1 con las cuatro columnas de negocio de siempre —sin ninguna columna de vigencia, porque SCD tipo 1, por definición, no las necesita—, y aplica el cambio de P002 con un UPDATE directo:
# scd_type1.py
import duckdb
from kiosko import DIM_PRODUCT
con = duckdb.connect()
con.execute("""
CREATE TABLE dim_product_type1 (
product_key INTEGER,
product_id VARCHAR,
product_name VARCHAR,
category VARCHAR,
unit_cost DOUBLE
)
""")
con.executemany(
"INSERT INTO dim_product_type1 VALUES (?, ?, ?, ?, ?)",
[(i + 1, p["product_id"], p["product_name"], p["category"], p["unit_cost"])
for i, p in enumerate(DIM_PRODUCT)],
)
print("=== dim_product_type1, ANTES del cambio (vigente hasta el 2026-08-14) ===")
print(con.sql("SELECT * FROM dim_product_type1 WHERE product_id = 'P002'"))
# El cambio real de P002, efectivo el 2026-08-15: SCD tipo 1 lo aplica con un solo UPDATE,
# sobre la MISMA fila -- sin insertar ninguna fila nueva.
con.execute("""
UPDATE dim_product_type1
SET category = 'health-snacks', unit_cost = 0.68
WHERE product_id = 'P002'
""")
print("=== dim_product_type1, DESPUES del cambio (vigente desde el 2026-08-15) ===")
print(con.sql("SELECT * FROM dim_product_type1 WHERE product_id = 'P002'"))
print("=== Intentando recuperar category/unit_cost de ANTES del 2026-08-15 ===")
print(con.sql("SELECT DISTINCT category, unit_cost FROM dim_product_type1 WHERE product_id = 'P002'"))
Qué esperar. Al correr python3 scd_type1.py, la salida es exactamente esta:
=== dim_product_type1, ANTES del cambio (vigente hasta el 2026-08-14) ===
┌─────────────┬────────────┬──────────────┬──────────┬───────────┐
│ product_key │ product_id │ product_name │ category │ unit_cost │
│ int32 │ varchar │ varchar │ varchar │ double │
├─────────────┼────────────┼──────────────┼──────────┼───────────┤
│ 2 │ P002 │ Energy Bar │ snacks │ 0.6 │
└─────────────┴────────────┴──────────────┴──────────┴───────────┘
=== dim_product_type1, DESPUES del cambio (vigente desde el 2026-08-15) ===
┌─────────────┬────────────┬──────────────┬───────────────┬───────────┐
│ product_key │ product_id │ product_name │ category │ unit_cost │
│ int32 │ varchar │ varchar │ varchar │ double │
├─────────────┼────────────┼──────────────┼───────────────┼───────────┤
│ 2 │ P002 │ Energy Bar │ health-snacks │ 0.68 │
└─────────────┴────────────┴──────────────┴───────────────┴───────────┘
=== Intentando recuperar category/unit_cost de ANTES del 2026-08-15 ===
┌───────────────┬───────────┐
│ category │ unit_cost │
│ varchar │ double │
├───────────────┼───────────┤
│ health-snacks │ 0.68 │
└───────────────┴───────────┘
Detente en la última consulta, porque es la evidencia central de esta lección: SELECT DISTINCT category, unit_cost debería, si la historia estuviera preservada, devolver dos filas —la versión antes y la versión después—. Devuelve una — health-snacks, 0.68 —, porque solo existe una fila física para P002, y esa fila ya fue sobrescrita. No hay ninguna consulta SQL, sin importar cuán compleja, capaz de recuperar snacks y 0.60 de esta tabla: el UPDATE no dejó ningún rastro de lo que había antes. Esto no es un error del código — es exactamente lo que SCD tipo 1 promete y cumple: la fila más reciente, sin memoria del pasado.
Diagrama: una fila, dos momentos, un solo valor visible
flowchart LR
subgraph Antes["2026-08-01 al 2026-08-14"]
A["product_key: 2\nproduct_id: P002\ncategory: snacks\nunit_cost: 0.60"]
end
subgraph Despues["2026-08-15 en adelante"]
B["product_key: 2\nproduct_id: P002\ncategory: health-snacks\nunit_cost: 0.68"]
end
A -->|"UPDATE ... SET category = ..., unit_cost = ...\n(misma fila, mismo product_key)"| B
A -.->|"snacks / 0.60 -- IRRECUPERABLE"| X["??? "]
style X fill:#00000000,stroke-dasharray: 5 5
Profundización: cuándo SCD tipo 1 es la decisión correcta, no un atajo
Es tentador leer esta lección como "la forma incorrecta de hacerlo", y esperar que la lección 4 la reemplace por completo. Eso sería un error de interpretación. SCD tipo 1 no es una versión incompleta de SCD tipo 2 — es una técnica con su propio caso de uso legítimo, y Kimball la documenta con el mismo nivel de formalidad que las demás. La pregunta correcta no es "¿tipo 1 o tipo 2?" en abstracto, sino "¿este cambio específico merece un rastro auditable, o es simplemente la corrección del valor vigente?".
Piensa en la diferencia entre dos escenarios, ambos posibles para P002. Escenario A —el que esta lección y la lección 4 modelan—: Kiosko renegocia el costo con su proveedor y reposiciona el producto en una categoría nueva. Es un hecho de negocio real, con una fecha exacta, que un analista de márgenes podría necesitar consultar más adelante ("¿cuánto costaba P002 antes de la renegociación de agosto?"). Ese escenario justifica SCD tipo 2. Escenario B —que esta lección no modela, pero que es igual de común en la práctica—: alguien detecta que product_name tenía un error de tipeo desde el principio —digamos, un espacio de más o una mayúscula incorrecta en el sistema de origen— y lo corrige. Ese no es un "cambio" en el sentido de negocio; es una corrección de un dato que siempre debió ser el correcto. Nadie necesita un rastro de "cómo se veía el error" — de hecho, preservarlo como una versión histórica sería confuso, porque sugeriría que el nombre erróneo fue, en algún momento, un valor de negocio válido. Ese escenario es exactamente para lo que existe SCD tipo 1, y la lección 6 vuelve sobre esta distinción con criterio explícito, columna por columna.
La ventaja operativa de SCD tipo 1, además, es real y no debe subestimarse: una tabla sin historización es más simple de mantener, ocupa menos espacio (una fila por entidad, no una por versión), y cualquier consulta contra ella es trivialmente correcta —no hace falta preocuparse por unir con la versión vigente en un momento específico, porque solo existe una versión—. El costo de SCD tipo 2 —que la lección 4 va a hacer explícito— no es gratis: cada columna historizada agrega complejidad a cada consulta que la usa.
Errores comunes
Usar SCD tipo 1 para un cambio que sí necesita auditoría, "porque es más simple". Qué pasa: un equipo, presionado por tiempo, decide sobrescribir en el lugar un cambio de precio o de categoría real —como el de P002 en esta lección— sin darse cuenta de que, meses después, alguien va a necesitar reconstruir el margen histórico y no va a poder. Por qué pasa: SCD tipo 1 es genuinamente más fácil de implementar al principio, y el costo de no tener historia solo se hace visible cuando alguien la necesita y descubre que no existe. Cómo detectarlo: si tu equipo de análisis financiero o de reportes hace preguntas del tipo "¿cuánto costaba esto en tal fecha?" y la respuesta siempre es "no lo sabemos, se sobrescribió", tienes exactamente este problema. Cómo corregirlo: antes de elegir tipo 1 para una columna, pregúntate explícitamente si alguna vez alguien va a necesitar el valor histórico — no "es poco probable", sino "puedo garantizar que nunca hace falta". Si no puedes garantizarlo, usa tipo 2 — revertir un tipo 1 a tipo 2 después de haber perdido historia real es, literalmente, imposible.
Creer que agregar una columna updated_at a dim_product_type1 "arregla" la pérdida de historia. Qué pasa: alguien, incómodo con perder por completo el rastro del cambio, agrega una columna updated_at que registra cuándo fue la última modificación, y asume que eso es suficiente auditoría. Por qué pasa: updated_at sí responde "¿cuándo cambió por última vez?", lo que se siente como progreso. Cómo detectarlo: si intentas responder "¿cuál era el valor antes de esa fecha de actualización?" usando solo updated_at, te das cuenta de que la pregunta sigue sin respuesta — updated_at te dice cuándo cambió algo, nunca a qué cambió desde. Cómo corregirlo: updated_at es útil para auditoría técnica (saber que algo cambió), pero no sustituye la historización de negocio (saber los valores completos de cada versión). Eso es, precisamente, lo que agrega SCD tipo 2 en la lección siguiente: no solo una fecha, sino la fila completa de la versión anterior.
Aplicar el UPDATE sobre la columna equivocada, sin verificar el WHERE primero. Qué pasa: alguien escribe un UPDATE dim_product_type1 SET category = 'health-snacks', unit_cost = 0.68 sin la cláusula WHERE product_id = 'P002', y sobrescribe la categoría y el costo de los cuatro productos, no solo del que cambió. Por qué pasa: es un error de omisión fácil de cometer bajo presión, y en SCD tipo 1 —sin historia que consultar después— el error pasa desapercibido hasta que alguien nota que P001 también tiene, incorrectamente, category = 'health-snacks'. Cómo detectarlo: después de cualquier UPDATE sobre una dimensión, verifica el conteo de filas afectadas y compáralo contra lo esperado — un UPDATE sobre una sola fila que reporta cuatro filas modificadas es una señal inmediata de alarma. Cómo corregirlo: siempre verifica el resto de la tabla después de un UPDATE, no solo la fila que cambiaste — SELECT * FROM dim_product_type1 WHERE product_id != 'P002' debería seguir mostrando los valores originales de P001, P003 y P004, exactamente como en el ejercicio 1 de esta lección.
Ejercicios
Ejercicio 1 — Verifica que P001, P003 y P004 no fueron afectados por el UPDATE. Después de correr el ejemplo trabajado, escribe una consulta que confirme que los otros tres productos conservan sus valores originales de category y unit_cost.
Ver solución
print(con.sql("""
SELECT product_id, category, unit_cost
FROM dim_product_type1
WHERE product_id != 'P002'
ORDER BY product_id
"""))
Salida esperada:
┌────────────┬─────────────┬───────────┐
│ product_id │ category │ unit_cost │
│ varchar │ varchar │ double │
├────────────┼─────────────┼───────────┤
│ P001 │ beverages │ 0.4 │
│ P003 │ beverages │ 0.35 │
│ P004 │ electronics │ 2.1 │
└────────────┴─────────────┴───────────┘
Los tres productos conservan exactamente sus valores del módulo 1 — beverages/0.40 para P001, beverages/0.35 para P003, electronics/2.10 para P004 —, confirmando que el UPDATE de esta lección afectó únicamente la fila de P002, tal como lo especificó su cláusula WHERE.
Ejercicio 2 — Cuenta cuántas filas tiene dim_product_type1 después del cambio, y explica por qué ese número no cambió. Sin mirar la tabla directamente, predice el resultado de SELECT COUNT(*) FROM dim_product_type1 después del UPDATE, y explica en 1-2 frases por qué ese número es idéntico al de antes del cambio.
Ver solución
print(con.sql("SELECT COUNT(*) AS total_rows FROM dim_product_type1"))
Salida esperada:
┌────────────┐
│ total_rows │
│ int64 │
├────────────┤
│ 4 │
└────────────┘
total_rows sigue siendo 4, exactamente el mismo número que antes del cambio de P002. Esto es, precisamente, la firma de SCD tipo 1: el número de filas de la dimensión nunca crece por un cambio de atributo, porque cada cambio se aplica sobre una fila existente, nunca agrega una fila nueva. Compáralo con lo que vas a ver en la lección 4: ahí, el mismo cambio de P002 sí va a hacer crecer el conteo de filas, de 4 a 5.
Ejercicio 3 — Argumenta si product_name debería historizarse igual que category y unit_cost. Supón que, en algún punto futuro, Kiosko decide renombrar P002 de "Energy Bar" a "Energy Bar Max" como parte de un relanzamiento de marca. En 2-3 frases, argumenta si ese cambio debería tratarse con SCD tipo 1 o SCD tipo 2, usando el criterio de la profundización de esta lección.
Ver solución
Depende de si el nombre anterior importa para algún análisis futuro. Si "Energy Bar Max" es simplemente el nombre nuevo y nadie va a necesitar nunca saber "cómo se llamaba este producto antes del relanzamiento" para un reporte de negocio, es un caso razonable de SCD tipo 1 — el nombre es, en esencia, un dato descriptivo sin implicación financiera. Pero si el relanzamiento de marca es, en sí mismo, un evento de negocio que el equipo de mercadeo quiere poder auditar más adelante —por ejemplo, para medir si el cambio de nombre afectó las ventas—, entonces sí merece SCD tipo 2, porque el valor histórico del nombre se vuelve parte de la pregunta de negocio que alguien va a hacer. La lección 6 de este módulo formaliza esta misma decisión, columna por columna, para el catálogo completo de Kiosko.
Resumen y siguiente paso
Esta lección aplicó SCD tipo 1 sobre el cambio real de P002: un solo UPDATE, sin filas nuevas, dim_product_type1 pasa de snacks/0.60 a health-snacks/0.68 en el lugar. Confirmaste, con una consulta que solo puede devolver un valor, que la historia quedó irrecuperablemente perdida — y entendiste, con la profundización de esta lección, que esa pérdida no es un defecto del código, sino la promesa exacta que SCD tipo 1 cumple, y que en ciertos casos —correcciones de datos, cambios puramente cosméticos— es precisamente lo que se necesita.
Antes de avanzar deberías poder: escribir de memoria el UPDATE de una fila para SCD tipo 1; explicar por qué el conteo de filas de una dimensión con SCD tipo 1 nunca crece por un cambio de atributo; y nombrar al menos un escenario donde SCD tipo 1 es la elección correcta, no un atajo.
La lección 4 aplica el mismo cambio de P002 —misma fecha, mismas dos columnas— con la técnica que sí preserva la historia: SCD tipo 2, con valid_from, valid_to e is_current.
Recursos
- Kimball Group — "Slowly Changing Dimension Type 1" — la definición oficial que esta lección implementa: "el valor viejo del atributo en la fila de la dimensión se sobrescribe con el valor nuevo". kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/type-1. En inglés.
- DuckDB — documentación oficial del statement
UPDATE— la referencia de sintaxis para la operación que esta lección usa. duckdb.org/docs/current/sql/statements/update. En inglés.