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.
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.