En el diseño y la construcción de un Data Lake que sigue los principios de la Arquitectura Medallion (capas Bronze, Silver y Gold), el seguimiento preciso de la evolución temporal de las entidades es un pilar fundamental. Para el proyecto de la Plataforma de Contratación del Sector Público (PLACSP), hemos evolucionado desde el uso de claves naturales simples (identificadores oficiales) hacia una estrategia de Claves Subrogadas Deterministas.
Este documento detalla exhaustivamente los motivos, la implementación técnica y los beneficios arquitectónicos de este enfoque, así como la justificación del uso de MD5 como algoritmo principal.
Las licitaciones públicas en España no son entidades estáticas; son procesos dinámicos que atraviesan múltiples estados y fases. Una licitación puede ser:
- Publicada: Se anuncia inicialmente el proceso.
- Modificada (Enmiendas/Rectificaciones): Se corrigen errores en los pliegos o se amplían los plazos.
- Adjudicada: Se selecciona provisionalmente a un ganador.
- Formalizada/Cancelada: Se cierra el contrato o se desiste del proceso.
Si utilizáramos el identificador oficial (normalmente una URI o un ID único proporcionado por la fuente) como clave primaria, cada actualización del proceso sobrescribiría el registro anterior. Esto resultaría en la pérdida irremediable del historial de cambios. No podríamos auditar qué cambió entre una versión y otra, ni reconstruir el estado de la licitación en una fecha pasada específica.
Además, el XML de CODICE estructura una licitación como un árbol único que contiene, anidados, sus lotes, documentos y resultados. Para almacenar estas entidades en tablas Delta separadas — manteniendo la integridad referencial entre ellas desde el momento de la extracción — es necesario contar con un identificador de versión estable que actúe como clave foránea desde la primera capa de procesamiento.
Para resolver ambos problemas sin sacrificar eficiencia en sistemas distribuidos, implementamos Claves Subrogadas (SK) mediante hashing determinista. Aunque inicialmente se consideró SHA-256, hemos optado por MD5 por motivos de eficiencia y rendimiento.
La elección del algoritmo de hashing depende del caso de uso. En este proyecto, priorizamos el rendimiento y el ahorro de recursos:
| Criterio | MD5 | SHA-256 |
|---|---|---|
| Longitud | 32 caracteres (128 bits) | 64 caracteres (256 bits) |
| Espacio | Ocupa un 50% menos en disco y RAM | Mayor consumo de almacenamiento |
| Velocidad | Cálculo significativamente más rápido en Spark | Más costoso computacionalmente |
| Colisiones | Probabilidad despreciable ( |
Prácticamente nula |
| Uso Ideal | Generación de SK, CDC, Joins internos | Anonimización (RGPD), Seguridad, Criptografía |
Justificación técnica:
- Rendimiento en JOINs: Al generar identificadores de solo 32 caracteres, las operaciones de unión (
JOIN) en Spark son ligeramente más veloces y consumen menos memoria en el clúster. - Bajo Riesgo: Las claves subrogadas no se utilizan para cifrar datos sensibles, sino para identificar filas. Dado que no hay riesgo de ataques maliciosos sobre estos identificadores, la debilidad criptográfica de MD5 frente a SHA-256 es irrelevante.
- Probabilidad de Colisión: La probabilidad de una colisión accidental en MD5 es tan baja que es prácticamente imposible en datos normales de licitaciones.
Un aspecto clave del diseño es que la sk_id se genera en la capa Bronze y se hereda y aprovecha en Silver. Estas dos capas la utilizan con propósitos diferentes pero complementarios.
La sk_id se calcula en bronze/src/extraction.py durante la extracción del XML. Su propósito en Bronze es estructural: permite vincular mediante clave foránea las tablas de entidades derivadas (lots, documents, results) con la tabla principal (entries), haciendo posible almacenarlas en tablas Delta separadas desde el momento de la extracción.
Sin esta clave, la única alternativa sería mantener lotes, documentos y resultados como columnas anidadas (ArrayType<StructType>) dentro de una única tabla entries. Eso simplificaría Bronze pero trasladaría la complejidad de normalización a Silver, donde ya existen suficientes responsabilidades semánticas.
La sk_id en Bronze responde a la pregunta: ¿a qué versión de qué entry pertenece este lote/documento/resultado?
from pyspark.sql.functions import md5, concat_ws, col
# Generación de la Clave Subrogada Estructural en Bronze
md5(
concat_ws(
"||",
col("entry.id"), # Identificador natural de la licitación
col("entry.updated") # Timestamp de la última modificación
)
).alias("sk_id")concat_ws("||", ...): El separador||evita colisiones de hash en casos donde dos valores diferentes, al concatenarse, pudieran dar el mismo resultado (p. ej.,"12"+"3"vs."1"+"23").md5(...): Equilibrio óptimo entre velocidad de cálculo y longitud del identificador para el rendimiento de los joins.
En Silver, la misma sk_id generada por Bronze no se recalcula: se hereda directamente de las tablas Bronze. Su rol en esta capa pasa de ser estructural a ser histórico: permite almacenar múltiples filas para el mismo expediente (una por cada versión publicada) e implementar Slowly Changing Dimensions de Tipo 2 (SCD Tipo 2).
La sk_id en Silver responde a la pregunta: ¿cuándo fue válida esta versión del expediente, y qué cambió respecto a la anterior?
La lógica de generación de estas claves está integrada en bronze/src/extraction.py, en las funciones extract_bidding_entries() y extract_deleted_entries().
Para mantener la integridad referencial en todo el Data Lake, la sk_id se propaga a todas las entidades relacionadas desde Bronze:
- Lotes (Lots): Cada lote se vincula a la
sk_idde la versión de la licitación con la que fue publicado. - Documentos: Los pliegos y anexos se asocian a la versión exacta del anuncio que los referenciaba originalmente.
- Resultados de Adjudicación: Permite trazar cómo cambiaron las condiciones de adjudicación en respuesta a enmiendas previas.
La implementación de Claves Subrogadas mediante hashing desbloquea capacidades analíticas avanzadas:
Al generar la sk_id en Bronze, es posible separar lotes, documentos y resultados en tablas Delta propias desde el momento de la extracción. Esto simplifica radicalmente la capa Silver, que trabaja con tablas ya normalizadas en lugar de tener que resolver arrays anidados antes de poder aplicar transformaciones semánticas.
Gracias a la sk_id, Silver puede almacenar múltiples filas para el mismo ID de licitación. Esto permite implementar una estrategia de Dimensiones de Cambio Lento de Tipo 2, donde cada fila representa una "fotografía" válida del proceso en un momento determinado del tiempo.
En Delta Lake, las operaciones de unión (JOIN) sobre columnas de hash de longitud fija son extremadamente rápidas. Al evitar uniones basadas en cadenas de texto largas y variables (como las URIs originales), reducimos drásticamente el tiempo de ejecución de procesos ETL y consultas de BI.
Podemos reconstruir el estado de cualquier licitación en cualquier punto del tiempo simplemente filtrando por la columna entry_updated asociada a la clave hash. Esto es crítico para el cumplimiento normativo y el análisis forense de la contratación pública.
Para garantizar que el Data Lake sea autodescriptivo y facilite la labor de los científicos de datos y analistas, cada columna de clave subrogada se enriquece con metadatos específicos durante el proceso de transformación:
# Metadatos aplicados en Bronze (extraction.py)
df = df.withMetadata("sk_id", {
"comment": "Clave Subrogada estructural generada en Bronze. Identifica unívocamente una versión del registro (id + updated). Heredada por Silver para SCD Tipo 2.",
"business_logic": "MD5(id || updated)",
"layer": "Bronze"
})Esta práctica permite que herramientas de catálogo de datos o el comando DESCRIBE TABLE de Spark proporcionen contexto inmediato sobre el origen y el propósito de la clave.
La sk_id es una clave de doble propósito que actúa en dos capas con roles distintos pero complementarios:
| Capa | Rol de la sk_id |
Pregunta que responde |
|---|---|---|
| Bronze | Clave foránea estructural | ¿A qué versión de qué entry pertenece este lote/documento? |
| Silver | Clave de versión para SCD Tipo 2 | ¿Cuándo fue válida esta versión y qué cambió? |
Generarla en Bronze —la capa de extracción estructural— permite normalizar las entidades del XML en tablas separadas sin perder integridad referencial, y sin trasladar esa complejidad a Silver. Silver hereda la clave y la aprovecha para el seguimiento histórico, manteniendo su responsabilidad exclusivamente en la semántica de negocio.
La transición de claves naturales a Claves Subrogadas basadas en Hash marca un hito en la madurez técnica del proyecto PLACSP. No solo asegura la preservación del historial completo de la contratación pública española, sino que también establece las bases para un sistema de datos escalable, performante y preparado para auditorías complejas en la capa analítica (Gold).