Delta Lake: ACID sobre tu data lake
Parquet sobre object storage no tiene transacciones, ni control de concurrencia, ni upserts reales. Delta Lake resuelve eso con un log de transacciones que convierte un directorio de archivos en algo que se comporta como una base de datos.
Un data lake construido sobre Parquet plano en S3, ADLS o GCS es, en el fondo, una colección de archivos inmutables sin ningún árbitro que decida qué versión de la verdad es la correcta. Si dos jobs escriben en la misma tabla al mismo tiempo, no hay bloqueo ni aislamiento: el que termina último gana, y si un job falla a mitad de escritura, quedan archivos parciales que un lector puede recoger como si fueran datos válidos. No existe un UPDATE o DELETE real a nivel de fila: la práctica habitual es reescribir particiones enteras. Y si necesitas saber cómo estaba la tabla ayer a las 3pm para depurar un pipeline o auditar un cambio, no hay dónde buscarlo. Esto no es un defecto de Parquet como formato de archivo, es una consecuencia de no tener ninguna capa transaccional por encima. Delta Lake ataca exactamente ese problema: añade metadatos y un protocolo de commits sobre los mismos archivos Parquet para convertir un directorio en algo que se comporta como una tabla.
El transaction log como fuente única de verdad
La pieza central de Delta Lake es un subdirectorio llamado _delta_log que vive junto a los archivos de datos. Cada operación sobre la tabla (un INSERT, un MERGE, un cambio de esquema) genera una entrada numerada secuencialmente en ese log, en formato JSON: 00000000000000000000.json, 00000000000000000001.json, y así sucesivamente. Cada entrada no contiene los datos en sí, sino acciones: qué archivos Parquet se añadieron, cuáles quedaron obsoletos, si cambió el esquema, metadatos de la transacción. Un lector nunca "adivina" el estado de la tabla escaneando el directorio de datos; reconstruye ese estado reproduciendo el log en orden, igual que un motor de base de datos reproduce su write-ahead log.
Esto es deliberadamente parecido a cómo Git resuelve el historial de una rama: una secuencia de commits que se pueden reproducir hacia adelante o consultar en cualquier punto intermedio. Como esa reconstrucción sería costosa si hubiera que leer miles de archivos JSON, Delta Lake escribe automáticamente un checkpoint en Parquet cada diez commits, con el estado consolidado hasta ese punto. Un lector nuevo solo necesita el último checkpoint más los commits posteriores, no el historial completo desde el primer día.
Un detalle que sorprende a quien viene de bases de datos tradicionales: borrar o sobrescribir una fila no borra físicamente el archivo Parquet que la contenía. Delta Lake marca ese archivo como "removido" en el log y añade uno nuevo con el dato actualizado. El archivo viejo sigue en disco hasta que se ejecuta VACUUM, que es el comando explícito para purgar archivos ya no referenciados por ninguna versión dentro de la ventana de retención.
Qué garantiza realmente ACID aquí
Sobre object storage, que no tiene bloqueos ni transacciones nativas, ACID se implementa en la capa de metadatos, no en el almacenamiento subyacente. Atomicidad significa que una transacción se traduce en una única entrada nueva en el log: si el proceso que escribe los datos falla antes de confirmar esa entrada, la tabla simplemente no vio cambios, sin archivos huérfanos contaminando lecturas futuras. Consistencia se aplica mediante schema enforcement en cada escritura: si el esquema entrante no encaja con el de la tabla (tipos incompatibles, columnas nuevas no declaradas, diferencias de capitalización en nombres de columna), la transacción se rechaza antes de tocar disco.
Aislamiento se resuelve con control de concurrencia optimista: cada escritor comprueba, antes de confirmar su commit, si alguien más ya escribió una versión más reciente contra la que su transacción entraría en conflicto; si es así, reintenta sobre la nueva versión en lugar de corromper el resultado. Durabilidad es la propiedad más directa: una vez que el archivo JSON del commit se confirma en el object storage subyacente, la transacción es definitiva y visible para cualquier lector que consulte el log a partir de ese momento.
El log de transacciones no es un detalle de implementación: es literalmente el mecanismo que hace que "leer una tabla Delta" sea una operación con una respuesta única y consistente, en lugar de una carrera entre escritores.
Time travel: versionado sin backups manuales
Como cada versión de la tabla queda registrada como una secuencia reproducible de commits, consultar el estado histórico no requiere haber tomado un snapshot manual de antemano. Se puede leer una tabla tal como estaba en una versión numerada o en un timestamp específico, y se puede revertir la tabla completa a un estado anterior con RESTORE. Esto resuelve de forma nativa varios problemas que antes exigían infraestructura aparte: auditoría regulatoria sobre cómo lucían los datos en una fecha dada, reproducibilidad de un experimento de machine learning entrenado contra una versión concreta del dataset, o simplemente deshacer un UPDATE sin WHERE que acabas de ejecutar por error.
Esta capacidad tiene un costo de almacenamiento que hay que gestionar activamente. Los archivos de datos no se eliminan solos; permanecen mientras alguna versión retenida los referencie. La ventana de retención de datos por defecto es de 7 días, configurable por tabla, y la de los archivos de log del propio _delta_log es de 30 días. Si necesitas time travel a un año vista por requisito regulatorio, hay que extender ambas retenciones explícitamente, y aceptar que el almacenamiento crecerá en proporción. Para horizontes de retención muy largos suele ser más barato archivar snapshots periódicos en tablas aparte que mantener toda la historia viva en la tabla operacional.
Upserts reales y evolución de esquema controlada
A diferencia de Parquet plano, donde un "upsert" implica reescribir la partición completa o hacer malabares con lógica externa, Delta Lake soporta DML completo: UPDATE, DELETE y, sobre todo, MERGE INTO para combinar un lote entrante con una tabla existente en una sola operación atómica. Internamente esto sigue funcionando a nivel de archivo (se reescriben los archivos Parquet afectados, no filas sueltas), pero desde la perspectiva del que escribe el pipeline es una sentencia declarativa, no un proceso artesanal.
La gestión de esquema tiene dos caras que conviene no confundir. Schema enforcement es la barrera que impide que datos con forma incorrecta entren a la tabla: por defecto, una escritura con columnas nuevas no declaradas falla. Schema evolution es el mecanismo opuesto y complementario: cuando se habilita explícitamente con la opción mergeSchema, una columna nueva en los datos entrantes se añade al esquema de la tabla, rellenando con null las filas históricas. Es evolución aditiva y controlada, no un "todo vale": cambios de tipo incompatibles siguen fallando, y renombrar o eliminar columnas requiere sentencias ALTER TABLE explícitas, no ocurre por accidente en una escritura normal.
Rendimiento: compactación y Z-order
Los mismos mecanismos que dan fiabilidad tienen un efecto secundario: escrituras incrementales frecuentes (streaming, micro-batches) generan muchos archivos Parquet pequeños, y eso degrada la lectura porque cada archivo abierto tiene overhead fijo. El comando OPTIMIZE compacta esos archivos pequeños en archivos de tamaño balanceado sin cambiar el contenido lógico de la tabla, y es idempotente: ejecutarlo dos veces seguidas no hace daño ni duplica trabajo.
Delta Lake también mantiene estadísticas por archivo (mínimos, máximos, conteo de nulos) sobre las primeras columnas del esquema, lo que permite descartar archivos completos en tiempo de consulta cuando el filtro cae fuera de su rango: esto es data skipping, y es la razón por la que ordenar los datos importa. ZORDER BY, combinado con OPTIMIZE, reorganiza físicamente los datos para que valores similares en las columnas indicadas queden colocados juntos, estrechando esos rangos y haciendo el data skipping mucho más efectivo. La limitación práctica es que Z-order no es incremental: cada ejecución reagrupa todo el conjunto afectado, así que en tablas con escritura continua conviene programarlo con una cadencia razonable en vez de ejecutarlo tras cada micro-batch. Liquid clustering es la evolución más reciente de esta idea: sustituye tanto el particionado clásico como Z-order con un mecanismo que sí admite clustering incremental en cada escritura, pensado para tablas con alta cardinalidad de filtro o patrones de acceso que cambian con el tiempo.
| Capacidad | Parquet plano en object storage | Delta Lake |
|---|---|---|
| Transacciones ACID | No existen | Vía transaction log (_delta_log) |
| Lecturas concurrentes durante escritura | Pueden ver archivos parciales | Siempre ven una versión consistente |
| UPDATE / DELETE / MERGE | Requiere reescribir particiones a mano | DML nativo, atómico |
| Historial de versiones | Ninguno sin snapshots manuales | Time travel por versión o timestamp |
| Validación de esquema al escribir | Ninguna | Schema enforcement configurable |
| Archivos pequeños | Se acumulan sin mecanismo de limpieza | OPTIMIZE / compaction |
Cuándo usarlo en la práctica
Delta Lake tiene sentido casi por defecto en cualquier lakehouse donde múltiples jobs escriben o leen la misma tabla de forma concurrente, donde hay procesos batch y streaming conviviendo sobre la misma fuente, o donde el pipeline necesita upserts reales en lugar de reescrituras completas. También es la opción correcta cuando existe una obligación de auditoría o cumplimiento normativo sobre el histórico de los datos, o cuando la calidad de datos es un problema recurrente y quieres que una escritura con esquema incorrecto falle de forma ruidosa en vez de colarse silenciosamente.
No lo adoptes solo porque "es lo moderno". Si tu carga de trabajo es predominantemente de solo lectura, con datos que se escriben una vez y no cambian, y no necesitas versionado ni concurrencia de escritura, el overhead de mantener _delta_log, ejecutar OPTIMIZE y VACUUM periódicamente, y gestionar ventanas de retención es costo sin beneficio correspondiente. Tampoco tiene sentido si tu stack de consulta no tiene un conector Delta razonable; aunque el ecosistema ha crecido (Spark, Trino, Presto, Flink, Power BI vía Delta Sharing, y UniForm para interoperar con Iceberg y Hudi), sigue valiendo la pena verificar el soporte del motor específico antes de comprometer la arquitectura.
Antes de escribir la primera línea de un pipeline sobre Delta Lake, decide explícitamente tu política de retención de datos y de logs, y programa VACUUM y OPTIMIZE como tareas de mantenimiento recurrentes desde el día uno. Es mucho más barato definir esto de entrada que rediseñarlo cuando el almacenamiento ya se disparó.