Hace unas semanas revisé un workspace de Fabric en producción. Llevaba 8 meses funcionando, con pipelines que ejecutaban MERGE a diario. Nadie había tocado el mantenimiento. El resultado: OneLake almacenaba el triple de lo necesario. Archivos Parquet obsoletos que ya no servían para nada, pero que seguían ahí ocupando espacio y generando coste.
Es el problema más tonto y más caro que me encuentro en Fabric. Y se soluciona con un comando.
Por qué pasa esto
Las tablas Delta guardan datos en archivos Parquet. Cada vez que haces un INSERT, UPDATE, DELETE o MERGE, se crean archivos nuevos y los anteriores se marcan como obsoletos. Pero no se borran — se quedan ahí para el time travel (poder consultar versiones anteriores de la tabla). Suena útil en teoría. En la práctica, si tienes pipelines diarios, en pocos meses una parte enorme de tu OneLake son archivos fantasma. Y OneLake cobra por GB almacenado. Es dinero tirado, literalmente.
VACUUM: el comando que nadie programa
VACUUM delta.`ruta_tabla` RETAIN 168 HOURS. Eso es todo. Borra los archivos obsoletos de más de 7 días. Pierdes el time travel de antes de esa ventana, que en producción raramente necesitas. Delta tiene un check de seguridad que no te deja bajar de 7 días de retención — así no te cargas nada por accidente.
Lo que hay que hacer: automatizar VACUUM como parte del pipeline (no ejecutarlo a mano cada vez que te acuerdas), combinarlo con OPTIMIZE para compactar los archivos Parquet pequeños que fragmentan las consultas, y definir una retención de 7 días que para la mayoría de casos sobra.
Lo que me preocupa: nadie vigila esto
Este caso es un ejemplo perfecto de por qué un workspace de Fabric no se puede dejar en piloto automático. Si nadie entiende cómo funciona Delta Lake por debajo, si nadie programa el mantenimiento, si nadie mira cuánto almacenamiento se está consumiendo, la factura crece en silencio hasta que alguien se lleva un susto. He visto workspaces donde un VACUUM semanal habría ahorrado miles al año. Y nadie se había dado cuenta porque nadie estaba mirando.
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
# --- CONFIGURATION ---
RETENTION_HOURS = 168 # 168 hours = 7 days
WORKSPACE_ID = fabric.get_workspace_id()
# Optional: List of specific Lakehouse names.
# If empty [], will process ALL in the workspace.
LAKEHOUSES_FILTER = [] # E.g: ["LH_Sales", "LH_Logistics"]
# Disable safety check to allow RETAIN 0 (if necessary)
spark.conf.set("spark.databricks.delta.retentionDurationCheck.enabled", "false")
# --- 1. GETTING LAKEHOUSES ---
print(">>> Inventorying 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"Detected {len(lakehouses_info)} Lakehouses to process.")
# --- 2. SUPPORT FUNCTIONS ---
all_tables = []
def find_delta_tables(path, lh_name):
"""Recursively searches for Delta tables and saves context."""
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):
"""Calculates size recursively."""
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. GLOBAL SEARCH ---
print("-" * 60)
for lh_name, lh_path in lakehouses_info:
print(f">>> Scanning Lakehouse: {lh_name} ...")
find_delta_tables(lh_path, lh_name)
print(f">>> Total tables found: {len(all_tables)}")
print("-" * 60)
# --- 4. RUNNING 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)}] Processing: {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"Cleaned. Savings: {ahorro:.4f} GB")
except Exception as e:
print("FAILED.")
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. FINAL REPORT ---
print("\n--- FINAL SUMMARY ---")
df_res = (
spark.createDataFrame(
results,
["Lakehouse", "Table", "GB_Before", "GB_After", "GB_Saved", "Status", "Path"]
)
.withColumn("GB_Before", round(col("GB_Before"), 4))
.withColumn("GB_After", round(col("GB_After"), 4))
.withColumn("GB_Saved", round(col("GB_Saved"), 4))
.orderBy(desc("GB_Saved"))
)
display(df_res)
try:
total_saved = df_res.agg({"GB_Saved": "sum"}).collect()[0][0]
print(f"Total space recovered in Workspace: {total_saved if total_saved else 0:.4f} GB")
except:
print("Could not calculate 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.