Volver al blog

Mantenimiento de Lakehouses en Fabric: por qué VACUUM es imprescindible

Si trabajas con Lakehouses en Microsoft Fabric y nadie se encarga de su mantenimiento, tu factura de almacenamiento crece cada día sin que te des cuenta. Es uno de los problemas más comunes — y más evitables — que me encuentro en workspaces de producción.

El problema: archivos fantasma que cuestan dinero

Las tablas Delta almacenan datos en archivos Parquet. Cada operación de escritura (INSERT, UPDATE, DELETE, MERGE) crea archivos nuevos y marca los anteriores como obsoletos — pero no los borra. Se conservan para time travel. Si tienes pipelines que ejecutan MERGE a diario, en pocos meses una parte significativa de tu almacenamiento en OneLake son archivos que ya no sirven para nada. Y OneLake factura por GB almacenado.

La solución: VACUUM

El comando VACUUM elimina los archivos obsoletos que superan un periodo de retención. La sintaxis: VACUUM delta.`ruta_tabla` RETAIN 168 HOURS. Esto borra todo lo que tenga más de 7 días. Pierdes time travel anterior a ese periodo, pero recuperas el espacio. Delta tiene un check de seguridad que impide retenciones menores a 7 días.

Lo que hay que hacer es simple: automatizar VACUUM como parte del pipeline de datos, combinar con OPTIMIZE (que compacta archivos Parquet pequeños), y definir una retención coherente (7 días suele ser suficiente en producción).

Por qué esto importa: sin administración, la factura se dispara

Este es un ejemplo perfecto de por qué un workspace de Fabric necesita a alguien que lo administre. Sin un responsable que entienda cómo funciona Delta Lake, que configure el mantenimiento, y que monitorice el almacenamiento, la factura crece de forma silenciosa e indefinida. He visto workspaces donde un simple VACUUM programado hubiera ahorrado una cantidad significativa cada mes. Si un junior empieza a crear Lakehouses y cargar datos sin saber que el mantenimiento existe, el resultado es el mismo: almacenamiento descontrolado y coste innecesario.

Script: VACUUM recursivo sobre todo el workspace

Este notebook de Fabric recorre automáticamente todos los Lakehouses del workspace, encuentra todas las tablas Delta, ejecuta VACUUM, y genera un informe con el espacio recuperado por tabla.

Python · Fabric Notebook
import sempy.fabric as fabric
from notebookutils import mssparkutils
from pyspark.sql.functions import col, round, desc
import time

# --- CONFIGURACIÓN ---
RETENTION_HOURS = 168  # 168 horas = 7 días
WORKSPACE_ID = fabric.get_workspace_id()

# Opcional: Lista de nombres de Lakehouses específicos.
# Si vacía [], procesará TODOS los del workspace.
LAKEHOUSES_FILTER = []  # Ej: ["LH_Ventas", "LH_Logistica"]

# Desactivar check de seguridad para permitir RETAIN 0 (si fuera necesario)
spark.conf.set("spark.databricks.delta.retentionDurationCheck.enabled", "false")

# --- 1. OBTENCIÓN DE LAKEHOUSES ---
print(">>> Inventariando Lakehouses...")

df_items = fabric.list_items(workspace=WORKSPACE_ID, type="Lakehouse")

if LAKEHOUSES_FILTER:
    df_items = df_items[df_items['Display Name'].isin(LAKEHOUSES_FILTER)]

lakehouses_info = []
for _, row in df_items.iterrows():
    lh_name = row['Display Name']
    lh_id = row['Id']
    abfss_path = f"abfss://{WORKSPACE_ID}@onelake.dfs.fabric.microsoft.com/{lh_id}/Tables"
    lakehouses_info.append((lh_name, abfss_path))

print(f"Detectados {len(lakehouses_info)} Lakehouses para procesar.")

# --- 2. FUNCIONES DE APOYO ---
all_tables = []

def find_delta_tables(path, lh_name):
    """Busca recursivamente tablas Delta y guarda su contexto."""
    try:
        items = mssparkutils.fs.ls(path)
        if any(item.name == "_delta_log" and item.isDir for item in items):
            all_tables.append((lh_name, path))
            return
        subdirs = [item.path for item in items if item.isDir and item.name != "_delta_log"]
        for subdir in subdirs:
            find_delta_tables(subdir, lh_name)
    except Exception as e:
        pass

def get_folder_size(path):
    """Calcula tamaño físico recursivamente."""
    total = 0
    try:
        files = mssparkutils.fs.ls(path)
        for file in files:
            if file.isDir:
                total += get_folder_size(file.path)
            else:
                total += file.size
    except:
        pass
    return total

# --- 3. BÚSQUEDA GLOBAL ---
print("-" * 60)
for lh_name, lh_path in lakehouses_info:
    print(f">>> Escaneando Lakehouse: {lh_name} ...")
    find_delta_tables(lh_path, lh_name)

print(f">>> Total tablas encontradas: {len(all_tables)}")
print("-" * 60)

# --- 4. EJECUCIÓN DEL VACUUM ---
results = []

for i, (lh_name, table_path) in enumerate(all_tables):
    table_name_short = table_path.split("/")[-1]
    full_display_name = f"{lh_name}.{table_name_short}"

    print(f"[{i+1}/{len(all_tables)}] Procesando: {full_display_name} ...", end=" ")

    size_pre_bytes = get_folder_size(table_path)
    size_pre_gb = size_pre_bytes / (1024**3)

    status = "OK"
    size_post_gb = size_pre_gb

    try:
        spark.sql(f"VACUUM delta.`{table_path}` RETAIN {RETENTION_HOURS} HOURS")

        time.sleep(2)
        size_post_bytes = get_folder_size(table_path)
        size_post_gb = size_post_bytes / (1024**3)

        ahorro = size_pre_gb - size_post_gb
        print(f"Limpiado. Ahorro: {ahorro:.4f} GB")

    except Exception as e:
        print("FALLO.")
        status = f"Error: {str(e)[0:100]}"

    results.append((
        lh_name, table_name_short,
        size_pre_gb, size_post_gb,
        size_pre_gb - size_post_gb,
        status, table_path
    ))

# --- 5. INFORME FINAL ---
print("\n--- RESUMEN FINAL ---")

df_res = (
    spark.createDataFrame(
        results,
        ["Lakehouse", "Tabla", "GB_Antes", "GB_Despues", "GB_Ahorrados", "Estado", "Ruta"]
    )
    .withColumn("GB_Antes", round(col("GB_Antes"), 4))
    .withColumn("GB_Despues", round(col("GB_Despues"), 4))
    .withColumn("GB_Ahorrados", round(col("GB_Ahorrados"), 4))
    .orderBy(desc("GB_Ahorrados"))
)

display(df_res)

try:
    total_saved = df_res.agg({"GB_Ahorrados": "sum"}).collect()[0][0]
    print(f"Espacio total recuperado en el Workspace: {total_saved if total_saved else 0:.4f} GB")
except:
    print("No se pudo calcular el total.")

El informe final muestra GB antes, después, y ahorrados por tabla — ordenado de mayor a menor. Prográmalo semanalmente con un schedule de Fabric y el mantenimiento queda resuelto.

¿Necesitas ayuda con esto?

Si este artículo describe un reto parecido al tuyo, hablemos.

Hablemos de tu proyecto