<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[DataGym | Microsoft Fabric]]></title><description><![CDATA[DataGym | Microsoft Fabric]]></description><link>https://datagym.es</link><generator>RSS for Node</generator><lastBuildDate>Mon, 13 Apr 2026 23:23:15 GMT</lastBuildDate><atom:link href="https://datagym.es/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Change Data Feed en Delta Lake: Captura incremental de cambios para pipelines modernos en Microsoft Fabric]]></title><description><![CDATA[Change Data Feed es una funcionalidad de Delta Lake que registra los cambios a nivel de fila entre versiones de una tabla Delta. Al activarla, el runtime genera un evento de cambio por cada operación ]]></description><link>https://datagym.es/change-data-feed-en-delta-lake-captura-incremental-de-cambios-para-pipelines-modernos-en-microsoft-fabric</link><guid isPermaLink="true">https://datagym.es/change-data-feed-en-delta-lake-captura-incremental-de-cambios-para-pipelines-modernos-en-microsoft-fabric</guid><category><![CDATA[microsoft fabric]]></category><category><![CDATA[deltalake]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[lakehouse]]></category><category><![CDATA[cdf]]></category><category><![CDATA[ETL]]></category><category><![CDATA[#DataArchitecture]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Tue, 24 Mar 2026 07:52:26 GMT</pubDate><content:encoded><![CDATA[<p><strong>Change Data Feed</strong> es una funcionalidad de Delta Lake que registra los cambios a nivel de fila entre versiones de una tabla Delta. Al activarla, el runtime genera un <em>evento de cambio</em> por cada operación de escritura: inserciones, actualizaciones y eliminaciones.</p>
<p>Piénsalo como un CDC (<em>Change Data Capture</em>) nativo dentro de Delta Lake, sin necesidad de herramientas externas ni lectura de logs de base de datos. Los cambios quedan accesibles directamente vía Spark SQL o PySpark.</p>
<hr />
<h2>El esquema de eventos CDF</h2>
<p>Al leer el CDF, obtienes las columnas de datos de tu tabla más tres columnas de metadatos automáticas:</p>
<table>
<thead>
<tr>
<th>Columna</th>
<th>Tipo</th>
<th>Descripción</th>
</tr>
</thead>
<tbody><tr>
<td><code>_change_type</code></td>
<td>String</td>
<td>Tipo de operación: <code>insert</code>, <code>update_preimage</code>, <code>update_postimage</code>, <code>delete</code></td>
</tr>
<tr>
<td><code>_commit_version</code></td>
<td>Long</td>
<td>Versión del log de Delta del commit</td>
</tr>
<tr>
<td><code>_commit_timestamp</code></td>
<td>Timestamp</td>
<td>Timestamp exacto del commit</td>
</tr>
</tbody></table>
<p>Los cuatro valores posibles de <code>_change_type</code> son:</p>
<ul>
<li><p><code>insert</code> — Fila nueva insertada</p>
</li>
<li><p><code>update_preimage</code> — Estado de la fila <em>antes</em> de la actualización</p>
</li>
<li><p><code>update_postimage</code> — Estado de la fila <em>después</em> de la actualización</p>
</li>
<li><p><code>delete</code> — Fila eliminada</p>
</li>
</ul>
<blockquote>
<p><strong>Nota clave sobre updates:</strong> cada UPDATE genera <em>dos</em> filas: <code>update_preimage</code> y <code>update_postimage</code>. Esto es fundamental para SCD2, ya que tenemos el estado anterior y el nuevo con sus timestamps exactos.</p>
</blockquote>
<hr />
<h1><strong>Cómo habilitar el Change Data Feed en Fabric</strong></h1>
<p>CDF <strong>no está activo por defecto</strong>: debe activarse explícitamente. Además, <strong>solo captura cambios realizados después de su activación</strong> — el historial previo no queda registrado.</p>
<p>Hay tres formas de activarlo:</p>
<h2>1. Al crear una tabla nueva</h2>
<pre><code class="language-sql">CREATE TABLE raw_productos (
    id       INT,
    nombre   STRING,
    precio   DECIMAL(10,2),
    stock    INT
)
TBLPROPERTIES (delta.enableChangeDataFeed = true);
</code></pre>
<h2>2. En una tabla existente</h2>
<pre><code class="language-sql">ALTER TABLE raw_productos
SET TBLPROPERTIES (delta.enableChangeDataFeed = true);
</code></pre>
<h2>3. Para todas las tablas nuevas de la sesión</h2>
<pre><code class="language-python">spark.conf.set('spark.microsoft.delta.properties.defaults.enableChangeDataFeed', 'true')
</code></pre>
<h2>Verificar que está habilitado</h2>
<pre><code class="language-python">spark.sql("DESCRIBE DETAIL raw_productos") \
     .select('name', 'properties') \
     .show(truncate=False)
</code></pre>
<p>Resultado esperado en Fabric:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729d2b7863d50b7acfa2d96/8c2b473f-3189-4bb7-9179-f8f5ae5d962a.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-plaintext">{delta.enableChangeDataFeed -&gt; true, delta.stats.extended.collect -&gt; true, ...}
</code></pre>
<hr />
<h1>Lectura de cambios</h1>
<p>Puedes consultar los cambios por <strong>rango de versiones</strong> o por <strong>rango de timestamps</strong>. Ambos extremos del rango son inclusivos.</p>
<h3>Spark SQL</h3>
<pre><code class="language-sql">-- Por rango de versiones
SELECT * FROM table_changes('raw_productos', 0, 10);

-- Desde una versión hasta la última disponible
SELECT * FROM table_changes('raw_productos', 0);

-- Por rango de timestamps
SELECT * FROM table_changes('raw_productos',
    '2026-01-01 00:00:00',
    '2026-03-05 09:30:55');
</code></pre>
<h3>PySpark</h3>
<pre><code class="language-python"># Por versiones
df = (spark.read
    .format('delta')
    .option('readChangeFeed', 'true')
    .option('startingVersion', 0)
    .option('endingVersion', 10)
    .table('raw_productos'))

# Por timestamps
df = (spark.read
    .format('delta')
    .option('readChangeFeed', 'true')
    .option('startingTimestamp', '2026-03-05 09:30:05')
    .option('endingTimestamp',   '2026-03-05 09:30:55')
    .table('raw_productos'))

# Desde una versión hasta la última disponible
df = (spark.read
    .format('delta')
    .option('readChangeFeed', 'true')
    .option('startingVersion', 0)
    .table('raw_productos'))
</code></pre>
<p>Ejemplo de salida real con una tabla de productos:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729d2b7863d50b7acfa2d96/bfd078f0-a4a5-4782-8d2e-6084bb5bde99.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Almacenamiento de los datos de cambio</h1>
<p>Delta Lake almacena los registros de cambio para operaciones <strong>UPDATE</strong>, <strong>DELETE</strong> y <strong>MERGE</strong> en la carpeta <code>_change_data</code>dentro del directorio de la tabla Delta en OneLake. Sin embargo, existen situaciones donde Delta Lake optimiza el proceso y no genera archivos en esta carpeta:</p>
<ul>
<li><p>Operaciones de solo inserción (INSERT): los cambios se calculan directamente desde el transaction log.</p>
</li>
<li><p>Eliminaciones completas de partición: igualmente calculables sin archivos adicionales.</p>
</li>
</ul>
<p>Los archivos en <code>_change_data</code> siguen la misma política de retención de la tabla. Si ejecutas <code>VACUUM</code> con el período de retención por defecto (7 días), también se limpiarán estos registros de cambio.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729d2b7863d50b7acfa2d96/3e865471-ae3a-49ad-b6c5-5aefde8088f6.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Caso de uso 1: Sincronización Silver → Gold</h1>
<p>El primer caso de uso es el más frecuente en arquitecturas Medallion: propagar cambios desde la capa Silver a Gold procesando únicamente las filas que han cambiado.</p>
<h2>Datos iniciales</h2>
<p>Disponemos de dos tablas de productos (productos_silver y productos_gold) con los siguientes datos:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6729d2b7863d50b7acfa2d96/f2f44eef-e189-469f-8df4-af0345510976.png" alt="" style="display:block;margin:0 auto" />

<p>Y se generan los siguientes cambios sobre la tabla de silver:</p>
<pre><code class="language-python"># Actualizar precio del Laptop (bajada de precio)
spark.sql("""
    UPDATE productos_silver
    SET precio = 849.99, stock = 45
    WHERE producto_id = 1
""")

# Añadir nuevo producto al catálogo
nuevo = spark.createDataFrame([
    Row(5, 'Webcam HD', 'Perifericos', Decimal('59.99'), 180)
], schema)
nuevo.write.format('delta').mode('append').saveAsTable('productos_silver')

# Eliminar producto descatalogado
spark.sql("DELETE FROM productos_silver WHERE producto_id = 4")
</code></pre>
<h2>Punto de partida: detectar la versión CDF inicial</h2>
<p>Un problema práctico es saber <strong>desde qué versión empezar a leer el CDF</strong>. Consultar el historial de la tabla y buscar cuándo se habilitó <code>delta.enableChangeDataFeed</code> resuelve esto de forma robusta:</p>
<pre><code class="language-python">def get_cdf_start_version(table_name: str) -&gt; int:
    """
    Obtiene la primera versión donde CDF fue habilitado en la tabla,
    consultando DESCRIBE HISTORY y buscando en operationParameters
    la propiedad delta.enableChangeDataFeed = true.
    """
    history_df = spark.sql(f"DESCRIBE HISTORY {table_name}")
    cdf_versions = []

    for row in history_df.collect():
        operation = row['operation'] or ''
        params    = row['operationParameters'] or {}

        if operation not in ('SET TBLPROPERTIES', 'CREATE TABLE',
                             'CREATE OR REPLACE TABLE AS SELECT'):
            continue

        raw_props = params.get('properties', '{}')
        try:
            props = json.loads(raw_props)
        except (json.JSONDecodeError, TypeError):
            props = {}

        if props.get('delta.enableChangeDataFeed') == 'true':
            cdf_versions.append(row['version'])

    if not cdf_versions:
        raise ValueError(
            f"CDF no está habilitado en '{table_name}'. "
            f"Ejecuta: ALTER TABLE {table_name} "
            f"SET TBLPROPERTIES (delta.enableChangeDataFeed = true)"
        )

    return min(cdf_versions)
</code></pre>
<h2>El proceso de sincronización completo</h2>
<p>Con los datos de Silver cargados y los cambios simulados (update de precio, insert de nuevo producto, delete de producto descatalogado), el proceso completo es:</p>
<pre><code class="language-python">from delta.tables import DeltaTable
from pyspark.sql import functions as F
from pyspark.sql.window import Window

# Obtener versión inicial del CDF
cdf_start_version = get_cdf_start_version('productos_silver')

# Leer todos los cambios
cambios_cdf = (spark.read
               .format('delta')
               .option('readChangeFeed', 'true')
               .option('startingVersion', cdf_start_version)
               .table('productos_silver'))

# Filtrar update_preimage (nos quedamos con el estado final)
cambios_filtrados = cambios_cdf.filter(
    F.col('_change_type') != 'update_preimage'
)

# Quedarse con la última operación por producto (en caso de múltiples cambios)
window = Window.partitionBy('producto_id').orderBy(F.col('_commit_version').desc())
ultimos_cambios = (cambios_filtrados
    .withColumn('rn', F.row_number().over(window))
    .filter(F.col('rn') == 1)
    .drop('rn'))

# Aplicar MERGE a Gold
gold   = DeltaTable.forName(spark, 'productos_gold')
upserts = ultimos_cambios.drop('_commit_version', '_commit_timestamp')

gold.alias('gold').merge(
    upserts.alias('silver'),
    'gold.producto_id = silver.producto_id'
).whenMatchedDelete(
    condition="silver._change_type = 'delete'"
).whenMatchedUpdateAll(
    condition="silver._change_type = 'update_postimage'"
).whenNotMatchedInsert(
    condition="silver._change_type = 'insert'",
    values={
        'producto_id': 'silver.producto_id',
        'nombre':      'silver.nombre',
        'categoria':   'silver.categoria',
        'precio':      'silver.precio',
        'stock':       'silver.stock',
    }
).execute()
</code></pre>
<p>El MERGE discrimina por <code>_change_type</code> dentro de cada cláusula <code>whenMatched</code> / <code>whenNotMatched</code>, propagando correctamente inserts, updates y deletes a Gold. Con <code>row_number()</code> resuelve el caso en que el mismo registro cambia varias veces entre dos ejecuciones — nos quedamos únicamente con su estado final.</p>
<pre><code class="language-sql">select * from productos_gold
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6729d2b7863d50b7acfa2d96/cb246edc-b878-457c-92d3-ab779937e9a6.png" alt="" style="display:block;margin:0 auto" />

<p>Puedes ver el notebook completo aquí: <a href="https://github.com/kilianbs/DataGym/blob/main/Microsoft%20Fabric/notebooks/Change%20Data%20Feed/NB_cdf_sample.ipynb">NB_cdf_sample.ipynb</a></p>
<hr />
<h1>Caso de uso 2: SCD Tipo 2 con CDF</h1>
<p>El segundo caso de uso es más sofisticado: implementar <strong>Slowly Changing Dimension Type 2 (SCD2)</strong> aprovechando el preimage/postimage que proporciona CDF.</p>
<p>En SCD2, cada cambio genera una nueva fila en lugar de sobreescribir la existente, manteniendo el historial completo con columnas <code>valid_from</code>, <code>valid_to</code> e <code>is_current</code>.</p>
<p>CDF es ideal para SCD2 porque nos da exactamente lo que necesitamos: el estado anterior (<code>update_preimage</code>) y el nuevo (<code>update_postimage</code>) con el timestamp exacto del cambio.</p>
<h2>Inicialización: snapshot inicial de Silver</h2>
<p>En la primera ejecución no hay historial CDF que procesar — construimos Gold desde el snapshot de Silver en la versión donde se habilitó CDF:</p>
<pre><code class="language-python">SILVER_TABLE = 'productos_silver_scd2'
GOLD_TABLE   = 'productos_gold_scd2'

gold_exists = spark.catalog.tableExists(GOLD_TABLE)
cdf_start_version, cdf_start_timestamp = get_cdf_start_info(SILVER_TABLE)

if not gold_exists:
    # Primera ejecución: leemos el snapshot de silver exactamente en la versión
    # donde se habilitó CDF, usando Delta Time Travel. Así obtenemos el estado
    # inicial de la tabla sin depender de tipos de eventos CDF.
    silver_snapshot = (spark.read
                        .format('delta')
                        .option('versionAsOf', cdf_start_version)
                        .table(SILVER_TABLE))

    gold_df = (silver_snapshot
               .withColumn('valid_from', F.lit(cdf_start_timestamp).cast('timestamp'))
               .withColumn('valid_to',   F.lit(None).cast('timestamp'))
               .withColumn('is_current', F.lit(True)))

    gold_df.write.format('delta').mode('overwrite').saveAsTable(GOLD_TABLE)

    # Guardar watermark: la versión de silver que acabamos de procesar
    spark.sql(f"""
        ALTER TABLE {GOLD_TABLE}
        SET TBLPROPERTIES ('scd2.last_processed_version' = '{cdf_start_version}')
    """)

    print(f"[INIT] Tabla '{GOLD_TABLE}' creada desde snapshot v{cdf_start_version} ({cdf_start_timestamp}).")

else:
    print(f"[SKIP] Tabla '{GOLD_TABLE}' ya existe. Ejecuta el proceso incremental.")

display(spark.table(GOLD_TABLE).orderBy('producto_id'))
</code></pre>
<blockquote>
<p><strong>Patrón watermark:</strong> almacenar la última versión procesada como propiedad de la propia tabla Gold (<code>scd2.last_processed_version</code>) es una buena técnica — el estado del pipeline viaja junto con los datos, sin necesidad de tablas de control externas.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/6729d2b7863d50b7acfa2d96/5803f21e-5efa-443f-9ae1-b125b2366fff.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Simular cambios en Silver</h2>
<pre><code class="language-python"># Actualizar precio del Laptop (bajada de precio)
spark.sql("""
    UPDATE productos_silver_scd2
    SET precio = 849.99, stock = 45
    WHERE producto_id = 1
""")

# Añadir nuevo producto al catálogo
nuevo = spark.createDataFrame([
    Row(5, 'Webcam HD', 'Perifericos', Decimal('59.99'), 180)
], schema)
nuevo.write.format('delta').mode('append').saveAsTable('productos_silver_scd2')

# Eliminar producto descatalogado
spark.sql("DELETE FROM productos_silver_scd2 WHERE producto_id = 4")
</code></pre>
<hr />
<h2>Proceso incremental SCD2</h2>
<p>Una vez existe la tabla gold, el proceso incremental lee el CDF de silver <strong>desde la versión siguiente al watermark</strong> almacenado como propiedad de la tabla gold (<code>scd2.last_processed_version</code>).</p>
<p>Por cada lote de cambios aplica la lógica SCD2 en dos pasos:</p>
<ol>
<li><p><strong>Cerrar registros activos</strong> (<code>is_current = True</code>) en gold para los registros que han sido actualizados (<code>update_preimage</code>) o eliminados (<code>delete</code>) — se les asigna <code>valid_to = _commit_timestamp</code> e <code>is_current = False</code> mediante un MERGE.</p>
</li>
<li><p><strong>Insertar nuevas versiones</strong> en gold para los registros nuevos (<code>insert</code>) o actualizados (<code>update_postimage</code>) — con <code>valid_from = _commit_timestamp</code>, <code>valid_to = null</code> e <code>is_current = True</code>.</p>
</li>
</ol>
<p>Al finalizar, el watermark se actualiza con la última versión procesada.</p>
<pre><code class="language-python">SILVER_TABLE = 'productos_silver_scd2'
GOLD_TABLE   = 'productos_gold_scd2'

# Leer watermark: última versión de silver ya procesada
gold_props        = spark.sql(f"DESCRIBE DETAIL {GOLD_TABLE}").collect()[0]['properties']
last_version      = int(gold_props['scd2.last_processed_version'])
next_version      = last_version + 1

# Versión más reciente disponible en silver
silver_latest     = spark.sql(f"DESCRIBE HISTORY {SILVER_TABLE} LIMIT 1").collect()[0]['version']

if next_version &gt; silver_latest:
    print(f"[SKIP] No hay cambios nuevos en silver desde la versión {last_version}.")
else:
    print(f"Procesando versiones {next_version} – {silver_latest} de silver...")

    cambios_df = (spark.read
                   .format('delta')
                   .option('readChangeFeed', 'true')
                   .option('startingVersion', next_version)
                   .table(SILVER_TABLE)
                   .cache())

    max_version = cambios_df.agg(F.max('_commit_version')).collect()[0][0]

    # ── Paso 1: cerrar registros activos en gold ──────────────────────────────
    # Para cada UPDATE (preimage) y DELETE buscamos el registro con is_current=True
    # y le ponemos valid_to = timestamp del commit en que dejó de ser válido.
    to_close = (cambios_df
                .filter(F.col('_change_type').isin(['update_preimage', 'delete']))
                .select('producto_id', '_commit_timestamp')
                .distinct())

    (DeltaTable.forName(spark, GOLD_TABLE).alias('gold')
     .merge(
         to_close.alias('c'),
         'gold.producto_id = c.producto_id AND gold.is_current = true'
     )
     .whenMatchedUpdate(set={
         'valid_to':   'c._commit_timestamp',
         'is_current': F.lit(False)
     })
     .execute())

    # ── Paso 2: insertar nuevas versiones en gold ─────────────────────────────
    # UPDATE postimage → nueva versión activa del registro modificado.
    # INSERT           → registro completamente nuevo en silver.
    new_records = (cambios_df
                   .filter(F.col('_change_type').isin(['update_postimage', 'insert']))
                   .withColumn('valid_from', F.col('_commit_timestamp'))
                   .withColumn('valid_to',   F.lit(None).cast('timestamp'))
                   .withColumn('is_current', F.lit(True))
                   .drop('_change_type', '_commit_version', '_commit_timestamp'))

    new_records.write.format('delta').mode('append').saveAsTable(GOLD_TABLE)

    # ── Paso 3: actualizar watermark ──────────────────────────────────────────
    spark.sql(f"""
        ALTER TABLE {GOLD_TABLE}
        SET TBLPROPERTIES ('scd2.last_processed_version' = '{max_version}')
    """)

    cambios_df.unpersist()
    print(f"[OK] Versiones {next_version}–{max_version} procesadas.")

display(spark.table(GOLD_TABLE).orderBy('producto_id', 'valid_from'))
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6729d2b7863d50b7acfa2d96/a92e5fe3-bbf5-48eb-8636-6a473d7309da.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Simular múltiples cambios sobre el mismo registro</h2>
<p>El proceso incremental anterior funciona cuando cada producto cambia <strong>una sola vez</strong> por batch. Pero si el mismo <code>producto_id</code> recibe varios UPDATEs entre dos ejecuciones, el MERGE simple falla: después de cerrar el primer registro activo, el segundo preimage no encontrará ningún <code>is_current = True</code> y la cadena de versiones quedará rota.</p>
<p>Simulamos ese caso: dos bajadas de precio consecutivas sobre el producto 1 y un ajuste de stock en el producto 3, los tres en el mismo batch incremental.</p>
<pre><code class="language-python"># Primera bajada de precio del Laptop (commit independiente)
spark.sql("""
    UPDATE productos_silver_scd2
    SET precio = 799.99, stock = 30
    WHERE producto_id = 1
""")

# Segunda bajada de precio del Laptop (otro commit independiente)
spark.sql("""
    UPDATE productos_silver_scd2
    SET precio = 749.99, stock = 20
    WHERE producto_id = 1
""")

# Ajuste de stock en el Teclado
spark.sql("""
    UPDATE productos_silver_scd2
    SET stock = 180
    WHERE producto_id = 3
""")
</code></pre>
<hr />
<h2>Proceso incremental SCD2 con varias versiones</h2>
<p>La solución al problema de múltiples cambios por producto es usar <code>lead()</code> <strong>sobre</strong> <code>_commit_version</code> en lugar del MERGE doble.</p>
<p><strong>Lógica en dos pasos:</strong></p>
<ol>
<li><p><strong>Cerrar el registro activo en gold</strong> usando el timestamp del <strong>primer evento</strong> del batch para ese producto (mínimo <code>_commit_timestamp</code> de preimages/deletes). Esto cierra exactamente una vez independientemente de cuántos cambios haya.</p>
<pre><code class="language-python">cambios_df = (spark.read
                .format('delta')
                .option('readChangeFeed', 'true')
                .option('startingVersion', next_version)
                .table(SILVER_TABLE)
                .cache())

max_version = cambios_df.agg(F.max('_commit_version')).collect()[0][0]

first_event = (cambios_df
                .filter(F.col('_change_type').isin(['update_preimage', 'delete']))
                .groupBy('producto_id')
                .agg(F.min('_commit_timestamp').alias('first_change_ts')))

(DeltaTable.forName(spark, GOLD_TABLE).alias('gold')
     .merge(
         first_event.alias('c'),
         'gold.producto_id = c.producto_id AND gold.is_current = true'
     )
     .whenMatchedUpdate(set={
         'valid_to':   'c.first_change_ts',
         'is_current': F.lit(False)
     })
     .execute())

display(cambios_df.orderBy('_commit_timestamp'))

display(first_event)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6729d2b7863d50b7acfa2d96/5bcd21b4-5887-4430-9359-626c28487b2f.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p><strong>Construir todas las nuevas filas SCD2</strong> a partir de los eventos <code>update_postimage</code> e <code>insert</code>, ordenados por <code>_commit_version</code>. Con <code>lead(_commit_timestamp)</code> calculamos el <code>valid_to</code> de cada fila: es el timestamp del siguiente evento para ese mismo <code>producto_id</code>. Si no hay siguiente evento (<code>lead</code> = null) es la versión activa (<code>is_current = True</code>). Los eventos <code>delete</code> se incluyen en la ventana para propagar su timestamp como <code>valid_to</code> de la última postimage, pero no generan fila propia.</p>
<pre><code class="language-python">w = Window.partitionBy('producto_id').orderBy('_commit_version')

new_rows = (cambios_df
            .filter(F.col('_change_type').isin(['insert', 'update_postimage', 'delete']))
            .withColumn('next_ts', F.lead('_commit_timestamp').over(w))
            .filter(F.col('_change_type') != 'delete')
            .withColumn('valid_from', F.col('_commit_timestamp'))
            .withColumn('valid_to',   F.col('next_ts'))
            .withColumn('is_current', F.col('next_ts').isNull())
            .drop('_change_type', '_commit_version', '_commit_timestamp', 'next_ts'))

new_rows.write.format('delta').mode('append').saveAsTable(GOLD_TABLE)

display(new_rows.orderBy('producto_id', 'valid_from'))
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/6729d2b7863d50b7acfa2d96/aead3446-0b04-4b24-890b-35b043e02e28.png" alt="" style="display:block;margin:0 auto" /></li>
</ol>
<p><strong>Resultado final</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6729d2b7863d50b7acfa2d96/5ca6e595-c30a-408b-87ac-306c55e3ade6.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Reconstrucción completa de gold desde silver</h2>
<p>En lugar de mantener el proceso incremental, a veces es útil (o necesario) reconstruir la tabla gold <strong>desde cero</strong> a partir de todo el historial CDF de silver.</p>
<p>La clave es que los eventos del CDF (<code>insert</code>, <code>update_postimage</code>, <code>delete</code>) forman una <strong>línea de tiempo ordenada por</strong> <code>_commit_version</code> para cada <code>producto_id</code>. Aplicando <code>lead(_commit_timestamp)</code> sobre esa ventana obtenemos directamente el <code>valid_to</code> de cada versión sin necesidad de MERGE ni watermark:</p>
<ul>
<li><p><code>insert</code> / <code>update_postimage</code> → generan una fila en gold con <code>valid_from = _commit_timestamp</code> y <code>valid_to = siguiente evento del mismo producto</code></p>
</li>
<li><p><code>delete</code> → no genera fila, pero su timestamp se propaga como <code>valid_to</code> de la última postimage gracias al <code>lead()</code></p>
</li>
<li><p><code>update_preimage</code> → descartado (es el espejo del postimage anterior, no aporta información nueva)</p>
</li>
</ul>
<p>El resultado es idéntico al que produce el proceso incremental acumulado, pero calculado en un único scan del CDF.</p>
<pre><code class="language-python">SILVER_TABLE    = 'productos_silver_scd2'
GOLD_TABLE_FULL = 'productos_gold_scd2_full'

cdf_start_version, _ = get_cdf_start_info(SILVER_TABLE)

# Leer todo el historial CDF desde la versión inicial
all_events = (spark.read
               .format('delta')
               .option('readChangeFeed', 'true')
               .option('startingVersion', cdf_start_version)
               .table(SILVER_TABLE))

# Ventana por producto ordenada por versión de commit (orden real de los cambios)
w = Window.partitionBy('producto_id').orderBy('_commit_version')

gold_full_df = (all_events
    # Conservamos insert, update_postimage y delete.
    # delete entra en la ventana para que su timestamp llegue como valid_to
    # de la última postimage/insert, pero no generará fila propia.
    .filter(F.col('_change_type').isin(['insert', 'update_postimage', 'delete']))
    # valid_to = timestamp del siguiente evento para este producto_id (o null si es el último)
    .withColumn('next_ts', F.lead('_commit_timestamp').over(w))
    # Eliminar los delete (no generan fila en gold)
    .filter(F.col('_change_type') != 'delete')
    .withColumn('valid_from', F.col('_commit_timestamp'))
    .withColumn('valid_to',   F.col('next_ts'))
    .withColumn('is_current', F.col('next_ts').isNull())
    .drop('_change_type', '_commit_version', '_commit_timestamp', 'next_ts'))

gold_full_df.write.format('delta').mode('overwrite').saveAsTable(GOLD_TABLE_FULL)

print(f"Tabla '{GOLD_TABLE_FULL}' generada con {gold_full_df.count()} filas.")
display(spark.table(GOLD_TABLE_FULL).orderBy('producto_id', 'valid_from'))
</code></pre>
<p>Puedes ver el notebook completo aquí: <a href="https://github.com/kilianbs/DataGym/blob/main/Microsoft%20Fabric/notebooks/Change%20Data%20Feed/NB_cdf_scd2.ipynb">NB_cdf_scd2.ipynb</a></p>
<hr />
<h1><strong>VACUUM y CDF: una convivencia que hay que gestionar</strong></h1>
<p>Una de las operaciones de mantenimiento más habituales en Delta Lake es VACUUM, que elimina los archivos físicos que ya no son necesarios para la versión actual de la tabla. En tablas con CDF activo, esta operación tiene consecuencias criticas que hay que entender bien antes de ejecutarla.</p>
<p>⚠️<strong>El problema central</strong></p>
<p>VACUUM elimina tanto los archivos de datos de versiones antiguas como los archivos de la carpeta <code>changedata</code>. Una vez ejecutado, es imposible recuperar esos cambios: no existe ninguna operacion de rollback para datos fisicamente borrados.</p>
<p>CDF y VACUUM compiten directamente: CDF necesita el historial para ser util; VACUUM lo elimina para ahorrar almacenamiento.</p>
<h2>Que ocurre si VACUUM elimina versiones que necesita CDF</h2>
<pre><code class="language-python"># Despues de ejecutar VACUUM, esta lectura puede FALLAR:
spark.read \
    .format('delta') \
    .option('readChangeFeed', 'true') \
    .option('startingVersion', 0) \
    .table('productos_silver')

# Error: Error getting change data for range [0, 5].
# The provided starting version 0 is older than the
# earliest available version for this table.
</code></pre>
<h2>Controlar la retención: dos propiedades clave</h2>
<p>Delta Lake tiene dos propiedades distintas que controlan la retención. Ambas deben estar alineadas: si solo amplias el log pero no los archivos fisicos, el log registrara versiones para las que ya no existen datos:</p>
<pre><code class="language-python"># Ver la configuracion actual
spark.sql('DESCRIBE DETAIL productos_silver') \
     .select('properties') \
     .show(truncate=False)

# Ampliar retencion a 90 dias
spark.sql("""
    ALTER TABLE productos_silver
    SET TBLPROPERTIES (
        delta.logRetentionDuration         = 'interval 90 days',
        delta.deletedFileRetentionDuration = 'interval 90 days'
    )
""")

# delta.logRetentionDuration      -&gt; conserva entradas del transaction log
# delta.deletedFileRetentionDuration -&gt; conserva archivos fisicos eliminados incluida la carpeta _change_data)
</code></pre>
<h2>Validar la versión disponible antes de leer CDF</h2>
<p>Es recomendable comprobar que la versión de inicio del CDF sigue disponible antes de lanzar el pipeline, especialmente en entornos donde VACUUM se ejecuta de forma automática:</p>
<pre><code class="language-python">def get_oldest_available_version(table_name: str) -&gt; int:
    """
    Devuelve la version mas antigua disponible en el historial.
    Util para detectar si VACUUM ha eliminado parte del historial CDF.
    """
    history_df = spark.sql(f'DESCRIBE HISTORY {table_name}')
    return history_df.agg({'version': 'min'}).collect()[0][0]


oldest = get_oldest_available_version('productos_silver')
print(f'Version mas antigua disponible: {oldest}')

if cdf_start_version &lt; oldest:
    raise ValueError(
        f'La version de inicio del CDF ({cdf_start_version}) ya no esta disponible. '
        f'La version mas antigua en el historial es {oldest}. '
        f'Es posible que VACUUM haya eliminado parte del historial.'
    )
</code></pre>
<hr />
<h1>Recomendaciones y buenas prácticas</h1>
<h2>1. Activa CDF desde el inicio del ciclo de vida de la tabla</h2>
<p>CDF no captura el historial previo a su activación. Actívalo al crear la tabla o lo antes posible. En Fabric, usa la configuración global de sesión para que todas las tablas nuevas lo hereden:</p>
<pre><code class="language-python">spark.conf.set('spark.microsoft.delta.properties.defaults.enableChangeDataFeed', 'true')
</code></pre>
<p>O bien pásalo como opción al escribir:</p>
<pre><code class="language-python">df.write.format('delta').option('delta.enableChangeDataFeed', 'true').saveAsTable('mi_tabla')
</code></pre>
<h2>2. Usa siempre <code>startingVersion</code>, no <code>startingTimestamp</code>, en procesos incrementales</h2>
<p>Los timestamps pueden tener problemas de zona horaria o precisión. La versión del log de Delta es determinista e inequívoca. Guarda el watermark como número de versión.</p>
<h2>3. Verifica siempre si hay cambios nuevos antes de leer el CDF</h2>
<p>Antes de llamar a <code>readChangeFeed</code>, comprueba que realmente hay versiones nuevas:</p>
<pre><code class="language-python">silver_latest = spark.sql(f"DESCRIBE HISTORY {SILVER_TABLE} LIMIT 1").collect()[0]['version']
if next_version &gt; silver_latest:
    print("[SKIP] No hay cambios nuevos.")
</code></pre>
<p>Esto evita errores cuando no hay nada que procesar.</p>
<h2>4. Filtra <code>update_preimage</code> lo antes posible</h2>
<p>En la mayoría de casos de uso (sincronización simple, upserts), el <code>update_preimage</code> no es necesario. Filtrarlo lo antes posible reduce el volumen de datos que Spark tiene que manejar. En SCD2 sí lo necesitas, pero solo para extraer el timestamp del primer evento.</p>
<h2>5. Usa <code>cache()</code> cuando el DataFrame de CDF se lee múltiples veces</h2>
<p>En el proceso SCD2, el DataFrame de cambios se usa en dos pasos (cerrar registros + insertar nuevos). Cachearlo evita releer el CDF dos veces:</p>
<pre><code class="language-python">cambios_df = (spark.read
               .format('delta')
               .option('readChangeFeed', 'true')
               .option('startingVersion', next_version)
               .table(SILVER_TABLE)
               .cache())

# resto de código

cambios_df.unpersist()
</code></pre>
<h2>6. Usa el patrón <code>lead()</code> para manejar múltiples cambios en el mismo batch</h2>
<p>El MERGE simple de SCD2 solo funciona cuando cada registro cambia una vez por batch. Para el caso general, el patrón con <code>lead(_commit_timestamp)</code> sobre una ventana ordenada por <code>_commit_version</code> es la solución correcta y escalable.</p>
<h2>7. Ten en cuenta el coste de almacenamiento</h2>
<p>CDF genera archivos adicionales de cambios en <code>_change_data/</code> dentro del directorio de la tabla. En tablas con alta frecuencia de escritura, esto puede aumentar el almacenamiento significativamente. Evalúa si necesitas CDF en todas las tablas o solo en las que son fuente de otros procesos.</p>
<h2>8. CDF y VACUUM: precaución con la retención</h2>
<p>Si ejecutas <code>VACUUM</code> con una retención baja, puedes perder archivos de cambios que todavía no has procesado. Asegúrate de que la retención de VACUUM sea mayor que el intervalo máximo entre ejecuciones de tu pipeline.</p>
]]></content:encoded></item><item><title><![CDATA[Cómo usar Workspace Identity para la autenticación en Microsoft Fabric]]></title><description><![CDATA[Workspace Identity es una identidad gestionada automáticamente que Microsoft Fabric asocia a un workspace. Es esencialmente un service principal que se crea en Microsoft Entra ID (Azure AD) sin que te]]></description><link>https://datagym.es/c-mo-usar-workspace-identity-para-la-autenticaci-n-en-microsoft-fabric</link><guid isPermaLink="true">https://datagym.es/c-mo-usar-workspace-identity-para-la-autenticaci-n-en-microsoft-fabric</guid><category><![CDATA[microsoft fabric]]></category><category><![CDATA[workspace identity]]></category><category><![CDATA[data-engineering]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Thu, 05 Mar 2026 08:56:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769699394299/2bfad5d3-8de3-40e1-92a7-9b116869fc02.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Workspace Identity</strong> es una identidad gestionada automáticamente que Microsoft Fabric asocia a un workspace. Es esencialmente un <strong>service principal</strong> que se crea en Microsoft Entra ID (Azure AD) sin que tengas que gestionar manualmente credenciales, secretos o certificados.</p>
<h3>Ventajas principales</h3>
<ul>
<li><p>✅ <strong>Sin gestión de credenciales</strong>: No hay contraseñas, secretos ni certificados que mantener</p>
</li>
<li><p>✅ <strong>Sin expiración</strong>: Las credenciales no caducan, evitando interrupciones en pipelines productivos</p>
</li>
<li><p>✅ <strong>Mayor seguridad</strong>: Elimina el riesgo de fugas de credenciales en código o configuraciones</p>
</li>
<li><p>✅ <strong>Autenticación unificada</strong>: Usa Microsoft Entra ID para todos los servicios compatibles</p>
</li>
</ul>
<hr />
<h1>Cómo crear Workspace Identity</h1>
<p>Ve a tu workspace en Fabric y haz clic en el icono de <strong>engranaje</strong> (⚙️) para abrir <strong>Workspace Settings</strong></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769605821341/0f0a65f8-e236-46ac-be8b-aed46f46ba5d.png" alt="" style="display:block;margin:0 auto" />

<p>Selecciona la pestaña <strong>Workspace identity</strong> y haz clic en <strong>+ Workspace identity</strong></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769605902736/b65d5551-2b8e-4a05-b2b5-481946b2168c.png" alt="" style="display:block;margin:0 auto" />

<h2>¿Qué ocurre en segundo plano?</h2>
<p>Al crear la Workspace Identity, Fabric automáticamente:</p>
<ol>
<li><p><strong>Registra una aplicación empresarial</strong> (Enterprise Application) en Microsoft Entra ID</p>
</li>
<li><p>El service principal se crea con el <strong>mismo nombre que tu workspace</strong></p>
</li>
<li><p>Puedes encontrarlo en Azure Portal → Microsoft Entra ID → Enterprise Applications</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769606344628/6f3b50fd-ca09-4d56-8c7c-a5d2106e2b65.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>⚠️ <strong>Importante</strong>: La creación del service principal NO otorga permisos automáticos sobre ningún recurso de Azure. Debes configurar los permisos manualmente para cada servicio.</p>
</blockquote>
<h2>El flujo de trabajo completo</h2>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769946237225/737b06be-9308-4442-8841-9a5cb669c222.jpeg" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Caso de uso 1: Azure Storage Account</h1>
<h2>Escenario</h2>
<p>Quieres leer datos en un Azure Data Lake Storage Gen2 (ADLS Gen2) desde tus pipelines de Fabric.</p>
<h2>Paso 1: Crear Workspace Identity</h2>
<p>Ya cubierto en la sección anterior.</p>
<h2>Paso 2: Asignar permisos en Azure Storage</h2>
<ol>
<li><p>Ve a tu <strong>Storage Account</strong> en Azure Portal</p>
</li>
<li><p>En el menú lateral, selecciona <strong>Access Control (IAM)</strong></p>
</li>
<li><p>Haz clic en <strong>+ Add</strong> → <strong>Add role assignment</strong></p>
 <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769612610701/df6e226c-8e36-4e57-afe0-74713974f187.png" alt="" />
 </li>
<li><p>Selecciona el rol apropiado:</p>
<ul>
<li><strong>Storage Blob Data Reader</strong>: Solo lectura</li>
</ul>
</li>
<li><p>En la pestaña <strong>Members</strong>, haz clic en <strong>+ Select members</strong></p>
</li>
<li><p>Busca tu workspace por nombre (el service principal tiene el mismo nombre)</p>
 <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769612774576/1c967d3a-fe67-448a-91d0-df9d1cb30d75.png" alt="" />
 </li>
<li><p>Selecciónalo y haz clic en <strong>Select</strong></p>
</li>
<li><p>Haz clic en <strong>Review + assign</strong></p>
</li>
</ol>
<h2>Paso 3: Creación de la conexión</h2>
<p>Una vez asignados los permisos, se debe de crear la conexión en Fabric.</p>
<ol>
<li><p>Ve a <strong>Manage connections and gateways</strong></p>
 <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769618721681/9c6d02fb-d37f-4e4f-a75d-ec014d62d218.png" alt="" style="display:block;margin:0 auto" />
 </li>
<li><p>Haz clic en <strong>+ New connection</strong> → <strong>Cloud</strong></p>
</li>
<li><p>Selecciona <strong>Azure Data Lake Storage Gen2</strong></p>
</li>
<li><p>Completa los datos:</p>
<ul>
<li><p><strong>Connection name</strong>: Nombre descriptivo (ej: "ADLS-WorkspaceIdentity")</p>
</li>
<li><p><strong>Account name or URL</strong>: Tu storage account URL (<a href="https://mystorageaccount.dfs.core.windows.net"><code>https://mystorageaccount.dfs.core.windows.net</code></a>)</p>
</li>
<li><p><strong>Authentication method</strong>: Selecciona <strong>Workspace identity</strong></p>
</li>
</ul>
<ol>
<li>Haz clic en <strong>Create</strong></li>
</ol>
</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769619301715/8cffde66-fe05-469b-9c49-1e4257e29a11.png" alt="" style="display:block;margin:0 auto" />

<h2>Paso 4: Usar Workspace Identity en una Pipeline</h2>
<p>En tu Pipeline:</p>
<ol>
<li><p>Crea una actividad "Copy data"</p>
</li>
<li><p>En Source:</p>
<ul>
<li><p><strong>Connection</strong>: Selecciona la conexión creada anteriormente</p>
</li>
<li><p><strong>File path</strong>: Container y carpeta (ej: contoso-csv-1m/product.csv)</p>
  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769951134180/d0c4fca7-7c90-4c59-9154-2b811b9e0bc6.png" alt="" style="display:block;margin:0 auto" /></li>
</ul>
</li>
<li><p>En Destination:</p>
<ul>
<li><p>Selecciona tu Lakehouse</p>
</li>
<li><p>Tabla destino (Nueva o ya existente)</p>
</li>
</ul>
</li>
<li><p>Ejecuta el pipeline</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769620420463/7a705466-f9e4-46d9-a7c6-7f4920891653.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769620490439/60f02bc8-5e4f-46dc-be47-504ca1e4f5de.png" alt="" style="display:block;margin:0 auto" />

<h2>Bonus: OneLake Shortcut con Workspace Identity</h2>
<ol>
<li><p>Ve a tu <strong>Lakehouse</strong> en Fabric</p>
</li>
<li><p>Haz clic en <strong>New shortcut</strong></p>
</li>
<li><p>Selecciona <strong>Azure Data Lake Storage Gen2</strong></p>
 <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769620576177/2882e428-c68f-4a69-905d-0b2cc341cfe5.png" alt="" style="display:block;margin:0 auto" />
 </li>
<li><p>Selecciona la conexión creada anteriormente</p>
 <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769620606656/f2b1bb88-0789-42dd-9ddc-8cb7d8b652ad.png" alt="" style="display:block;margin:0 auto" />
 </li>
<li><p>Haz clic en <strong>Create</strong></p>
</li>
</ol>
<p>Ahora puedes navegar por los datos del storage como si estuvieran en tu Lakehouse, sin moverlos físicamente.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769620747844/559d12ed-91c1-434b-8f26-382eef9c69d1.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Caso de uso 2: Azure SQL Database</h1>
<h2>Paso 1: Crear Workspace Identity</h2>
<p>Ya cubierto anteriormente.</p>
<h2>Paso 2: Dar permisos en Azure SQL Database</h2>
<p>Debes crear un <strong>usuario de base de datos</strong> que represente al service principal y asignarle los permisos necesarios.</p>
<h3>Conectar a la base de datos</h3>
<p>Usa SQL Server Management Studio (SSMS), Azure Data Studio o el Query Editor de Azure Portal para conectarte a tu base de datos con una cuenta de administrador.</p>
<h3>Ejecutar comandos SQL</h3>
<pre><code class="language-sql">-- Crear el usuario para el service principal
-- El nombre debe coincidir EXACTAMENTE con el nombre de tu workspace
CREATE USER [DataGym] FROM EXTERNAL PROVIDER;

-- Asignar roles de base de datos
-- Para solo lectura:
ALTER ROLE db_datareader ADD MEMBER [DataGym];

-- Para ejecutar stored procedures:
GRANT EXECUTE TO [DataGym];

-- Verificar que el usuario se creó correctamente
SELECT name, type_desc, authentication_type_desc 
FROM sys.database_principals 
WHERE name = 'DataGym';
</code></pre>
<blockquote>
<p>💡 <strong>Tip</strong>: Si el nombre de tu workspace contiene caracteres especiales, asegúrate de usar corchetes <code>[]</code> alrededor del nombre.</p>
</blockquote>
<h2>Paso 3: Crear conexión</h2>
<ol>
<li><p>En Fabric, ve a <strong>Manage connections and gateways</strong></p>
</li>
<li><p>Haz clic en <strong>+ New connection</strong> → <strong>Cloud</strong></p>
</li>
<li><p>Selecciona <strong>SQL Server</strong></p>
</li>
<li><p>Completa:</p>
<ul>
<li><p><strong>Connection name</strong>: "AzureSQL-WorkspaceIdentity"</p>
</li>
<li><p><strong>Server</strong>: <a href="http://myserver.database.windows.net"><code>myserver.database.windows.net</code></a></p>
</li>
<li><p><strong>Database</strong>: Nombre de tu base de datos</p>
</li>
<li><p><strong>Authentication method</strong>: <strong>Workspace identity</strong></p>
</li>
</ul>
</li>
<li><p>Haz clic en <strong>Create</strong></p>
</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769954474752/e5e146ed-0949-443b-8ef6-2346dfbd2be3.png" alt="" style="display:block;margin:0 auto" />

<h2>Paso 4: Usar Workspace Identity en una Pipeline</h2>
<p>En tu Pipeline:</p>
<ol>
<li><p>Crea una actividad "Copy data"</p>
</li>
<li><p>En Source:</p>
<ul>
<li><p><strong>Connection</strong>: Selecciona la conexión creada anteriormente</p>
  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769958661990/2ec98df1-56ba-419d-9145-ca2ed3da66d0.png" alt="" style="display:block;margin:0 auto" /></li>
</ul>
</li>
<li><p>En Destination:</p>
<ul>
<li><p>Selecciona tu Lakehouse</p>
</li>
<li><p>Tabla destino (Nueva o ya existente)</p>
</li>
</ul>
</li>
</ol>
<p>Ejecuta el pipeline</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769958793790/0fd39e9e-c89b-40fe-ac6e-3ca2e5d46d60.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Gestión Avanzada</h1>
<h2>Ver todas las Workspace Identities del tenant</h2>
<ol>
<li><p>Ve a <strong>Admin Portal</strong> en Fabric</p>
</li>
<li><p>Selecciona <strong>Fabric identities</strong></p>
</li>
<li><p>Aquí verás todas las identidades creadas y podrás:</p>
<ul>
<li><p>Ver detalles del service principal</p>
</li>
<li><p>Eliminar identidades (⚠️ acción irreversible)</p>
</li>
</ul>
</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769959118268/59311bac-9730-492e-820d-cce53b47f9fb.png" alt="" style="display:block;margin:0 auto" />

<h2>Auditoría</h2>
<p>Los eventos de creación y eliminación de Workspace Identities se registran en <strong>Microsoft Purview Audit Log</strong>:</p>
<ul>
<li><p><strong>Evento de creación</strong>: <code>WorkspaceIdentityCreated</code></p>
</li>
<li><p><strong>Evento de eliminación</strong>: <code>WorkspaceIdentityDeleted</code></p>
</li>
</ul>
<h2>Asignar rol al área de trabajo para automatizaciones</h2>
<p>Desde el 27 de julio de 2025, las nuevas Workspace Identities <strong>ya no tienen el rol Contributor asignado automáticamente</strong> sobre el workspace.</p>
<p><strong>Impacto</strong>: Si necesitas que la Workspace Identity tenga permisos dentro del workspace de Fabric (por ejemplo, para automatización), debes asignarlos explícitamente:</p>
<ol>
<li><p>Ve a <strong>Workspace Settings</strong> → <strong>Manage access</strong></p>
</li>
<li><p>Haz clic en <strong>+ Add people or groups</strong></p>
</li>
<li><p>Busca el nombre de tu workspace (el service principal)</p>
</li>
<li><p>Asigna el rol apropiado (Viewer, Contributor, etc.)</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769959234081/41fb786b-6453-448b-bb14-fc42a3551b84.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h1>Conclusión</h1>
<p><strong>Workspace Identity</strong> simplifica radicalmente la autenticación en Microsoft Fabric al:</p>
<ul>
<li><p>Eliminar la gestión manual de credenciales</p>
</li>
<li><p>Centralizar la autenticación en Microsoft Entra ID</p>
</li>
<li><p>Reducir riesgos de seguridad</p>
</li>
</ul>
<p>El flujo es siempre el mismo:</p>
<ol>
<li><p><strong>Crear</strong> la Workspace Identity en Fabric</p>
</li>
<li><p><strong>Asignar permisos</strong> al service principal en Azure/servicios externos</p>
</li>
<li><p><strong>Utilizar</strong> en tus conexiones seleccionando "Workspace identity"</p>
</li>
</ol>
<p>A medida que Microsoft expande el soporte a más conectores y artefactos, Workspace Identity se está convirtiendo en el <strong>método de autenticación estándar</strong> para entornos productivos de Fabric.</p>
<hr />
<h1>Referencias</h1>
<p><a href="https://learn.microsoft.com/es-es/fabric/data-factory/connector-overview#supported-connectors-in-fabric">Identidad del área de trabajo - Microsoft Fabric | M</a><a href="https://learn.microsoft.com/es-es/fabric/security/workspace-identity">icrosoft Learn</a></p>
<p><a href="https://learn.microsoft.com/es-es/fabric/security/workspace-identity-authenticate">Autenticación con la identidad del área de trabajo de Microsoft Fabric - Microsoft Fabric | Microsoft Learn</a></p>
<p><a href="https://learn.microsoft.com/es-es/fabric/data-factory/connector-overview#supported-connectors-in-fabric">Introducción a los conectores - Microsoft Fabric | Microsoft Learn</a></p>
]]></content:encoded></item><item><title><![CDATA[Comparativa de Consumo de CUs: Por qué elegir el artefacto incorrecto puede costarte miles de euros al año]]></title><description><![CDATA[Microsoft Fabric utiliza un modelo de consumo basado en Capacity Units (CUs) para medir y facturar el uso de recursos computacionales. Comprender cómo diferentes artefactos y escenarios de ingesta de datos impactan en el consumo de CUs es fundamental...]]></description><link>https://datagym.es/comparativa-de-consumo-de-cus-por-que-elegir-el-artefacto-incorrecto-puede-costarte-miles-de-euros-al-ano</link><guid isPermaLink="true">https://datagym.es/comparativa-de-consumo-de-cus-por-que-elegir-el-artefacto-incorrecto-puede-costarte-miles-de-euros-al-ano</guid><category><![CDATA[Capacity units]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[dataengineering]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Wed, 18 Feb 2026 10:19:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1771168175482/0dcb686e-fbf1-4aad-8245-c87ded564950.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Microsoft Fabric utiliza un modelo de consumo basado en <strong>Capacity Units (CUs)</strong> para medir y facturar el uso de recursos computacionales. Comprender cómo diferentes artefactos y escenarios de ingesta de datos impactan en el consumo de CUs es fundamental para optimizar costos y seleccionar la herramienta más eficiente para cada caso de uso.</p>
<p>Este artículo presenta una <strong>comparativa práctica y detallada</strong> del consumo de CUs en Microsoft Fabric al cargar datos desde diferentes orígenes y formatos utilizando los principales artefactos disponibles en la plataforma. Los objetivos específicos son:</p>
<ul>
<li><p><strong>Medir el consumo real de CUs</strong> en escenarios de ingesta de datos</p>
</li>
<li><p><strong>Comparar el rendimiento</strong> de diferentes artefactos: Notebooks (PySpark y Python), Pipelines, Copy Jobs y Dataflows Gen2</p>
</li>
<li><p><strong>Evaluar el impacto</strong> del tamaño de archivo en el consumo de recursos</p>
</li>
<li><p><strong>Analizar las diferencias</strong> entre datos estructurados (SQL Server) y ficheros</p>
</li>
<li><p><strong>Calcular el coste económico real</strong> de cada operación</p>
</li>
</ul>
<h3 id="heading-metodologia">Metodología</h3>
<p>Para cada escenario se han capturado las siguientes métricas mediante la aplicación <strong>Microsoft Fabric Capacity Metrics</strong>:</p>
<ul>
<li><p><strong>CUs consumidas</strong>: Total de Capacity Units utilizadas en la operación</p>
</li>
<li><p><strong>Duración</strong>: Tiempo de ejecución en segundos</p>
</li>
<li><p><strong>Coste</strong>: Coste económico calculado en base a la duración y el precio de la capacidad</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text"><strong>Importante</strong>: Todas las pruebas realizadas corresponden a <strong>cargas completas (full load)</strong> de datos desde el origen hasta el Lakehouse de Microsoft Fabric, sin ningún tipo de optimización de carga. No se han utilizado técnicas de carga incremental, Change Data Capture (CDC), filtrado de datos ni transformaciones de ningún tipo. El objetivo es medir el <strong>coste base del movimiento de datos puro</strong> entre origen y destino, lo que representa el escenario más comparable y reproducible entre artefactos.</div>
</div>

<h3 id="heading-configuracion-de-la-capacidad">Configuración de la Capacidad</h3>
<p>Para estas pruebas se ha utilizado una capacidad <strong>F2</strong> en modo <strong>Pay-as-you-go</strong> en la región <strong>Spain Central</strong>, con un precio de <strong>€0.323/hora</strong> según la página oficial de <a target="_blank" href="https://azure.microsoft.com/en-us/pricing/details/microsoft-fabric/">Microsoft Fabric - Pricing | Microsoft Azure</a>.</p>
<p><strong>Cálculo del coste:</strong></p>
<p>Coste (€) = (Duración en segundos × €0.323) / 3600</p>
<hr />
<h1 id="heading-escenario-1-ingesta-de-fichero-csv">Escenario 1: Ingesta de Fichero CSV</h1>
<h2 id="heading-dataset-utilizado">Dataset utilizado</h2>
<p>Archivo de ventas (sales.csv)</p>
<p><strong>Características:</strong></p>
<ul>
<li><p><strong>Ubicación</strong>: Lakehouse de Microsoft Fabric</p>
</li>
<li><p><strong>Nombre del archivo</strong>: sales.csv</p>
</li>
<li><p><strong>Tamaño del archivo</strong>: 189 MB</p>
</li>
<li><p><strong>Tipo</strong>: Datos de ventas en formato CSV</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770119539451/0628b4b4-a209-4f4d-9a88-18e85ec359f5.png" alt class="image--center mx-auto" /></p>
<p><strong>Operación realizada</strong>: Lectura de todos los ficheros CSV y escritura en tabla Delta sin transformaciones ni limpiezas. Operación pura de lectura y escritura para medir el consumo base.</p>
<h2 id="heading-artefactos-probados">Artefactos probados</h2>
<ul>
<li><p>Notebook (PySpark)</p>
</li>
<li><p>Notebook Python</p>
</li>
<li><p>Pipeline</p>
</li>
<li><p>Copy job</p>
</li>
<li><p>Dataflow Gen2</p>
</li>
</ul>
<h2 id="heading-resultados">Resultados</h2>
<p>Revisando el informe de Microsoft Fabric Capacity Metrics:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770119050088/40216511-e091-4a00-bacf-d80eccc4dbbe.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-coste-economico"><strong>Coste económico</strong></h3>
<p>El impacto económico para archivos pequeños es mínimo en términos absolutos, pero las diferencias porcentuales son significativas:</p>
<ul>
<li><p><strong>Pipeline</strong>: €0.0035 por ejecución - <strong>El más económico por tiempo</strong></p>
</li>
<li><p><strong>Notebook Python</strong>: €0.0038 por ejecución - 9% más que Pipeline</p>
</li>
<li><p><strong>Copy Job</strong>: €0.0042 por ejecución - 20% más que Pipeline</p>
</li>
<li><p><strong>Dataflow Gen2</strong>: €0.0047 por ejecución - 34% más que Pipeline</p>
</li>
<li><p><strong>Notebook PySpark</strong>: €0.0060 por ejecución - 71% más que Pipeline</p>
</li>
</ul>
<h3 id="heading-consumo-de-cus-y-tiempos-de-ejecucion"><strong>Consumo de CUs y tiempos de ejecución</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771260705778/5441131c-facd-4d3b-9f7b-c4c6d5fa9fc3.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">En archivos pequeños, <strong>Notebook Python</strong> <strong>es el más eficiente</strong> en CUs (76% menos que Pipeline), mientras que <strong>Pipeline es el más rápido</strong> (9% más rápido que Python).</div>
</div>

<h3 id="heading-comparativa-pyspark-vs-python-pandas">Comparativa PySpark vs Python (Pandas)</h3>
<p>Es importante destacar la diferencia entre los dos tipos de Notebooks:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Característica</td><td>Notebook PySpark</td><td>Notebook Python</td><td>Diferencia</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Motor de procesamiento</strong></td><td>Apache Spark (distribuido)</td><td>Pandas (single-node)</td><td>-</td></tr>
<tr>
<td><strong>CUs consumidas</strong></td><td>267.81</td><td>85.15</td><td><strong>-68%</strong> ✅</td></tr>
<tr>
<td><strong>Tiempo de ejecución</strong></td><td>66.95s</td><td>42.58s</td><td><strong>-36%</strong> ✅</td></tr>
<tr>
<td><strong>Throughput</strong></td><td>169.18 MB/min</td><td>266.01 MB/min</td><td><strong>+57%</strong> ✅</td></tr>
<tr>
<td><strong>Coste</strong></td><td>€0.0060</td><td>€0.0038</td><td><strong>-37%</strong> ✅</td></tr>
<tr>
<td><strong>Overhead inicial</strong></td><td>Alto (inicialización Spark)</td><td>Bajo (ejecución directa)</td><td>-</td></tr>
<tr>
<td><strong>Mejor para archivos</strong></td><td>&gt; 5 GB</td><td>&lt; 1 GB</td><td>-</td></tr>
<tr>
<td><strong>Paralelización</strong></td><td>Automática multi-nodo</td><td>Single-threaded</td><td>-</td></tr>
<tr>
<td><strong>Escalabilidad</strong></td><td>Excelente</td><td>Limitada por memoria</td><td>-</td></tr>
</tbody>
</table>
</div><p>Para archivos pequeños, <strong>Python/Pandas es claramente superior</strong> en todos los aspectos medibles: CUs, tiempo, throughput y coste. El overhead de inicialización y coordinación de Spark (sesión, workers, distribución de tareas) solo se justifica con volúmenes grandes donde su capacidad de procesamiento distribuido paralelo compensa estos costes iniciales.</p>
<hr />
<h1 id="heading-escenario-2-ingesta-de-fichero-csv-de-gran-volumen">Escenario 2: Ingesta de fichero CSV de gran volumen</h1>
<h2 id="heading-dataset-utilizado-1">Dataset utilizado</h2>
<p>Archivo unificado de CMS (cms_unificado.csv)</p>
<p><strong>Características:</strong></p>
<ul>
<li><p><strong>Ubicación</strong>: Lakehouse de Microsoft Fabric</p>
</li>
<li><p><strong>Nombre del archivo</strong>: cms_unificado.csv</p>
</li>
<li><p><strong>Tamaño del archivo</strong>: 83.76 GB</p>
</li>
<li><p><strong>Tipo</strong>: Datos de CMS consolidados en formato CSV</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770119539451/0628b4b4-a209-4f4d-9a88-18e85ec359f5.png" alt class="image--center mx-auto" /></p>
<p><strong>Operación realizada</strong>: Lectura del fichero CSV de gran volumen y escritura en tabla Delta sin transformaciones ni limpiezas. Operación pura de lectura y escritura para medir el consumo base en escenarios de producción con grandes volúmenes.</p>
<h2 id="heading-artefactos-probados-1">Artefactos probados</h2>
<p>Los mismos que en el escenario anterior:</p>
<ul>
<li><p>Notebook (PySpark)</p>
</li>
<li><p>Notebook Python</p>
</li>
<li><p>Pipeline (Copy data activity)</p>
</li>
<li><p>Copy job</p>
</li>
<li><p>Dataflow Gen2</p>
</li>
</ul>
<h2 id="heading-resultados-1">Resultados</h2>
<p>Revisando el informe de Microsoft Fabric Capacity Metrics:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770215767111/619ac689-3045-443c-89ac-c9fb0cf8c106.png" alt class="image--center mx-auto" /></p>
<p>El notebook de python no se ha podido evaluar debido al siguiente error:</p>
<p>❌ <strong>ERROR - Forced-process termination</strong></p>
<pre><code class="lang-python">Error exit code: <span class="hljs-number">-9</span> (Forced-process termination. 
This <span class="hljs-keyword">is</span> often caused by insufficient memory causing the process to be killed. 
Please check memory usage)
</code></pre>
<p><strong>Análisis del error</strong>: Pandas opera en un único nodo cargando todo el dataset en memoria RAM. Con un archivo de 83.76 GB, la memoria disponible en la capacidad F2 es insuficiente, resultando en la terminación forzada del proceso por el sistema operativo (OOM - Out Of Memory).</p>
<h3 id="heading-coste-economico-1"><strong>Coste económico</strong></h3>
<p>El impacto económico en archivos grandes es <strong>sustancial</strong> y debe ser considerado seriamente:</p>
<ul>
<li><p><strong>Notebook PySpark</strong>: €0.0610 por ejecución - <strong>El más económico</strong> 💰</p>
</li>
<li><p><strong>Pipeline</strong>: €0.3135 por ejecución - <strong>5.14x más caro que PySpark</strong></p>
</li>
<li><p><strong>Copy Job</strong>: €0.3158 por ejecución - <strong>5.18x más caro que PySpark</strong></p>
</li>
<li><p><strong>Dataflow Gen2</strong>: €0.3748 por ejecución - <strong>6.15x más caro que PySpark</strong></p>
</li>
</ul>
<h3 id="heading-consumo-de-cus-y-tiempos-de-ejecucion-1"><strong>Consumo de CUs y tiempos de ejecución</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771260854347/ac531d22-a802-47ed-b002-adb2cb6856df.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text">En archivos grandes, <strong>PySpark es el campeón absoluto</strong>: consume 87% menos CUs, es 5x más rápido y cuesta 5x menos que Pipeline.</div>
</div>

<p>Con archivos de gran volumen, las diferencias en consumo de CUs y tiempos de ejecución se vuelven <strong>dramáticamente significativas</strong></p>
<p>El <strong>Notebook PySpark</strong> consume menos de <strong>1/8 de las CUs</strong> del Pipeline y menos de <strong>1/25</strong> del Dataflow Gen2. Además, completa la carga en <strong>11 minutos</strong>, mientras que los demás artefactos tardan <strong>entre 58 y 70 minutos</strong> (alrededor de 1 hora).</p>
<h2 id="heading-conclusiones-del-escenario">Conclusiones del escenario</h2>
<h3 id="heading-notebook-de-python-no-aplicable">Notebook de Python no aplicable</h3>
<p>El Notebook Python/Pandas <strong>no es escalable</strong> para grandes volúmenes. Su eficiencia en archivos pequeños no se traslada a escenarios con datasets grandes.</p>
<h3 id="heading-superioridad-de-pyspark-en-grandes-volumenes-de-datos">Superioridad de PySpark en grandes volúmenes de datos</h3>
<p>El <strong>Notebook PySpark</strong> demuestra su verdadero valor en archivos grandes, invirtiendo completamente los resultados del Escenario 1:</p>
<p><strong>🏆 Ventajas decisivas de PySpark:</strong></p>
<ol>
<li><p><strong>Arquitectura distribuida</strong>: Distribuye el procesamiento entre múltiples workers en paralelo</p>
</li>
<li><p><strong>Procesamiento lazy</strong>: Solo carga en memoria lo necesario en cada momento</p>
</li>
<li><p><strong>Particionamiento automático</strong>: Divide el archivo en chunks procesables independientemente</p>
</li>
<li><p><strong>Optimización nativa para Delta</strong>: Escritura optimizada a formato Delta Lake</p>
</li>
<li><p><strong>Gestión eficiente de memoria</strong>: Spill to disk cuando es necesario sin fallar</p>
</li>
</ol>
<p>El overhead de Spark en archivos pequeños (inicialización, coordinación) se convierte en una <strong>ventaja masiva</strong> en archivos grandes gracias a su capacidad de distribución y paralelización.</p>
<h3 id="heading-analisis-critico-del-dataflow-gen2-en-grandes-volumenes-de-datos">Análisis crítico del Dataflow Gen2 en grandes volúmenes de datos</h3>
<p>El <strong>Dataflow Gen2</strong> muestra un rendimiento <strong>completamente inaceptable</strong> para archivos grandes:</p>
<p><strong>🚫 Consumo no apto para producción</strong></p>
<ul>
<li><p><strong>24.54x más CUs</strong> que PySpark (63,980 CUs de diferencia)</p>
</li>
<li><p><strong>6.15x más tiempo</strong> que PySpark (casi 70 minutos vs 11 minutos)</p>
</li>
<li><p><strong>€0.3138 más por ejecución</strong> (+515% de coste)</p>
</li>
</ul>
<p><strong>Proyección anual con carga diaria:</strong></p>
<ul>
<li><p>Coste PySpark: €22.27/año</p>
</li>
<li><p>Coste Dataflow Gen2: €136.80/año</p>
</li>
<li><p><strong>Sobrecoste: €114.53/año</strong> (para un solo archivo)</p>
</li>
</ul>
<p><strong>Problemas estructurales de Dataflow Gen2 con grandes volúmenes:</strong></p>
<ol>
<li><p><strong>Motor no optimizado</strong>: Power Query mashup engine no está diseñado para Big Data</p>
</li>
<li><p><strong>Procesamiento ineficiente</strong>: Operaciones fila por fila en lugar de vectorizadas</p>
</li>
<li><p><strong>Sin paralelización efectiva</strong>: No aprovecha arquitectura distribuida</p>
</li>
<li><p><strong>Alto overhead de memoria</strong>: Gestión ineficiente de grandes datasets</p>
</li>
<li><p><strong>Serialización costosa</strong>: Múltiples conversiones de formato internas</p>
</li>
</ol>
<p><strong>⚠️ Advertencia</strong>:</p>
<p>Aunque Dataflow Gen2 ofrece una interfaz visual atractiva y fácil de usar, <strong>su uso en producción con archivos grandes es técnicamente inadecuado y económicamente inviable</strong>. La facilidad de uso no justifica un sobrecosto del 515% y un tiempo de ejecución 6x superior.</p>
<h3 id="heading-consecuencias-de-una-mala-eleccion-del-artefacto"><strong>Consecuencias de una mala elección del artefacto</strong></h3>
<ol>
<li><p><strong>Saturación de capacidad</strong>: Consumo de CUs puede exceder la capacidad disponible</p>
</li>
<li><p><strong>Throttling</strong>: Fabric puede ralentizar o pausar trabajos</p>
</li>
<li><p><strong>Fallos en SLA</strong>: Procesos no completan en ventana de tiempo</p>
</li>
<li><p><strong>Costes desorbitados</strong>: Necesidad de capacidades superiores (F4, F8...)</p>
</li>
<li><p><strong>Impacto en otros procesos</strong>: Otros workloads en la misma capacidad sufren degradación</p>
</li>
</ol>
<hr />
<h1 id="heading-escenario-3-ingesta-desde-azure-sql-database">Escenario 3: Ingesta desde Azure SQL Database</h1>
<p>Este escenario evalúa el consumo de CUs al cargar datos desde una base de datos relacional estructurada en Azure SQL Database hacia tablas Delta en Microsoft Fabric.</p>
<p><strong>Operación realizada</strong>: Lectura completa de tablas desde Azure SQL Database y escritura en tablas Delta sin transformaciones. Operación de extracción pura (full load) para medir el consumo base en conexiones a bases de datos cloud.</p>
<h2 id="heading-dataset-utilizado-2">Dataset utilizado</h2>
<p>Se han evaluado dos tablas de diferentes tamaños para analizar el comportamiento escalable:</p>
<h4 id="heading-tabla-1-customer-pequena"><strong>Tabla 1: Customer (Pequeña)</strong></h4>
<p><strong>Características:</strong></p>
<ul>
<li><p><strong>Nombre de tabla</strong>: customer</p>
</li>
<li><p><strong>Número de registros</strong>: 1,679,846</p>
</li>
<li><p><strong>Tamaño</strong>: 679.47 MB</p>
</li>
</ul>
<h4 id="heading-tabla-2-sales-mediana-grande"><strong>Tabla 2: Sales (Mediana-Grande)</strong></h4>
<p><strong>Características:</strong></p>
<ul>
<li><p><strong>Nombre de tabla</strong>: sales</p>
</li>
<li><p><strong>Número de registros</strong>: 23,719,935</p>
</li>
<li><p><strong>Tamaño</strong>: 2,989 GB</p>
</li>
</ul>
<h2 id="heading-resultados-2">Resultados</h2>
<p>Revisando el informe de Microsoft Fabric Capacity Metrics:</p>
<h3 id="heading-tabla-customer">Tabla customer</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770999183008/ec90fa36-36cc-497c-8dad-214bbe2c8b73.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-coste-economico-2"><strong>Coste económico:</strong></h4>
<ul>
<li><p><strong>Pipeline</strong>: €0.0047 - El más económico por tiempo</p>
</li>
<li><p><strong>Notebook PySpark</strong>: €0.0050 - Prácticamente igual (+6%)</p>
</li>
<li><p><strong>Copy Job</strong>: €0.0052 - Similar (+11%)</p>
</li>
<li><p><strong>Dataflow Gen2</strong>: €0.0116 - <strong>147% más caro que Pipeline</strong></p>
</li>
</ul>
<h4 id="heading-consumo-de-cus-y-tiempos-de-ejecucion-2"><strong>Consumo de CUs y tiempos de ejecución</strong></h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771261194545/b8710108-dfd8-4b47-a419-3d0ab10ba537.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">En tablas SQL pequeñas, tanto <strong>Pipeline como PySpark son opciones válidas</strong> con diferencias mínimas. PySpark es más eficiente en CUs, Pipeline ligeramente más rápido.</div>
</div>

<h3 id="heading-tabla-sales">Tabla sales</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770999257887/5d81ef82-89f1-4cb7-99e8-c1c04443f512.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-coste-economico-3"><strong>Coste económico:</strong></h4>
<ul>
<li><p><strong>Notebook PySpark</strong>: €0.0082 - <strong>El más económico</strong></p>
</li>
<li><p><strong>Copy Job</strong>: €0.0173 - 2.11x más caro</p>
</li>
<li><p><strong>Pipeline</strong>: €0.0236 - <strong>2.88x más caro</strong></p>
</li>
<li><p><strong>Dataflow Gen2</strong>: €0.0667 - <strong>8.13x más caro</strong></p>
</li>
</ul>
<h4 id="heading-consumo-de-cus-y-tiempos-de-ejecucion-3"><strong>Consumo de CUs y tiempos de ejecución</strong></h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771261347169/17a393ab-d089-4b86-9e3e-d9a608ff327f.png" alt class="image--center mx-auto" /></p>
<p>La diferencia se amplifica: PySpark consume <strong>3x menos</strong> de las CUs de Pipeline y menos de <strong>23x</strong> del Dataflow Gen2. PySpark no solo consume menos CUs, sino que es casi <strong>3 veces más rápido</strong> que Pipeline.</p>
<hr />
<h1 id="heading-conclusion-final">Conclusión final</h1>
<p>La elección del artefacto correcto en Microsoft Fabric <strong>es una decisión estratégica</strong> que impacta:</p>
<ul>
<li><p>💰 <strong>Costes operativos</strong> (diferencias del 400-700% en grandes volúmenes)</p>
</li>
<li><p>⚡ <strong>Tiempos de ejecución</strong> (diferencias del 500-800% en grandes volúmenes)</p>
</li>
<li><p>📊 <strong>Capacidad disponible</strong> (eficiencia permite capacidades más pequeñas)</p>
</li>
<li><p>👥 <strong>Productividad del equipo</strong> (menos tiempo esperando procesos)</p>
</li>
</ul>
<p><strong>Lecciones aprendidas:</strong></p>
<ol>
<li><p><strong>El tamaño importa exponencialmente</strong>: Una diferencia del 9% en archivos pequeños se convierte en 500% en archivos grandes</p>
</li>
<li><p><strong>El origen importa</strong>: Azure SQL es más eficiente que CSV para todos los artefactos</p>
</li>
<li><p><strong>PySpark es el rey indiscutible en producción</strong>: Para cualquier volumen &gt;1 GB, PySpark es superior en todos los aspectos medibles</p>
</li>
<li><p><strong>Dataflow Gen2 no escala</strong>: Su facilidad de uso no justifica sobrecostos del 500-700% en producción</p>
</li>
<li><p><strong>La formación se paga sola</strong>: Invertir en formación de PySpark se amortiza en semanas con el ahorro en CUs</p>
</li>
</ol>
<blockquote>
<p>La interfaz visual y facilidad de uso tienen un precio en Capacity Units. En archivos o tablas pequeñas, ese precio puede ser aceptable. En archivos o tablas grandes, ese precio es prohibitivo.</p>
</blockquote>
<p>El potencial de ahorro de <strong>cientos o miles de euros anuales</strong> (dependiendo del volumen de cargas) justifica ampliamente:</p>
<ul>
<li><p>La inversión en formación del equipo</p>
</li>
<li><p>El tiempo de migración de procesos existentes</p>
</li>
<li><p>El establecimiento de buenas prácticas y estándares</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Cómo recuperar objetos eliminados de tu Lakehouse en Microsoft Fabric]]></title><description><![CDATA[¿Has eliminado accidentalmente una tabla Delta crítica o un archivo importante de tu lakehouse? No entres en pánico. Microsoft Fabric implementa un mecanismo de "soft delete" que mantiene tus objetos eliminados disponibles para recuperación durante 7...]]></description><link>https://datagym.es/como-recuperar-objetos-eliminados-de-tu-lakehouse-en-microsoft-fabric</link><guid isPermaLink="true">https://datagym.es/como-recuperar-objetos-eliminados-de-tu-lakehouse-en-microsoft-fabric</guid><category><![CDATA[microsoftfabric]]></category><category><![CDATA[dataengineering]]></category><category><![CDATA[onelake]]></category><category><![CDATA[DataRecovery ]]></category><category><![CDATA[semantic link labs]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Fri, 30 Jan 2026 08:27:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769761533851/273c9a1d-a3c5-4cb2-8ac6-3f0754b95c1d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>¿Has eliminado accidentalmente una tabla Delta crítica o un archivo importante de tu lakehouse? No entres en pánico. Microsoft Fabric implementa un mecanismo de "soft delete" que mantiene tus objetos eliminados disponibles para recuperación durante 7 días. En este artículo te mostraré cómo aprovechar esta característica usando la librería semantic-link-labs.</p>
<h1 id="heading-el-problema-eliminaciones-accidentales">El problema: Eliminaciones accidentales</h1>
<p>Trabajar con lakehouses implica gestionar grandes volúmenes de datos y múltiples artefactos. Es fácil cometer errores:</p>
<ul>
<li><p>Eliminar una tabla Delta pensando que era de desarrollo cuando era de producción</p>
</li>
<li><p>Borrar un archivo de configuración crítico durante una limpieza</p>
</li>
<li><p>Ejecutar un script que elimina carpetas completas por error</p>
</li>
<li><p>Sobrescribir datos importantes durante un proceso ETL</p>
</li>
</ul>
<p>Hasta hace poco, estos errores podían significar pérdida permanente de datos o recurrir a backups externos. Ahora, con el soft delete de OneLake, tienes una red de seguridad de 7 días.</p>
<hr />
<h1 id="heading-que-es-soft-delete-en-onelake">¿Qué es Soft Delete en OneLake?</h1>
<p>OneLake, el sistema de almacenamiento subyacente de Microsoft Fabric, implementa soft delete similar a Azure Blob Storage. Cuando eliminas un objeto:</p>
<ol>
<li><p>El objeto no se borra físicamente de inmediato</p>
</li>
<li><p>Se marca como "eliminado" y se oculta de las vistas normales</p>
</li>
<li><p>Permanece accesible para recuperación durante 7 días</p>
</li>
<li><p>Después de 7 días, se elimina permanentemente</p>
</li>
</ol>
<p>Esta funcionalidad aplica a todo el contenido de un lakehouse:</p>
<ul>
<li><p><strong>Tablas Delta completas</strong> (incluyendo sus archivos Parquet y logs de transacciones)</p>
</li>
<li><p><strong>Archivos individuales</strong> en la sección Files</p>
</li>
<li><p><strong>Carpetas completas</strong> con toda su estructura</p>
</li>
<li><p><strong>Esquemas</strong> con múltiples tablas</p>
</li>
</ul>
<hr />
<h1 id="heading-instalacion-de-semantic-link-labs">Instalación de semantic-link-labs</h1>
<p>La librería semantic-link-labs extiende las capacidades de Fabric con funciones avanzadas para lakehouses, semantic models y más. Para instalarla en tu notebook:</p>
<pre><code class="lang-python">%pip install semantic-link-labs
</code></pre>
<hr />
<h1 id="heading-explorando-objetos-eliminados">Explorando objetos eliminados</h1>
<p>Antes de recuperar nada, es fundamental identificar qué objetos están disponibles para restauración. La función <code>list_blobs()</code> nos permite listar todos los objetos, incluidos los eliminados.</p>
<h2 id="heading-listar-todos-los-blobs">Listar todos los blobs</h2>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> sempy_labs.lakehouse <span class="hljs-keyword">as</span> lake

<span class="hljs-comment"># Listar todos los objetos del lakehouse</span>
all_blobs = lake.list_blobs(
    lakehouse=<span class="hljs-literal">None</span>,  <span class="hljs-comment"># None usa el lakehouse del notebook</span>
    workspace=<span class="hljs-literal">None</span>   <span class="hljs-comment"># None usa el workspace del notebook</span>
)

print(<span class="hljs-string">f"Total de blobs encontrados: <span class="hljs-subst">{len(all_blobs)}</span>"</span>)
all_blobs.head()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767015121189/1ab0ef5b-0b98-4dfc-ba55-5c3d62525d5d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-filtrar-objetos-eliminados">Filtrar objetos eliminados</h2>
<p>El DataFrame resultante incluye una columna <code>Is Deleted</code> que podemos usar para filtrar:</p>
<pre><code class="lang-python">deleted_objects = all_blobs[all_blobs[<span class="hljs-string">'Is Deleted'</span>] == <span class="hljs-literal">True</span>]

print(<span class="hljs-string">f"Objetos eliminados: <span class="hljs-subst">{len(deleted_objects)}</span>"</span>)

<span class="hljs-keyword">if</span> len(deleted_objects) &gt; <span class="hljs-number">0</span>:
    print(<span class="hljs-string">"\nObjetos disponibles para recuperación:"</span>)
    print(<span class="hljs-string">"-"</span> * <span class="hljs-number">80</span>)
    <span class="hljs-keyword">for</span> _, obj <span class="hljs-keyword">in</span> deleted_objects.iterrows():
        print(<span class="hljs-string">f"📁 <span class="hljs-subst">{obj[<span class="hljs-string">'Blob Name'</span>]}</span>"</span>)
        print(<span class="hljs-string">f"   Eliminado: <span class="hljs-subst">{obj[<span class="hljs-string">'Deleted Time'</span>]}</span>"</span>)
        print(<span class="hljs-string">f"   Días restantes: <span class="hljs-subst">{obj[<span class="hljs-string">'Remaining Retention Days'</span>]}</span>"</span>)
        print(<span class="hljs-string">f"   Tamaño: <span class="hljs-subst">{obj[<span class="hljs-string">'Content Length'</span>]}</span> bytes"</span>)
        print()
<span class="hljs-keyword">else</span>:
    print(<span class="hljs-string">"✓ No hay objetos eliminados en este lakehouse"</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767015290042/6205390b-6fd0-4fd2-9fbc-f75e0ae8ffdb.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-filtrar-por-tipo-de-contenedor">Filtrar por tipo de contenedor</h2>
<p>Puedes especificar si quieres listar solo objetos de <code>Tables</code> o <code>Files</code>:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Solo tablas eliminadas</span>
deleted_tables = lake.list_blobs(
    lakehouse=<span class="hljs-literal">None</span>,  <span class="hljs-comment"># None usa el lakehouse del notebook</span>
    workspace=<span class="hljs-literal">None</span>,   <span class="hljs-comment"># None usa el workspace del notebook</span>
    container=<span class="hljs-string">"Tables"</span>
)
deleted_tables = deleted_tables[deleted_tables[<span class="hljs-string">'Is Deleted'</span>] == <span class="hljs-literal">True</span>]

<span class="hljs-comment"># Solo archivos eliminados</span>
deleted_files = lake.list_blobs(
    lakehouse=<span class="hljs-literal">None</span>,  <span class="hljs-comment"># None usa el lakehouse del notebook</span>
    workspace=<span class="hljs-literal">None</span>,   <span class="hljs-comment"># None usa el workspace del notebook</span>
    container=<span class="hljs-string">"Files"</span>
)
deleted_files = deleted_files[deleted_files[<span class="hljs-string">'Is Deleted'</span>] == <span class="hljs-literal">True</span>]

print(<span class="hljs-string">f"Tablas eliminadas: <span class="hljs-subst">{len(deleted_tables)}</span>"</span>)
print(<span class="hljs-string">f"Archivos eliminados: <span class="hljs-subst">{len(deleted_files)}</span>"</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767015376622/e38620fe-4c9f-4c65-ad79-80c00a5022f7.png" alt class="image--center mx-auto" /></p>
<hr />
<h1 id="heading-recuperando-objetos">Recuperando objetos</h1>
<p>Una vez identificado el objeto a recuperar, el proceso es muy simple usando <code>recover_lakehouse_object()</code>.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> sempy_labs.lakehouse <span class="hljs-keyword">as</span> lake

lake.recover_lakehouse_object(
    file_path=<span class="hljs-string">'ruta/del/objeto'</span>,
    lakehouse=<span class="hljs-literal">None</span>,  <span class="hljs-comment"># Nombre o ID del lakehouse</span>
    workspace=<span class="hljs-literal">None</span>   <span class="hljs-comment"># Nombre o ID del workspace</span>
)
</code></pre>
<h2 id="heading-ejemplo-1-recuperar-una-tabla-delta">Ejemplo 1: Recuperar una Tabla Delta</h2>
<p>Las tablas Delta son estructuras complejas con múltiples archivos Parquet y un transaction log. La recuperación restaura toda la estructura:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Recuperar tabla en el nivel raíz</span>
lake.recover_lakehouse_object(
    file_path=<span class="hljs-string">'Tables/green_tripdata_2022'</span>,
    lakehouse=<span class="hljs-string">"000xxx-xxxx-xxxx-xxxx-xxx000"</span>,
    workspace=<span class="hljs-string">"000xxx-xxxx-xxxx-xxxx-xxx000"</span>
)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767015556172/ab98e46f-b265-4e46-87ba-ffab9c12e7e1.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-ejemplo-2-recuperar-archivos-individuales">Ejemplo 2: Recuperar archivos individuales</h2>
<p>Los archivos en la sección Files se recuperan de la misma manera:</p>
<pre><code class="lang-python">lake.recover_lakehouse_object(
    file_path=<span class="hljs-string">'Files/Maestros/MaestroFechas.xlsx'</span>,
    lakehouse=<span class="hljs-literal">None</span>,
    workspace=<span class="hljs-literal">None</span>
)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767016094383/5247c565-3741-4a18-aa81-8b5afeaa3691.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767016133410/8129f3fa-38f1-427b-bd52-753705476a34.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-ejemplo-3-recuperar-carpetas-completas">Ejemplo 3: Recuperar carpetas completas</h2>
<p>Puedes recuperar carpetas enteras con todo su contenido:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Recuperar carpeta completa</span>
lake.recover_lakehouse_object(
    file_path=<span class="hljs-string">'Files/Maestros'</span>,
    lakehouse=<span class="hljs-literal">None</span>,
    workspace=<span class="hljs-literal">None</span>
)
</code></pre>
<p><strong>Importante</strong>: Al recuperar una carpeta, se restauran recursivamente todos los archivos y subcarpetas que contenía.</p>
<hr />
<h1 id="heading-limitaciones-y-consideraciones">Limitaciones y consideraciones</h1>
<h2 id="heading-ventana-de-recuperacion">⏱️ Ventana de recuperación</h2>
<p><strong>7 días es el límite absoluto</strong>. Después de este período:</p>
<ul>
<li><p>Los objetos se eliminan permanentemente de OneLake</p>
</li>
<li><p>No hay forma de recuperarlos sin un backup externo</p>
</li>
<li><p>El contador comienza en el momento de la eliminación</p>
</li>
</ul>
<p><strong>Recomendación</strong>: Implementa alertas automáticas para objetos próximos a expirar.</p>
<h2 id="heading-estructura-de-paths">📂 Estructura de Paths</h2>
<p>Los paths deben seguir la estructura exacta:</p>
<pre><code class="lang-plaintext">✓ Correcto:
  - Tables/FactSales
  - Tables/sales/FactSales
  - Files/raw/data.csv
  - Files/configs/app.json

✗ Incorrecto:
  - FactSales (falta el contenedor)
  - Tables\FactSales (barra invertida)
  - tables/FactSales (minúsculas)
</code></pre>
<h2 id="heading-tablas-delta-y-consistencia">🔄 Tablas Delta y consistencia</h2>
<p>Al recuperar una tabla Delta:</p>
<ul>
<li><p>Se restaura el estado exacto en el momento de eliminación</p>
</li>
<li><p>El transaction log se recupera completo</p>
</li>
<li><p>Todas las particiones y archivos Parquet se restauran</p>
</li>
<li><p>Los metadatos del Hive Metastore pueden requerir sincronización</p>
</li>
</ul>
<h2 id="heading-conflictos-de-nombres">⚠️ Conflictos de nombres</h2>
<p>Si existe un objeto con el mismo nombre que el que intentas recuperar:</p>
<ul>
<li><p>La operación puede fallar</p>
</li>
<li><p>Puede sobrescribirse el objeto actual (según configuración)</p>
</li>
<li><p>Es mejor renombrar o mover el objeto actual antes de recuperar</p>
</li>
</ul>
<h2 id="heading-permisos-requeridos">🔐 Permisos requeridos</h2>
<p>Para recuperar objetos necesitas:</p>
<ul>
<li><p>Permisos de <strong>escritura</strong> en el lakehouse</p>
</li>
<li><p>Permisos de <strong>administrador</strong> para carpetas del sistema</p>
</li>
<li><p>El rol de <strong>Contributor</strong> o superior en el workspace</p>
</li>
</ul>
<h2 id="heading-impacto-en-el-almacenamiento">💾 Impacto en el almacenamiento</h2>
<p>Los objetos en soft delete <strong>cuentan para tu cuota de almacenamiento</strong> de OneLake. No se libera espacio hasta la eliminación permanente.</p>
]]></content:encoded></item><item><title><![CDATA[Cómo recuperar Workspaces eliminados en Microsoft Fabric]]></title><description><![CDATA[¿Alguna vez has eliminado accidentalmente un workspace en Microsoft Fabric y has sentido ese momento de pánico? No te preocupes, Microsoft ha incorporado una API de administración que te permite restaurar workspaces eliminados. En este artículo te mo...]]></description><link>https://datagym.es/como-recuperar-workspaces-eliminados-en-microsoft-fabric</link><guid isPermaLink="true">https://datagym.es/como-recuperar-workspaces-eliminados-en-microsoft-fabric</guid><category><![CDATA[Microsoft Fabric API]]></category><category><![CDATA[Microsoft Fabric Workspace Recovery]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[PySpark]]></category><category><![CDATA[api]]></category><category><![CDATA[Azure]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[administration]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Fri, 16 Jan 2026 09:03:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768554141726/009e7fa3-ecf9-40c6-9819-76f71b586a08.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>¿Alguna vez has eliminado accidentalmente un workspace en Microsoft Fabric y has sentido ese momento de pánico? No te preocupes, Microsoft ha incorporado una API de administración que te permite restaurar workspaces eliminados. En este artículo te mostraré cómo hacerlo usando un notebook de PySpark directamente en Fabric.</p>
<h1 id="heading-por-que-es-importante-esta-funcionalidad">¿Por qué es importante esta funcionalidad?</h1>
<p>Los workspaces en Microsoft Fabric son contenedores cruciales que almacenan todos tus artefactos: lakehouse, notebooks, pipelines, datasets y más. Una eliminación accidental puede significar la pérdida de horas o días de trabajo. Afortunadamente, Microsoft Fabric ofrece una API administrativa que permite recuperar estos workspaces antes de que se eliminen permanentemente.</p>
<h1 id="heading-requisitos-previos">Requisitos previos</h1>
<p>Antes de comenzar, asegúrate de cumplir con estos requisitos:</p>
<ul>
<li><p><strong>Permisos de Fabric Administrator</strong>: Esta operación requiere privilegios administrativos a nivel de tenant</p>
</li>
<li><p><strong>Alcance delegado</strong>: <code>Tenant.ReadWrite.All</code></p>
</li>
<li><p><strong>Un workspace eliminado</strong>: Necesitas el ID (UUID) del workspace que deseas recuperar</p>
</li>
<li><p><strong>ID del nuevo administrador</strong>: Usuario o service principal que será el admin del workspace restaurado</p>
</li>
</ul>
<p><strong>Importante</strong>: La API tiene un límite de 10 peticiones por minuto, así que planifica tus operaciones en consecuencia.</p>
<h1 id="heading-la-api-de-restauracion">La API de Restauración</h1>
<p>Microsoft Fabric expone un endpoint REST específico para esta tarea:</p>
<pre><code class="lang-python">POST https://api.fabric.microsoft.com/v1/admin/workspaces/{workspaceId}/restore
</code></pre>
<p>Esta API acepta dos parámetros principales:</p>
<ol>
<li><p><strong>newWorkspaceName</strong>: El nombre que tendrá el workspace restaurado (obligatorio para "My workspace")</p>
</li>
<li><p><strong>newWorkspaceAdminPrincipal</strong>: El principal que será administrador del workspace restaurado</p>
</li>
</ol>
<p>El principal puede ser de varios tipos:</p>
<ul>
<li><p><code>User</code>: Un usuario de Microsoft Entra</p>
</li>
<li><p><code>ServicePrincipal</code>: Un service principal de Microsoft Entra</p>
</li>
<li><p><code>Group</code>: Un grupo de seguridad</p>
</li>
</ul>
<h1 id="heading-implementacion-con-pyspark">Implementación con PySpark</h1>
<h2 id="heading-configuracion-de-parametros">Configuración de parámetros</h2>
<p>Define los parámetros necesarios para la restauración:</p>
<pre><code class="lang-python">WORKSPACE_ID = <span class="hljs-string">"000xxx-xxxx-xxxx-xxxx-xxx000"</span>
NEW_WORKSPACE_NAME = <span class="hljs-string">"Workspace Restored"</span>
NEW_ADMIN_ID = <span class="hljs-string">"000xxx-xxxx-xxxx-xxxx-xxx000"</span>
PRINCIPAL_TYPE = <span class="hljs-string">"User"</span>
</code></pre>
<p><strong>Nota importante</strong>: El <code>NEW_ADMIN_ID</code> es el Object ID del usuario o service principal, que puedes encontrar en Azure Portal:</p>
<ul>
<li><p>Para usuarios: Azure Active Directory &gt; Users &gt; [seleccionar usuario] &gt; Object ID</p>
</li>
<li><p>Para service principals: Azure Active Directory &gt; Enterprise applications &gt; [seleccionar aplicación] &gt; Object ID</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767008406842/778c359b-5a52-4350-a2f4-3d243b2353db.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-listar-workspaces-eliminados">Listar Workspaces eliminados</h2>
<p>Antes de restaurar, puede ser útil obtener una lista de todos los workspaces eliminados disponibles. Aquí te muestro cómo hacerlo:</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">list_deleted_workspaces</span>():</span>
    <span class="hljs-string">"""
    Lista los workspaces eliminados disponibles para restauración
    """</span>

    url = <span class="hljs-string">"https://api.fabric.microsoft.com/v1/admin/workspaces"</span>
    headers = {
        <span class="hljs-string">"Authorization"</span>: <span class="hljs-string">f"Bearer <span class="hljs-subst">{token}</span>"</span>,
        <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>
    }

    <span class="hljs-comment"># Filtrar workspaces eliminados</span>
    params = {
        <span class="hljs-string">"state"</span>: <span class="hljs-string">"Deleted"</span>
    }

    <span class="hljs-keyword">try</span>:
        response = requests.get(url, headers=headers, params=params)
        <span class="hljs-keyword">if</span> response.status_code == <span class="hljs-number">200</span>:
            workspaces = response.json()
            print(<span class="hljs-string">f"Se encontraron <span class="hljs-subst">{len(workspaces.get(<span class="hljs-string">'value'</span>, []))}</span> workspaces eliminados:"</span>)
            <span class="hljs-keyword">for</span> ws <span class="hljs-keyword">in</span> workspaces.get(<span class="hljs-string">'value'</span>, []):
                print(<span class="hljs-string">f"  - <span class="hljs-subst">{ws.get(<span class="hljs-string">'displayName'</span>)}</span> (ID: <span class="hljs-subst">{ws.get(<span class="hljs-string">'id'</span>)}</span>)"</span>)
            <span class="hljs-keyword">return</span> workspaces
        <span class="hljs-keyword">else</span>:
            print(<span class="hljs-string">f"Error al listar workspaces: <span class="hljs-subst">{response.status_code}</span>"</span>)
            <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"Excepción: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>
</code></pre>
<p>Esta función te permite:</p>
<ul>
<li><p>Ver todos los workspaces que pueden ser restaurados</p>
</li>
<li><p>Obtener los IDs necesarios para la restauración</p>
</li>
<li><p>Verificar el nombre original del workspace antes de restaurarlo</p>
</li>
</ul>
<p>Es especialmente útil cuando no recuerdas el ID exacto del workspace que necesitas recuperar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767009189775/85e8dc04-b93a-4cbb-ba52-c4a92051b7cd.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-llamada-a-la-api">Llamada a la API</h2>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests

WORKSPACE_ID = <span class="hljs-string">"000xxx-xxxx-xxxx-xxxx-xxx000"</span>
NEW_WORKSPACE_NAME = <span class="hljs-string">"Workspace Restored"</span>
NEW_ADMIN_ID = <span class="hljs-string">"000xxx-xxxx-xxxx-xxxx-xxx000"</span>
PRINCIPAL_TYPE = <span class="hljs-string">"User"</span>

url = <span class="hljs-string">f"https://api.fabric.microsoft.com/v1/admin/workspaces/<span class="hljs-subst">{WORKSPACE_ID}</span>/restore"</span>

headers = {
    <span class="hljs-string">"Authorization"</span>: <span class="hljs-string">f"Bearer <span class="hljs-subst">{token}</span>"</span>,
    <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>
}

body = {
    <span class="hljs-string">"newWorkspaceName"</span>: NEW_WORKSPACE_NAME,
    <span class="hljs-string">"newWorkspaceAdminPrincipal"</span>: {
        <span class="hljs-string">"id"</span>: NEW_ADMIN_ID,
        <span class="hljs-string">"type"</span>: PRINCIPAL_TYPE
    }
}

response = requests.post(url, headers=headers, json=body)
</code></pre>
<p><strong>Importante</strong>: La restauración del workspace no solo recupera el contenedor, sino también todos los artefactos que contenía en el momento de su eliminación (notebooks, lakehouses, pipelines, datasets, etc.). El usuario o service principal especificado en <code>newWorkspaceAdminPrincipal</code> se convertirá automáticamente en el administrador del workspace restaurado, con permisos completos sobre él y todos sus artefactos.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767011103422/a9b72847-adb7-43af-8490-5df8dc1702f3.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767011134008/441c37cd-9022-4256-b5ec-97c17433299f.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-solucion-completa">Solución completa</h1>
<p>He preparado un notebook completo que encapsula toda esta funcionalidad en funciones reutilizables. El código incluye:</p>
<ul>
<li><p><strong>Autenticación automática</strong> con manejo de errores</p>
</li>
<li><p><strong>Función</strong> <code>list_deleted_workspaces()</code> para listar workspaces disponibles para restauración</p>
</li>
<li><p><strong>Función</strong> <code>restore_workspace()</code> parametrizable y reutilizable</p>
</li>
<li><p><strong>Validación de respuestas</strong> con mensajes descriptivos</p>
</li>
<li><p><strong>Logging detallado</strong> para depuración</p>
</li>
<li><p><strong>Documentación inline</strong> con todos los detalles importantes</p>
</li>
</ul>
<p>El flujo de trabajo típico sería:</p>
<ol>
<li><p>Ejecutar <code>list_deleted_workspaces()</code> para ver qué workspaces están disponibles</p>
</li>
<li><p>Identificar el workspace que necesitas restaurar</p>
</li>
<li><p>Copiar su ID y ejecutar <code>restore_workspace()</code> con los parámetros apropiados</p>
</li>
</ol>
<p>Puedes encontrar el código completo en mi GitHub: <a target="_blank" href="https://github.com/kilianbs/DataGym/blob/main/Microsoft%20Fabric/notebooks/Como%20recuperar%20Workspaces%20eliminados%20en%20Microsoft%20Fabric%20con%20PySpark.ipynb">notebook</a></p>
<h1 id="heading-casos-de-uso-practicos">Casos de Uso Prácticos</h1>
<p>Esta solución es especialmente útil en varios escenarios:</p>
<ol>
<li><p><strong>Recuperación de emergencia</strong>: Restaurar workspaces eliminados accidentalmente por error humano</p>
</li>
<li><p><strong>Automatización</strong>: Integrar en pipelines de governance que detecten y restauren workspaces críticos</p>
</li>
<li><p><strong>Migraciones</strong>: Crear scripts de recuperación masiva durante reorganizaciones de tenant</p>
</li>
<li><p><strong>Testing</strong>: Eliminar y restaurar workspaces en entornos de desarrollo de forma programática</p>
</li>
</ol>
<h1 id="heading-limitaciones-y-consideraciones">Limitaciones y Consideraciones</h1>
<p>Ten en cuenta estas limitaciones de la API:</p>
<ul>
<li><p><strong>Estado de preview</strong>: Esta API está en preview y puede cambiar</p>
</li>
<li><p><strong>Rate limiting</strong>: Máximo 10 peticiones por minuto</p>
</li>
<li><p><strong>Ventana de recuperación</strong>: Los workspaces eliminados solo están disponibles durante un período limitado</p>
</li>
<li><p><strong>Permisos requeridos</strong>: Solo usuarios con rol de Fabric Administrator pueden ejecutar esta operación</p>
</li>
</ul>
<h1 id="heading-recursos-adicionales">Recursos adicionales</h1>
<ul>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/rest/api/fabric/articles/">Microsoft Fabric REST API references - Microsoft Fabric REST APIs | Microsoft Learn</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/rest/api/fabric/articles/">Workspaces - Restore Workspace - REST API (Admin) |</a> <a target="_blank" href="https://learn.microsoft.com/en-us/rest/api/fabric/admin/workspaces/restore-workspace?tabs=HTTP">Microsoft Learn</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/kilianbs/DataGym/blob/main/Microsoft%20Fabric/notebooks/Como%20recuperar%20Workspaces%20eliminados%20en%20Microsoft%20Fabric%20con%20PySpark.ipynb">Código completo en GitHub</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Sparkwise: Optimización Inteligente para Apache Spark en Microsoft Fabric]]></title><description><![CDATA[Si trabajas con Apache Spark en Microsoft Fabric, probablemente te hayas enfrentado a la complejidad de optimizar configuraciones, reducir costos y mejorar el rendimiento de tus workloads. Sparkwise es una librería de Python diseñada específicamente ...]]></description><link>https://datagym.es/sparkwise-optimizacion-inteligente-para-apache-spark-en-microsoft-fabric</link><guid isPermaLink="true">https://datagym.es/sparkwise-optimizacion-inteligente-para-apache-spark-en-microsoft-fabric</guid><category><![CDATA[SparkOptimization]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[#apache-spark]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[lakehouse]]></category><category><![CDATA[big data]]></category><category><![CDATA[PySpark]]></category><category><![CDATA[deltalake]]></category><category><![CDATA[performance]]></category><category><![CDATA[Cost Optimization]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Fri, 09 Jan 2026 16:57:13 GMT</pubDate><content:encoded><![CDATA[<p>Si trabajas con Apache Spark en Microsoft Fabric, probablemente te hayas enfrentado a la complejidad de optimizar configuraciones, reducir costos y mejorar el rendimiento de tus workloads. <strong>Sparkwise</strong> es una librería de Python diseñada específicamente para resolver estos desafíos, actuando como un especialista automatizado en ingeniería de datos que te ayuda a lograr el mejor equilibrio entre precio y rendimiento.</p>
<hr />
<h1 id="heading-que-es-sparkwise">¿Qué es Sparkwise?</h1>
<p>Sparkwise es una herramienta de diagnóstico y optimización para Apache Spark que proporciona análisis inteligentes, recomendaciones de configuración y perfilado completo de sesiones. Su objetivo es hacer que la optimización de Spark sea simple, efectiva y hasta divertida, eliminando la necesidad de ser un experto en configuraciones complejas.</p>
<hr />
<h1 id="heading-por-que-usar-sparkwise">¿Por qué usar Sparkwise?</h1>
<p>La optimización de Spark en Microsoft Fabric puede ser costosa y compleja. Sparkwise aborda estos problemas ofreciendo:</p>
<ul>
<li><p><strong>💰 Optimización de costos</strong>: Detecta configuraciones que desperdician capacidad y aumentan el tiempo de ejecución</p>
</li>
<li><p><strong>⚡ Maximización del rendimiento</strong>: Habilita optimizaciones específicas de Fabric como Native Engine, V-Order y perfiles de recursos</p>
</li>
<li><p><strong>🎓 Aprendizaje simplificado</strong>: Asistente interactivo de Q&amp;A para 133 configuraciones de Spark, Delta Lake y Fabric</p>
</li>
<li><p><strong>🔍 Comprensión de workloads</strong>: Perfilado exhaustivo de sesiones, ejecutores, jobs y recursos</p>
</li>
<li><p><strong>⏱️ Ahorro de tiempo</strong>: Detecta bloqueadores de Starter Pool para evitar cold-starts de 3-5 minutos</p>
</li>
<li><p><strong>📊 Decisiones basadas en datos</strong>: Recomendaciones priorizadas con análisis de impacto</p>
</li>
</ul>
<p>Puedes leer todas las características que incluye aquí: <a target="_blank" href="https://pypi.org/project/sparkwise/">sparkwise · PyPI</a></p>
<hr />
<h1 id="heading-sparkwise-en-accion">Sparkwise en acción</h1>
<h2 id="heading-instalacion-y-configuracion">Instalación y configuración</h2>
<p>En tu notebook de Fabric, ejecuta:</p>
<pre><code class="lang-python">%pip install sparkwise
</code></pre>
<p>Verificar la instalación</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> sparkwise
print(<span class="hljs-string">f"Sparkwise versión: <span class="hljs-subst">{sparkwise.__version__}</span>"</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767877929739/b5f75194-8c29-4ce6-9910-5c75596ccd54.png" alt class="image--center mx-auto" /></p>
<p>Importar módulos necesarios</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sparkwise <span class="hljs-keyword">import</span> (
    diagnose, 
    ask, 
    profile, 
    predict_scalability,
    analyze_efficiency,
    detect_skew
)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767877948603/e7f25ea2-5bdc-417b-854f-db62cb3cf791.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-analisis-exhaustivo-de-la-sesion-actual">Análisis exhaustivo de la sesión actual</h2>
<pre><code class="lang-python">diagnose.analyze()
</code></pre>
<p><strong>¿Qué verás?</strong></p>
<p>El diagnóstico te mostrará 5 categorías de análisis:</p>
<ol>
<li><p><strong>Native Execution Engine</strong>: ¿Está usando Velox para acelerar queries?</p>
</li>
<li><p><strong>Spark Compute</strong>: ¿Estás en Starter Pool o Custom Pool?</p>
</li>
<li><p><strong>Data Skew</strong>: ¿Hay tareas desequilibradas?</p>
</li>
<li><p><strong>Delta Optimizations</strong>: ¿Están habilitadas V-Order, Optimize Write?</p>
</li>
<li><p><strong>Runtime Tuning</strong>: ¿Está habilitado AQE (Adaptive Query Execution)?</p>
</li>
</ol>
<h3 id="heading-interpretar-los-resultados">Interpretar los resultados</h3>
<p>Supongamos que obtienes esta salida:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767892665953/0d54d307-acb7-4f58-afae-b19607acb356.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767892680748/d841e70e-4d63-48cf-848b-9453b78dcb9c.png" alt class="image--center mx-auto" /></p>
<p><strong>Esto significa:</strong></p>
<ul>
<li><p><strong>Native Engine deshabilitado</strong>: Tu workload no está aprovechando Velox, el motor de ejecución nativo de Fabric que puede acelerar consultas entre 3-8x. Esto es especialmente crítico para operaciones de agregación, filtrado y joins.</p>
</li>
<li><p><strong>AQE crítico</strong>: Sin Adaptive Query Execution, Spark no puede ajustar dinámicamente el plan de ejecución basándose en estadísticas reales. Estás perdiendo optimizaciones automáticas como:</p>
<ul>
<li><p>Coalescing de particiones pequeñas</p>
</li>
<li><p>Optimización de joins sesgados</p>
</li>
<li><p>Mejor paralelización dinámica</p>
</li>
</ul>
</li>
<li><p><strong>Tamaño de partición subóptimo</strong>: Con ejecutores de 56GB, particiones de 128MB son demasiado pequeñas. Esto genera overhead innecesario al procesar muchas particiones pequeñas en lugar de menos particiones más grandes y eficientes.</p>
</li>
</ul>
<h3 id="heading-resumen-consolidado">Resumen consolidado</h3>
<p>Al final del diagnóstico, Sparkwise muestra un resumen consolidado que te permite ver de un vistazo el estado de tu configuración:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767901974732/08dd5ded-9ff1-45a0-8270-d34548f397d1.png" alt class="image--center mx-auto" /></p>
<p><strong>Cómo leer esta tabla:</strong></p>
<ul>
<li><p><strong>Status</strong>: Indica el estado general de cada categoría</p>
<ul>
<li><p>✅ Good: Configuración óptima sin problemas críticos</p>
</li>
<li><p>⚠️ Issues: Hay problemas que requieren atención</p>
</li>
<li><p>❌ Critical: Problemas graves que afectan significativamente el rendimiento</p>
</li>
</ul>
</li>
<li><p><strong>Critical Issues</strong>: Número de configuraciones con prioridad CRITICAL que deben solucionarse inmediatamente</p>
</li>
<li><p><strong>Recommendations</strong>: Total de recomendaciones de mejora (incluye todos los niveles de prioridad)</p>
</li>
</ul>
<h3 id="heading-tabla-de-recomendaciones-priorizadas">Tabla de recomendaciones priorizadas</h3>
<p>Después del resumen, Sparkwise presenta una tabla detallada con todas las recomendaciones ordenadas por prioridad:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767902065208/b2ad1fb6-ba50-4d33-8c20-26f4431fc793.png" alt class="image--center mx-auto" /></p>
<p><strong>Prioridades explicadas:</strong></p>
<ol>
<li><p><strong>🔴 CRITICAL</strong>: Problemas que impactan significativamente el rendimiento o costos. Deben aplicarse inmediatamente.</p>
<ul>
<li>Ejemplo: AQE deshabilitado puede causar 30-50% más de tiempo de ejecución</li>
</ul>
</li>
<li><p><strong>🟡 HIGH</strong>: Optimizaciones importantes con impacto medible y claro.</p>
<ul>
<li>Ejemplo: Native Engine puede acelerar queries 3-5x</li>
</ul>
</li>
<li><p><strong>🔵 MEDIUM</strong>: Mejoras relevantes para casos de uso específicos.</p>
<ul>
<li>Ejemplo: V-Order beneficia principalmente a workloads de lectura intensiva</li>
</ul>
</li>
<li><p><strong>⚪ LOW</strong>: Ajustes finos y optimizaciones menores.</p>
<ul>
<li>Ejemplo: Ajustes de tamaño de partición para ejecutores específicos</li>
</ul>
</li>
</ol>
<hr />
<h2 id="heading-prediccion-de-escalabilidad-y-costos">Predicción de escalabilidad y costos</h2>
<p>Una de las características más valiosas de Sparkwise es su capacidad para predecir costos y recomendar la configuración óptima de infraestructura. Después de ejecutar tu workload, puedes usar <code>predict_scalability()</code> para obtener un análisis detallado de costos y rendimiento.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Predecir costos si ejecutas este job 100 veces al mes</span>
predict_scalability(runs_per_month=<span class="hljs-number">100</span>)
</code></pre>
<p><strong>Obtendrás:</strong></p>
<ul>
<li><p>Comparación entre Starter Pool vs Custom Pool</p>
</li>
<li><p>VCore-horas mensuales</p>
</li>
<li><p>Costos estimados</p>
</li>
<li><p>Recomendación de configuración óptima</p>
</li>
</ul>
<p>El resultado que me ha dado a mi ha sido este… tendré que probarlo más a fondo porque no realiza bien las comparativas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767956690324/1b9114ff-30c4-4f10-a436-694f78d9531a.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-analizar-eficiencia-de-computo">Analizar eficiencia de cómputo</h3>
<pre><code class="lang-python">analyze_efficiency(runs_per_month=<span class="hljs-number">100</span>)
</code></pre>
<p><strong>Verás:</strong></p>
<ul>
<li><p>Tiempo de cómputo activo vs desperdiciado</p>
</li>
<li><p>Costo del desperdicio en VCore-horas</p>
</li>
<li><p>Score de eficiencia (0-100%)</p>
</li>
<li><p>Estrategia de reducción de desperdicios</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767957343088/945ebbab-0640-43d8-b150-a89a14621e3a.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767957363069/2db76207-0ba8-48e0-b380-73be29239fcd.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-analisis-avanzado-de-tus-sesiones">Análisis avanzado de tus sesiones</h2>
<h3 id="heading-analizar-la-sesion-completa">Analizar la sesión completa</h3>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sparkwise <span class="hljs-keyword">import</span> profile, profile_executors, profile_jobs, profile_resources

profile.profile()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767904591662/8ea53cee-462e-46d4-b0cc-26370f0e65d6.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-analizar-ejecutores-jobs-y-recursos">Analizar ejecutores, jobs y recursos</h3>
<pre><code class="lang-python">print(<span class="hljs-string">"⚡ Perfil de Ejecutores:"</span>)
profile_executors.profile()

print(<span class="hljs-string">"🚀 Perfil de Jobs:"</span>)
profile_jobs.profile()

print(<span class="hljs-string">"💾 Perfil de Recursos:"</span>)
profile_resources.profile()
</code></pre>
<hr />
<h2 id="heading-entender-configuraciones-con-el-asistente-qampa">Entender configuraciones con el asistente Q&amp;A</h2>
<h3 id="heading-preguntar-sobre-configuraciones">Preguntar sobre configuraciones</h3>
<p>Si no entiendes alguna configuración, usa el asistente:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Preguntar sobre perfiles de recursos</span>
ask.config(<span class="hljs-string">'spark.fabric.resourceProfile'</span>)
</code></pre>
<p><strong>Obtendrás:</strong></p>
<ul>
<li><p>Qué hace la configuración</p>
</li>
<li><p>Valores recomendados para tu workload</p>
</li>
<li><p>Ejemplos de uso</p>
</li>
<li><p>Configuraciones relacionadas</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767903573021/a34f290c-969b-408c-b15b-50cf90041808.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-buscar-configuraciones-por-tema">Buscar configuraciones por tema</h3>
<pre><code class="lang-python"><span class="hljs-comment"># Buscar todas las configuraciones relacionadas con "optimize"</span>
ask.search(<span class="hljs-string">'optimize'</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767903691361/38f6726d-8dde-44ec-9542-9aceb60c8381.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-deteccion-y-solucion-de-data-skew">Detección y solución de Data Skew</h2>
<h3 id="heading-detectar-skew-basico">Detectar Skew Básico</h3>
<pre><code class="lang-python">skew_results = detect_skew()
</code></pre>
<p><strong>Identificarás:</strong></p>
<ul>
<li><p>Tareas que tardan mucho más que otras</p>
</li>
<li><p>Particiones desbalanceadas</p>
</li>
<li><p>Joins problemáticos</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767963399886/32a8111b-6627-48fe-b5aa-fb4a4d0b4d54.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-analisis-avanzado-de-skew-en-dataframes">Análisis avanzado de Skew en DataFrames</h3>
<p>Para un análisis más profundo a nivel de partición, usa <code>AdvancedSkewDetector</code>:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sparkwise.core.advanced_skew_detector <span class="hljs-keyword">import</span> AdvancedSkewDetector
detector = AdvancedSkewDetector()
detector.analyze_partition_skew(your_df, [<span class="hljs-string">"key_column"</span>])
</code></pre>
<p>El análisis muestra dos secciones clave:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767964138521/b2103d3f-772d-4f80-9770-bc07b42ed3c1.png" alt class="image--center mx-auto" /></p>
<p>📊 <strong>Partition Size Distribution</strong></p>
<p><strong>Qué significa:</strong></p>
<ul>
<li><p><strong>Mean Size</strong>: Tamaño promedio por partición (1 fila) - extremadamente bajo</p>
</li>
<li><p><strong>Max Size</strong>: La partición más grande tiene 2.8M filas</p>
</li>
<li><p><strong>Min Size</strong>: La partición más pequeña tiene solo 1 fila</p>
</li>
<li><p><strong>Std Dev</strong>: Desviación estándar de 1,119 indica alta variabilidad</p>
</li>
</ul>
<p><strong>🎯 Skew Metrics (Métricas de Sesgo)</strong></p>
<ol>
<li><p><strong>Skew Ratio: 1963143.87x</strong> 🔴</p>
<ul>
<li><p>La partición más grande es <strong>casi 2 millones de veces</strong> más grande que la más pequeña</p>
</li>
<li><p>Esto es <strong>EXTREMADAMENTE CRÍTICO</strong></p>
</li>
<li><p>Umbrales típicos:</p>
<ul>
<li><p>&lt; 3x: ✅ Aceptable</p>
</li>
<li><p>3-10x: ⚠️ Moderado</p>
</li>
<li><p>10-100x: 🔴 Alto</p>
</li>
<li><blockquote>
<p>100x: 🔴 <strong>CRÍTICO</strong> (tu caso: 1.9M x)</p>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Variation: 77601.48%</strong></p>
<ul>
<li><p>Hay una variabilidad del 77,601% entre particiones</p>
</li>
<li><p>Indica que los datos están <strong>completamente desbalanceados</strong></p>
</li>
</ul>
</li>
<li><p><strong>Severity: CRITICAL</strong></p>
<ul>
<li><p>Requiere <strong>acción inmediata</strong></p>
</li>
<li><p>Este nivel de skew puede causar:</p>
<ul>
<li><p>1 tarea tomando horas mientras las demás terminan en segundos</p>
</li>
<li><p>Out of Memory (OOM) en el ejecutor con la partición grande</p>
</li>
<li><p>Subutilización masiva de recursos (99% de ejecutores ociosos)</p>
</li>
</ul>
</li>
</ul>
</li>
</ol>
<h4 id="heading-impacto-en-el-rendimiento"><strong>Impacto en el rendimiento</strong></h4>
<p>Con un skew de 1.9M x:</p>
<ul>
<li><p><strong>Escenario sin skew</strong>: 10 ejecutores × 100 tareas = todas terminan en ~5 minutos</p>
</li>
<li><p><strong>Escenario con tu skew</strong>: 9 ejecutores terminan en 5 segundos (procesar 1 fila) 1 ejecutor tarda 2+ horas (procesar 2.8M filas)</p>
</li>
<li><p><strong>Resultado</strong>:</p>
<ul>
<li><p>99% de recursos desperdiciados esperando</p>
</li>
<li><p>Runtime total = tiempo del ejecutor más lento</p>
</li>
<li><p>Costo = 10 ejecutores × 2 horas (aunque 9 están ociosos)</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-estrategias-de-mitigacion"><strong>Estrategias de mitigación</strong></h3>
<p>Sparkwise te proporciona 4 estrategias ordenadas por efectividad para este caso:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767964538108/e67939d4-24a8-40e0-bc05-ab3fc636b2f1.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-estrategia-1-salting-mas-recomendada-para-skew-critico">🔴 <strong>Estrategia 1: Salting (MÁS RECOMENDADA para skew crítico)</strong></h4>
<p>El salting distribuye artificialmente las claves sesgadas</p>
<p><strong>Impacto esperado:</strong></p>
<ul>
<li><p>Reducción de runtime: 80-95%</p>
</li>
<li><p>Distribución: En lugar de 1 partición con 2.8M filas, tendrás 20 particiones con ~140K filas cada una</p>
</li>
<li><p>Utilización de recursos: Pasarás de 1 ejecutor trabajando a 10-20 ejecutores en paralelo</p>
</li>
</ul>
<p><strong>¿Por qué funciona?</strong></p>
<ul>
<li><p>Convierte 1 clave problemática en 20 claves diferentes</p>
</li>
<li><p>Spark puede distribuir esas 20 claves entre múltiples ejecutores</p>
</li>
<li><p>Todos los ejecutores procesan cargas similares (~140K filas)</p>
</li>
</ul>
<h4 id="heading-estrategia-2-filter-before-join"><strong>⚠️ Estrategia 2: Filter Before Join</strong></h4>
<p>Reduce el volumen de datos <strong>antes</strong> de operaciones costosas</p>
<p><strong>Impacto:</strong></p>
<ul>
<li><p>Reducción de 30-70% en tiempo de procesamiento</p>
</li>
<li><p>Menos shuffle de datos</p>
</li>
<li><p>Solo útil si puedes filtrar sin perder datos necesarios</p>
</li>
</ul>
<h4 id="heading-estrategia-3-repartition-by-multiple-columns"><strong>🔄 Estrategia 3: Repartition by Multiple Columns</strong></h4>
<p>Distribuye por múltiples dimensiones para mejor balance</p>
<p><strong>Cuándo usar:</strong></p>
<ul>
<li><p>Cuando tienes otra columna con buena cardinalidad</p>
</li>
<li><p>Para operaciones posteriores de groupBy por múltiples columnas</p>
</li>
<li><p>Menos efectivo que salting para skew extremo como en este ejemplo</p>
</li>
</ul>
<h4 id="heading-estrategia-4-use-broadcast-join-for-small-tables"><strong>📡 Estrategia 4: Use Broadcast Join for Small Tables</strong></h4>
<p>Si estás haciendo join con una tabla pequeña</p>
<p><strong>Limitación:</strong></p>
<ul>
<li><p>Solo funciona si una tabla es pequeña (&lt;100MB por defecto)</p>
</li>
<li><p>No resuelve el skew dentro de una tabla, solo evita shuffle en joins</p>
</li>
</ul>
<hr />
<h2 id="heading-optimizacion-de-almacenamiento">Optimización de almacenamiento</h2>
<h3 id="heading-analisis-completo-de-almacenamiento">Análisis completo de almacenamiento</h3>
<pre><code class="lang-python"><span class="hljs-comment"># Analizar tabla Delta completa</span>
sparkwise.analyze_storage(<span class="hljs-string">"Tables/green_taxi_location_analysis"</span>)
</code></pre>
<p>Esto ejecuta 3 análisis:</p>
<ol>
<li><p>Detección de archivos pequeños</p>
</li>
<li><p>ROI de VACUUM</p>
</li>
<li><p>Efectividad de particiones</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767972487105/5b0b9268-914d-44a7-99c0-a3db2e40cbb5.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767972503161/8a965053-fd26-488b-b87a-dd96b690b208.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767972518473/79c0a6b4-1449-473a-93ac-c03e89adbdbb.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-solucionar-problema-de-archivos-pequenos">Solucionar problema de archivos pequeños</h3>
<p>Primero, detectar el problema</p>
<pre><code class="lang-python">sparkwise.check_small_files(<span class="hljs-string">"Tables/green_taxi_location_analysis"</span>, threshold_mb=<span class="hljs-number">10</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767973329350/51576068-4193-4997-8ef7-7123b02f07f5.png" alt class="image--center mx-auto" /></p>
<p>Seguir recomendaciones de optimización</p>
<pre><code class="lang-python">spark.sql(<span class="hljs-string">"OPTIMIZE delta.`Tables/green_taxi_location_analysis`"</span>)

<span class="hljs-comment"># Habilitar auto-optimización para el futuro</span>
spark.conf.set(<span class="hljs-string">"spark.databricks.delta.optimizeWrite.enabled"</span>, <span class="hljs-string">"true"</span>)
spark.conf.set(<span class="hljs-string">"spark.databricks.delta.autoCompact.enabled"</span>, <span class="hljs-string">"true"</span>)
</code></pre>
<h3 id="heading-calcular-roi-de-vacuum">Calcular ROI de VACUUM</h3>
<p>Analizar beneficio de limpiar versiones antiguas</p>
<pre><code class="lang-python">sparkwise.vacuum_roi(<span class="hljs-string">"Tables/green_taxi_location_analysis"</span>, retention_hours=<span class="hljs-number">168</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767973564336/6fef0c2b-2dbc-46a5-a75a-eabe56712cc2.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-analizar-efectividad-de-particiones">Analizar efectividad de particiones</h3>
<pre><code class="lang-python">sparkwise.check_partitions(<span class="hljs-string">"Tables/green_tripdata_2017"</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767973634598/2940b279-dc44-4a38-bc40-7bdc7be05331.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-analisis-de-planes-de-consulta-sql">Análisis de planes de consulta SQL</h2>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> sparkwise <span class="hljs-keyword">import</span> analyze_query

df = spark.sql(<span class="hljs-string">"""
    SELECT VendorID, payment_type, 
           SUM(fare_amount) as total_fare,
           AVG(trip_distance) as avg_distance
    FROM delta.`Tables/green_tripdata_2017`
    WHERE fare_amount &gt; 10
    GROUP BY VendorID, payment_type
"""</span>)

<span class="hljs-comment"># Analizar el plan de ejecución</span>
analyze_query(df)
</code></pre>
<p><strong>Detectarás:</strong></p>
<ul>
<li><p>Productos cartesianos accidentales</p>
</li>
<li><p>Full table scans innecesarios</p>
</li>
<li><p>Shuffles excesivos</p>
</li>
<li><p>Compatibilidad con Native Engine</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767974402584/5b69c5f4-dbae-4c9f-aded-5768204d77f1.png" alt class="image--center mx-auto" /></p>
<hr />
<h1 id="heading-conclusion">Conclusión</h1>
<p>Sparkwise transforma la compleja tarea de optimizar Apache Spark en Microsoft Fabric en un proceso guiado, automatizado y basado en datos. Ya seas ingeniero de datos, científico de datos o administrador de plataforma, esta librería te proporciona las herramientas necesarias para maximizar el rendimiento, minimizar costos y tomar decisiones informadas sobre tu infraestructura de datos.</p>
<p>Con su combinación de diagnósticos automatizados, análisis avanzados, optimización de almacenamiento y un asistente interactivo de configuración, sparkwise se posiciona como una herramienta esencial en el toolkit de cualquier profesional que trabaje con Spark en Fabric.</p>
<hr />
<h1 id="heading-agradecimiento-especial"><strong>Agradecimiento especial</strong></h1>
<p>Un sincero agradecimiento a <strong>Santhosh Ravindran</strong>. Ha construido una herramienta que democratiza la optimización de Apache Spark, haciendo accesible para ingenieros de datos de todos los niveles lo que antes requería años de experiencia especializada.</p>
]]></content:encoded></item><item><title><![CDATA[¿Qué es la deduplicación de datos y por qué es tan importante?]]></title><description><![CDATA[En el universo de los datos, la presencia de duplicados es casi una garantía. Desde registros de clientes que se repiten hasta transacciones que aparecen más de una vez, los datos duplicados son un problema silencioso que puede socavar la fiabilidad ...]]></description><link>https://datagym.es/que-es-la-deduplicacion-de-datos-y-por-que-es-tan-importante</link><guid isPermaLink="true">https://datagym.es/que-es-la-deduplicacion-de-datos-y-por-que-es-tan-importante</guid><category><![CDATA[data-engineering]]></category><category><![CDATA[deduplication]]></category><category><![CDATA[lakehouse]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[deltalake]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Mon, 15 Dec 2025 18:22:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765822762722/46779d24-aa7d-4aad-99c7-2b794d33c478.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>En el universo de los datos, la presencia de <strong>duplicados</strong> es casi una garantía. Desde registros de clientes que se repiten hasta transacciones que aparecen más de una vez, los datos duplicados son un problema silencioso que puede socavar la fiabilidad de tus análisis, inflar tus costos y ralentizar tus operaciones. Aquí es donde entra en juego la <strong>deduplicación de datos</strong>, una práctica esencial en la gestión de cualquier conjunto de datos.</p>
<h1 id="heading-que-es-exactamente-la-deduplicacion-de-datos">¿Qué es exactamente la deduplicación de datos?</h1>
<p>En términos sencillos, la <strong>deduplicación de datos</strong> es el proceso de identificar y eliminar registros duplicados de un conjunto de datos. El objetivo principal es asegurar la <strong>unicidad</strong> y la <strong>precisión</strong> de la información, manteniendo solo una versión "verdadera" o "maestra" de cada entidad o evento.</p>
<p>Piensa en una tabla de clientes: si tienes a "Cliente1" con su información dos veces, la deduplicación se encargaría de dejar solo una versión del cliente, que será la más reciente o no dependiendo el método de deduplicación que utilicemos.</p>
<h1 id="heading-por-que-es-tan-importante-la-deduplicacion-de-datos">¿Por qué es tan importante la deduplicación de datos?</h1>
<p>La importancia de la deduplicación de datos tiene un impacto directo en la <strong>calidad de los datos</strong>, la <strong>eficiencia operativa</strong> y la <strong>toma de decisiones</strong>.</p>
<h4 id="heading-1-mejora-la-calidad-y-fiabilidad-de-los-datos">1. Mejora la calidad y fiabilidad de los datos</h4>
<ul>
<li><p>Los datos duplicados sesgan los resultados. Si un cliente aparece cinco veces, tus informes de ventas o marketing lo contarán cinco veces, dándote una visión inflada y errónea. La deduplicación asegura que tus análisis reflejen la realidad.</p>
</li>
<li><p>Basar decisiones estratégicas en datos imprecisos puede llevar a resultados desastrosos. Con datos deduplicados, los <em>insights</em> que obtengas serán más fiables, permitiéndote tomar decisiones informadas y con mayor confianza.</p>
</li>
</ul>
<h4 id="heading-2-optimiza-el-rendimiento-y-reduce-costos">2. Optimiza el rendimiento y reduce costos</h4>
<ul>
<li><p><strong>Menor consumo de almacenamiento:</strong> Los datos duplicados ocupan espacio valioso. Eliminar las copias innecesarias reduce los requisitos de almacenamiento, lo que se traduce directamente en ahorros de costos.</p>
</li>
<li><p><strong>Procesamiento de Datos más Rápido:</strong> Procesar menos datos significa que tus <em>pipelines</em> de ETL, tus consultas de bases de datos y tus modelos de machine learning se ejecutarán de forma más eficiente y rápida, ahorrando tiempo y recursos computacionales.</p>
</li>
</ul>
<h4 id="heading-3-facilita-la-gobernanza-de-datos">3. Facilita la Gobernanza de Datos</h4>
<ul>
<li><strong>Coherencia de Datos:</strong> La deduplicación ayuda a mantener la coherencia en todo tu ecosistema de datos, asegurando que todos los sistemas utilicen la misma versión de la verdad.</li>
</ul>
<h1 id="heading-como-se-realiza-la-deduplicacion-de-datos">¿Cómo se realiza la deduplicación de datos?</h1>
<p>A continuación veremos como realizar la deduplicación de datos con PySpark en Microsoft Fabric. Utilizaremos unos datos de ejemplo sobre productos.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Datos de ejemplo con duplicados</span>
data = [
    (<span class="hljs-number">1</span>, <span class="hljs-string">"Producto1"</span>, <span class="hljs-number">9.99</span>, datetime.strptime(<span class="hljs-string">"2025-01-01 00:00:00"</span>, <span class="hljs-string">"%Y-%m-%d %H:%M:%S"</span>)),
    (<span class="hljs-number">2</span>, <span class="hljs-string">"Producto2"</span>, <span class="hljs-number">25.00</span>, datetime.strptime(<span class="hljs-string">"2025-02-10 11:30:00"</span>, <span class="hljs-string">"%Y-%m-%d %H:%M:%S"</span>)),
    (<span class="hljs-number">1</span>, <span class="hljs-string">"Producto1"</span>, <span class="hljs-number">9.99</span>, datetime.strptime(<span class="hljs-string">"2025-01-01 00:00:00"</span>, <span class="hljs-string">"%Y-%m-%d %H:%M:%S"</span>)), <span class="hljs-comment"># Duplicado exacto</span>
    (<span class="hljs-number">3</span>, <span class="hljs-string">"Producto3"</span>, <span class="hljs-number">42.35</span>, datetime.strptime(<span class="hljs-string">"2025-05-01 14:00:00"</span>, <span class="hljs-string">"%Y-%m-%d %H:%M:%S"</span>)),
    (<span class="hljs-number">2</span>, <span class="hljs-string">"Producto2"</span>, <span class="hljs-number">25.00</span>, datetime.strptime(<span class="hljs-string">"2025-03-01 15:32:58"</span>, <span class="hljs-string">"%Y-%m-%d %H:%M:%S"</span>)), <span class="hljs-comment"># Duplicado de Producto2, con fecha posterior</span>
    (<span class="hljs-number">4</span>, <span class="hljs-string">"Producto4"</span>, <span class="hljs-number">59.99</span>, datetime.strptime(<span class="hljs-string">"2025-05-27 16:00:00"</span>, <span class="hljs-string">"%Y-%m-%d %H:%M:%S"</span>)),
    (<span class="hljs-number">1</span>, <span class="hljs-string">"Producto1"</span>, <span class="hljs-number">12.99</span>, datetime.strptime(<span class="hljs-string">"2025-03-21 09:00:00"</span>, <span class="hljs-string">"%Y-%m-%d %H:%M:%S"</span>)), <span class="hljs-comment"># Duplicado de Producto1, con fecha más reciente</span>
    (<span class="hljs-number">5</span>, <span class="hljs-string">"Producto2"</span>, <span class="hljs-number">25.00</span>, datetime.strptime(<span class="hljs-string">"2025-03-01 15:32:58"</span>, <span class="hljs-string">"%Y-%m-%d %H:%M:%S"</span>)) <span class="hljs-comment"># Duplicado exacto de Producto2 (ID diferente pero mismos datos clave)</span>
]

schema = StructType([
    StructField(<span class="hljs-string">'id'</span>, IntegerType(), <span class="hljs-literal">True</span>),
    StructField(<span class="hljs-string">'product_name'</span>, StringType(), <span class="hljs-literal">True</span>),
    StructField(<span class="hljs-string">'price'</span>, DoubleType(), <span class="hljs-literal">True</span>),
    StructField(<span class="hljs-string">'last_updated'</span>, TimestampType(), <span class="hljs-literal">True</span>)
])

df = spark.createDataFrame(data, schema=schema)

print(<span class="hljs-string">"DataFrame Original:"</span>)
df.show()
print(<span class="hljs-string">f"Número de registros original: <span class="hljs-subst">{df.count()}</span>"</span>)
</code></pre>
<ul>
<li><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748775977025/cf799008-2ffc-4b9f-910e-0d4433ddff3c.png" alt class="image--center mx-auto" /></li>
</ul>
<p>Existen varias maneras de abordar la deduplicación. Las más comunes son:</p>
<h2 id="heading-deduplicacion-exacta">Deduplicación exacta</h2>
<p>Filas que son completamente idénticas en todas sus columnas, o en un subconjunto específico de columnas que actúan como "clave". En pyspark se suelen utilizar estas dos funciones:</p>
<ul>
<li><p><code>dropDuplicates()</code>: La forma más sencilla de eliminar filas completamente idénticas o basándose en un conjunto específico de columnas.</p>
</li>
<li><p><code>distinct()</code>: Similar a <code>dropDuplicates()</code> sin argumentos, elimina filas idénticas.</p>
</li>
</ul>
<pre><code class="lang-python"><span class="hljs-comment"># Deduplicación basada en todas las columnas</span>
df_deduplicated_all = df.dropDuplicates()

print(<span class="hljs-string">"DataFrame Deduplicado (Todas las columnas):"</span>)
df_deduplicated_all.show()

print(<span class="hljs-string">f"Número de registros deduplicados (todas las columnas): <span class="hljs-subst">{df_deduplicated_all.count()}</span>"</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748776597272/0d444fcc-d987-4d39-8b67-36bfb73b0f8a.png" alt class="image--center mx-auto" /></p>
<p>Como se puede ver, realizando la deduplicación de datos sobre todas las columnas solo elimina aquellos duplicados idénticos, que en este caso es el id 1 con precio 9.99.</p>
<p>Veamos que ocurre si <strong>utilizamos la misma función pero especificando la clave primaria</strong>.</p>
<pre><code class="lang-python">df_deduplicated_subset = df.dropDuplicates(subset=[<span class="hljs-string">"id"</span>])

print(<span class="hljs-string">"\nDataFrame Deduplicado (Basado en id):"</span>)
df_deduplicated_subset.show()

print(<span class="hljs-string">f"Número de registros deduplicados (id): <span class="hljs-subst">{df_deduplicated_subset.count()}</span>"</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748777962707/21dcb1ef-fd3d-4344-ad7f-a76e0fde6db6.png" alt class="image--center mx-auto" /></p>
<p>En esta ocasión podemos ver como no hay ids duplicados, pero, ¿la información que tenemos es precisa? spoiler: no.</p>
<p>Si nos fijamos en los datos del id = 1, tenemos tres registros. Con la operación de deduplicación anterior basada en la columna id, hemos eliminado los duplicados pero quedándonos con el primer valor, lo que nos hace tener información no válida para nuestros análisis (aquí no tenemos en cuenta las dimensiones lentamente cambiantes tipo 2).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749760701593/73f07525-6c7c-4088-a8da-458e112de89d.png" alt class="image--center mx-auto" /></p>
<p>En el caso que necesitemos eliminar los duplicados pero obteniendo el registro con los datos más actualizados, deberemos de utilizar otra forma de deduplicación.</p>
<h2 id="heading-deduplicacion-por-prioridadversion"><strong>Deduplicación por Prioridad/Versión:</strong></h2>
<p>Es la forma de deduplicación más robusta y la que se suele utilizar en los proyectos.</p>
<p><strong>¿Qué busca?</strong> Varias versiones del mismo registro de entidad, donde no todas las columnas son idénticas, pero se refieren a la misma "cosa" (por ejemplo, el mismo producto con información ligeramente diferente o con distintas fechas de actualización). Se aplica una lógica de negocio para elegir la "mejor" versión.</p>
<p>Uso de funciones de ventana (<code>Window functions</code>) junto con <code>row_number()</code>, <code>rank()</code>, <code>dense_rank()</code> para seleccionar un registro preferido (ej. el más reciente).</p>
<h2 id="heading-ejemplo-de-uso-con-pyspark">Ejemplo de uso con PySpark</h2>
<p>Para definir la ventana utilizaremos Window particionando por la clave primaria (columna id) y ordenamos de manera descendente por la columna last_updated para obtener el registro más actualizado primero.</p>
<p>Asignamos un número de fila para cada registro dentro de su partición por clave primaria.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> pyspark.sql.functions <span class="hljs-keyword">import</span> col, row_number, to_timestamp
<span class="hljs-keyword">from</span> pyspark.sql.window <span class="hljs-keyword">import</span> Window

window_spec_pk = Window.partitionBy(<span class="hljs-string">"id"</span>).orderBy(col(<span class="hljs-string">"last_updated"</span>).desc())

df_ranked_pk = df.withColumn(<span class="hljs-string">"row_num"</span>, row_number().over(window_spec_pk))

print(<span class="hljs-string">"\nDataFrame con número de fila (para priorización por id):"</span>)
df_ranked_pk.show()
</code></pre>
<p>El resultado que obtenemos es que para el producto 1 y 2 tenemos varías versiones, quedando la más reciente con el row_num a 1.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750007853876/2e71ffcc-f795-429e-9564-b6c72abb22db.png" alt class="image--center mx-auto" /></p>
<p>Si filtramos para mantener solo los registros más recientes (row_num = 1), obtenemos los datos deduplicados.</p>
<pre><code class="lang-python">df_deduplicated_pk_priority = df_ranked_pk.filter(col(<span class="hljs-string">"row_num"</span>) == <span class="hljs-number">1</span>).drop(<span class="hljs-string">"row_num"</span>)

print(<span class="hljs-string">"\nDataFrame Deduplicado por Clave Primaria (id) - Prioridad (más reciente):"</span>)
df_deduplicated_pk_priority.show()

print(<span class="hljs-string">f"Número de registros deduplicados por PK (prioridad): <span class="hljs-subst">{df_deduplicated_pk_priority.count()}</span>"</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750008282706/a48170d2-80eb-47c5-9246-484a74c55738.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-ejemplo-de-uso-con-sparksql">Ejemplo de uso con SparkSQL</h2>
<pre><code class="lang-python">df.createOrReplaceTempView(<span class="hljs-string">"productos"</span>)

df = spark.sql(<span class="hljs-string">"""
WITH dedupes AS
(
    SELECT *, ROW_NUMBER() OVER(PARTITION BY id ORDER BY last_updated DESC) AS row_num
    FROM productos
)
SELECT *
FROM dedupes
"""</span>)

df.show()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750008774484/a2c031e3-7c84-49eb-95bb-9503f97c96e8.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-python">df = spark.sql(<span class="hljs-string">"""
WITH dedupes AS
(
    SELECT *, ROW_NUMBER() OVER(PARTITION BY id ORDER BY last_updated DESC) AS row_num
    FROM productos
)
SELECT
    id,
    product_name,
    price,
    last_updated
FROM dedupes
WHERE row_num = 1
"""</span>)

df.show()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750008874180/df8b4f3d-99ce-4feb-b787-7cdf448a31b6.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-conclusion">Conclusión</h1>
<p>La deduplicación es una práctica esencial en el procesamiento de datos, y PySpark en Microsoft Fabric ofrece las herramientas robustas para realizarla de manera eficiente. Este proceso mejora la calidad de los datos, optimiza el rendimiento de las consultas y análisis, y reduce los costos de almacenamiento.</p>
]]></content:encoded></item><item><title><![CDATA[Deletion Vectors en Delta Lake: Funcionamiento interno, impacto en el rendimiento y recomendaciones prácticas]]></title><description><![CDATA[Los Deletion Vectors (DV) son una de las funcionalidades más relevantes en Delta Lake para acelerar las operaciones de modificación de datos.
¿Cómo funcionan los Deletion Vectors?
Tradicionalmente, Delta Lake utiliza un enfoque Copy-on-Write:cuando s...]]></description><link>https://datagym.es/deletion-vectors-en-delta-lake-funcionamiento-interno-impacto-en-el-rendimiento-y-recomendaciones-practicas</link><guid isPermaLink="true">https://datagym.es/deletion-vectors-en-delta-lake-funcionamiento-interno-impacto-en-el-rendimiento-y-recomendaciones-practicas</guid><category><![CDATA[Deletion Vectors]]></category><category><![CDATA[Delta Lake]]></category><category><![CDATA[dataengineering]]></category><category><![CDATA[microsoft fabric]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Thu, 04 Dec 2025 11:38:56 GMT</pubDate><content:encoded><![CDATA[<p>Los <strong>Deletion Vectors (DV)</strong> son una de las funcionalidades más relevantes en Delta Lake para acelerar las operaciones de modificación de datos.</p>
<h1 id="heading-como-funcionan-los-deletion-vectors">¿Cómo funcionan los Deletion Vectors?</h1>
<p>Tradicionalmente, Delta Lake utiliza un enfoque <strong>Copy-on-Write</strong>:<br />cuando se elimina, actualiza o mergea una fila dentro de un archivo Parquet, el archivo completo debe reescribirse, excluyendo las filas afectadas. Esto es costoso en E/S, especialmente para archivos grandes o modificaciones puntuales.</p>
<p>Con los <strong>Deletion Vectors</strong>, Delta introduce un modelo <strong>Merge-on-Read</strong>:</p>
<ul>
<li><p>Los archivos Parquet originales <strong>no se reescriben</strong>.</p>
</li>
<li><p>Las filas eliminadas se registran en un archivo auxiliar comprimido (un <code>.bin</code>).</p>
</li>
<li><p>Durante la lectura, el motor aplica el vector de eliminación y descarta esas posiciones lógicamente.</p>
</li>
<li><p>El coste de reescritura total se pospone a operaciones posteriores como <code>OPTIMIZE</code> o <code>REORG TABLE ... APPLY (PURGE)</code>.</p>
</li>
</ul>
<p>Este enfoque reduce de manera drástica la E/S asociada a las operaciones de DELETE/UPDATE/MERGE.</p>
<hr />
<h1 id="heading-ejemplo-practico-en-microsoft-fabric">🛠️ Ejemplo Práctico en Microsoft Fabric</h1>
<p>A continuación definimos dos tablas Delta:<br />una sin Deletion Vectors (Copy-on-Write) y otra con DV habilitado (Merge-on-Read).</p>
<h2 id="heading-preparacion-y-creacion-de-tablas">Preparación y creación de tablas</h2>
<p>Primero, creamos los datos base y luego las dos tablas, una sin la propiedad Deletion Vectors y otra con esta propiedad habilitada.</p>
<pre><code class="lang-python">data = [
    (<span class="hljs-number">1</span>, <span class="hljs-string">"Ana"</span>, <span class="hljs-string">"Ventas"</span>), 
    (<span class="hljs-number">2</span>, <span class="hljs-string">"Luis"</span>, <span class="hljs-string">"IT"</span>), 
    (<span class="hljs-number">3</span>, <span class="hljs-string">"Marta"</span>, <span class="hljs-string">"Marketing"</span>), 
    (<span class="hljs-number">4</span>, <span class="hljs-string">"Carlos"</span>, <span class="hljs-string">"Ventas"</span>), 
    (<span class="hljs-number">5</span>, <span class="hljs-string">"Elena"</span>, <span class="hljs-string">"IT"</span>)
]
columns = [<span class="hljs-string">"id"</span>, <span class="hljs-string">"nombre"</span>, <span class="hljs-string">"departamento"</span>]
df = spark.createDataFrame(data, columns)

<span class="hljs-comment"># --- Tabla SIN Deletion Vectors (Comportamiento Predeterminado) ---</span>
print(<span class="hljs-string">"Creando tabla SIN Deletion Vectors..."</span>)
df.coalesce(<span class="hljs-number">1</span>).write.format(<span class="hljs-string">"delta"</span>).mode(<span class="hljs-string">"overwrite"</span>).saveAsTable(<span class="hljs-string">"tabla_sin_dv"</span>)

<span class="hljs-comment"># --- Tabla CON Deletion Vectors ---</span>
print(<span class="hljs-string">"Creando tabla CON Deletion Vectors..."</span>)
df.coalesce(<span class="hljs-number">1</span>).write.format(<span class="hljs-string">"delta"</span>).mode(<span class="hljs-string">"overwrite"</span>) \
  .option(<span class="hljs-string">"overwriteSchema"</span>, <span class="hljs-string">"true"</span>) \
  .option(<span class="hljs-string">"delta.enableDeletionVectors"</span>, <span class="hljs-string">"true"</span>) \
  .saveAsTable(<span class="hljs-string">"tabla_con_dv"</span>)
</code></pre>
<p>Puedes verificar la configuración de cada tabla con:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SHOW</span> TBLPROPERTIES tabla_sin_dv;
<span class="hljs-keyword">SHOW</span> TBLPROPERTIES tabla_con_dv;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764014347992/09d0016b-f7a0-4ab6-87c3-2790f1f290a3.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764014380563/f6f33f3c-477e-4266-bd52-0156680029ad.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-comportamiento-sin-deletion-vectors-copy-on-write">📂 Comportamiento SIN Deletion Vectors (Copy-on-Write)</h2>
<p>Antes del DELETE, la tabla contiene un único Parquet.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764081341025/136a17e2-b726-42e8-89b1-f6d05e704bce.png" alt class="image--center mx-auto" /></p>
<p>Se puede ver que existe un único fichero parquet y un commit en la carpeta <code>_delta_log</code>. Tras ejecutar:</p>
<pre><code class="lang-python">spark.sql(<span class="hljs-string">f"DELETE FROM tabla_sin_dv WHERE id = 3"</span>)
</code></pre>
<p>Delta:</p>
<ol>
<li><p>marca el archivo original como <code>remove</code> en <code>_delta_log</code>,</p>
</li>
<li><p>genera un archivo Parquet nuevo con 4 filas,</p>
</li>
<li><p>registra el <code>add</code> correspondiente.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764081715107/2d478b5c-5925-4f57-abc9-6e0835b325ea.png" alt class="image--center mx-auto" /></p>
<p>Ejemplo mínimo del commit JSON:</p>
<pre><code class="lang-json">{
    ...
        <span class="hljs-attr">"operationMetrics"</span>: {
            <span class="hljs-attr">"numRemovedFiles"</span>: <span class="hljs-string">"1"</span>,
            <span class="hljs-attr">"numCopiedRows"</span>: <span class="hljs-string">"4"</span>,
            <span class="hljs-attr">"numDeletionVectorsAdded"</span>: <span class="hljs-string">"0"</span>,
            <span class="hljs-attr">"numDeletionVectorsRemoved"</span>: <span class="hljs-string">"0"</span>,
            <span class="hljs-attr">"numAddedChangeFiles"</span>: <span class="hljs-string">"0"</span>,
            <span class="hljs-attr">"numDeletionVectorsUpdated"</span>: <span class="hljs-string">"0"</span>,
            <span class="hljs-attr">"numDeletedRows"</span>: <span class="hljs-string">"1"</span>,
            <span class="hljs-attr">"numAddedFiles"</span>: <span class="hljs-string">"1"</span>,
            ...
        ...
}
{
    <span class="hljs-attr">"remove"</span>: {
        <span class="hljs-attr">"path"</span>: <span class="hljs-string">"part-00000-ff53dcfe-e9ee-44c5-ae25-c9e90ba6ff46-c000.snappy.parquet"</span>,
        <span class="hljs-attr">"deletionTimestamp"</span>: <span class="hljs-number">1764081601227</span>,
        ...
    }
}
{
    <span class="hljs-attr">"add"</span>: {
        <span class="hljs-attr">"path"</span>: <span class="hljs-string">"part-00000-3c93366e-b599-49fe-99e0-f2087490b294-c000.snappy.parquet"</span>,
        <span class="hljs-attr">"modificationTime"</span>: <span class="hljs-number">1764081601092</span>,
        ...
    }
}
</code></pre>
<h2 id="heading-comportamiento-con-deletion-vectors-merge-on-read">📂 Comportamiento CON Deletion Vectors (Merge-on-Read)</h2>
<p>Antes del DELETE, la tabla también tiene un único Parquet.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764085009706/bd0c389f-e152-4b59-af9f-9f50da05b014.png" alt class="image--center mx-auto" /></p>
<p>Se puede ver que también existe un único fichero parquet y un commit en la carpeta <code>_delta_log</code>. Tras ejecutar:</p>
<pre><code class="lang-python">spark.sql(<span class="hljs-string">f"DELETE FROM tabla_con_dv WHERE id = 3"</span>)
</code></pre>
<p>Haciendo el mismo borrado pero para la tabla con Deletion Vectors habilitado, el contenido de la carpeta es el siguiente:</p>
<p>Delta:</p>
<ol>
<li><p>mantiene el archivo Parquet tal cual,</p>
</li>
<li><p>añade un archivo <code>deletion_vector_....bin</code> con la posición invalidada,</p>
</li>
<li><p>actualiza el commit indicando el DeletionVector aplicado.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764085125595/9741d1b4-5390-4755-bd92-f95e29145a07.png" alt class="image--center mx-auto" /></p>
<p>Nuestro nuevo commit contendría lo siguiente, donde se elimina la referencia al archivo Parquet existente y se añade un puntero al mismo archivo Parquet con un vector de eliminación:</p>
<pre><code class="lang-json">{
    ...
        <span class="hljs-attr">"operationMetrics"</span>: {
            ...
            <span class="hljs-attr">"numRemovedFiles"</span>: <span class="hljs-string">"0"</span>,
            <span class="hljs-attr">"numCopiedRows"</span>: <span class="hljs-string">"0"</span>,
            <span class="hljs-attr">"numDeletionVectorsAdded"</span>: <span class="hljs-string">"1"</span>,
            <span class="hljs-attr">"numDeletionVectorsRemoved"</span>: <span class="hljs-string">"0"</span>,
            <span class="hljs-attr">"numAddedChangeFiles"</span>: <span class="hljs-string">"0"</span>,
            <span class="hljs-attr">"numDeletionVectorsUpdated"</span>: <span class="hljs-string">"0"</span>,
            <span class="hljs-attr">"numDeletedRows"</span>: <span class="hljs-string">"1"</span>,
            <span class="hljs-attr">"numAddedFiles"</span>: <span class="hljs-string">"0"</span>,
            ...
        },
        ...
}
{
    <span class="hljs-attr">"add"</span>: {
        <span class="hljs-attr">"path"</span>: <span class="hljs-string">"part-00000-67be0c96-23bd-48e4-97a1-c0b43f615ad3-c000.snappy.parquet"</span>,
        ...
        <span class="hljs-attr">"deletionVector"</span>: {
            <span class="hljs-attr">"storageType"</span>: <span class="hljs-string">"u"</span>,
            <span class="hljs-attr">"pathOrInlineDv"</span>: <span class="hljs-string">"5+[vtVR%EkPn{Xs}UU8&lt;"</span>,
            <span class="hljs-attr">"offset"</span>: <span class="hljs-number">1</span>,
            <span class="hljs-attr">"sizeInBytes"</span>: <span class="hljs-number">34</span>,
            <span class="hljs-attr">"cardinality"</span>: <span class="hljs-number">1</span>
        }
    }
}
{
    <span class="hljs-attr">"remove"</span>: {
        <span class="hljs-attr">"path"</span>: <span class="hljs-string">"part-00000-67be0c96-23bd-48e4-97a1-c0b43f615ad3-c000.snappy.parquet"</span>,
        ...
    }
}
</code></pre>
<h2 id="heading-resumen-de-los-dos-comportamientos">Resumen de los dos comportamientos</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Tabla</strong></td><td><strong>Cambios en archivos</strong></td><td><strong>Comportamiento</strong></td></tr>
</thead>
<tbody>
<tr>
<td><code>tabla_sin_dv</code></td><td>Hay <strong>2 archivos Parquet</strong>: el original se marca como eliminado en el Log Delta, y se escribe un <strong>archivo Parquet nuevo y más pequeño</strong> (el <em>Copy-on-Write</em>).</td><td>Se reescribe el archivo afectado.</td></tr>
<tr>
<td><code>tabla_con_dv</code></td><td>Se mantienen los <strong>archivos Parquet originales</strong> y un nuevo archivo con la extensión <code>.bin</code> (el <strong>Deletion Vector</strong>).</td><td>Se escribe solo el Deletion Vector. El archivo Parquet original NO se reescribe, solo se marca la posición de la fila eliminada.</td></tr>
</tbody>
</table>
</div><hr />
<h1 id="heading-analisis-del-impacto-en-rendimiento">📊 Análisis del impacto en rendimiento</h1>
<p>Ahora que comprendemos cómo funcionan conceptualmente los vectores de eliminación, veamos el impacto real en el rendimiento.</p>
<p>Para ello, he utilizado un conjunto de datos idéntico de 100 millones de filas en dos tablas Delta diferentes, una con vectores de eliminación habilitados y otra sin. Las pruebas que he realizado para medir el impacto en el rendimiento son:</p>
<ul>
<li><p>Borrado de un registro</p>
</li>
<li><p>Borrado del 25% de la tabla</p>
</li>
<li><p>Actualización del 5% de la tabla</p>
</li>
<li><p>MERGE de un nuevo dataset que contiene 2 millones de filas (2%) en la tabla existente</p>
</li>
<li><p>SELECT COUNT(1) WHERE</p>
</li>
<li><p>SELECT SUM()</p>
</li>
<li><p>OPTIMIZE</p>
</li>
<li><p>VACUUM</p>
</li>
</ul>
<p>Las operaciones se han hecho en el mismo orden que aparecen en el listado y los resultados han sido los siguientes:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764093542856/55dff980-1f6e-4217-8812-4016f5ee7b1b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-resultados-principales">⏱️ Resultados principales</h2>
<ul>
<li><p><strong>DELETE (1 fila)</strong>: La tabla con Deletion Vectors realiza el borrado <strong>6.2x más rápido</strong></p>
</li>
<li><p><strong>DELETE (25M)</strong>: La tabla con Deletion Vectors realiza el borrado <strong>3.2x más rápido</strong></p>
</li>
<li><p><strong>UPDATE (5M)</strong>: La tabla con Deletion Vectors realiza el update <strong>3.5x más lento</strong></p>
</li>
<li><p><strong>MERGE (2M)</strong>: rendimiento similar, ligera penalización con DV</p>
</li>
<li><p><strong>OPTIMIZE</strong>: Deletion Vectors es <strong>208x más rápido</strong></p>
</li>
<li><p><strong>VACUUM</strong>: Deletion Vectors es <strong>1.6x más rápido</strong></p>
</li>
<li><p><strong>SELECT COUNT(1)</strong>: DV es <strong>5.9x más lento</strong></p>
</li>
<li><p><strong>SELECT SUM(price)</strong>: DV es <strong>1.8x más lento</strong></p>
</li>
</ul>
<p>Esto nos lleva a un punto clave:<br /><strong>el beneficio de DV es enorme en escritura, pero las lecturas pueden penalizarse seriamente.</strong></p>
<hr />
<h1 id="heading-por-que-las-lecturas-son-mas-lentas-con-deletion-vectors">🔍 ¿Por qué las lecturas son más lentas con Deletion Vectors?</h1>
<p>La causa no es el DV en sí, sino el modelo <strong>Merge-on-Read</strong>, donde el lector debe:</p>
<ol>
<li><p>Leer los archivos Parquet originales</p>
</li>
<li><p>Leer los vectores de eliminación asociados</p>
</li>
<li><p>Combinar ambas fuentes</p>
</li>
<li><p>Filtrar las filas inválidas</p>
</li>
<li><p>Reconstruir el dataset resultante.</p>
</li>
</ol>
<p>Cuantos más deletes/updates acumula una tabla, más trabajo deben hacer los lectores.</p>
<h2 id="heading-el-efecto-se-amplifica-si-no-se-ejecuta-optimize">📈 El efecto se amplifica si no se ejecuta OPTIMIZE</h2>
<p>En las pruebas:</p>
<ul>
<li><p>SELECT COUNT(1) → <strong>5.9x más lento</strong></p>
</li>
<li><p>SELECT SUM() → <strong>1.8x más lento</strong></p>
</li>
</ul>
<p>Esto se debe a que el motor debe leer <strong>más datos de los necesarios</strong>, incluyendo registros ya invalidados.</p>
<h2 id="heading-el-remedio-compactacion">🧹 El remedio: compactación</h2>
<p>Después de un:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">OPTIMIZE</span> &lt;tabla&gt;
</code></pre>
<p>o</p>
<pre><code class="lang-sql">REORG TABLE &lt;tabla&gt; APPLY (<span class="hljs-keyword">PURGE</span>)
</code></pre>
<p>la tabla queda físicamente limpia y las lecturas vuelven a ser rápidas.</p>
<hr />
<h1 id="heading-cuando-habilitar-deletion-vectors">🧭 ¿Cuándo habilitar Deletion Vectors?</h1>
<h2 id="heading-casos-recomendados">✔️ Casos recomendados</h2>
<h3 id="heading-1-capas-bronze-y-silver">1. Capas Bronze y Silver</h3>
<p>Ideal cuando:</p>
<ul>
<li><p>hay ingestas frecuentes</p>
</li>
<li><p>existen deletes/updates parciales</p>
</li>
<li><p>se realizan merges incrementales</p>
</li>
<li><p>la prioridad es la velocidad de ingestión.</p>
</li>
</ul>
<h3 id="heading-2-workloads-mor-donde-la-es-es-el-cuello-de-botella"><strong>2. Workloads MoR donde la E/S es el cuello de botella</strong></h3>
<p>DV evita reescrituras costosas en Parquet.</p>
<h3 id="heading-3-cuando-hay-una-estrategia-de-optimizacion-establecida"><strong>3. Cuando hay una estrategia de optimización establecida</strong></h3>
<p>Es imprescindible:</p>
<ul>
<li><p>Programar <code>OPTIMIZE</code> o <code>REORG ... APPLY (PURGE)</code></p>
</li>
<li><p>Ejecutar <code>VACUUM</code> periódicamente</p>
</li>
</ul>
<h2 id="heading-casos-no-recomendados">❌ Casos NO recomendados</h2>
<h3 id="heading-1-tablas-con-pocas-escrituras-y-muchas-lecturas"><strong>1. Tablas con pocas escrituras y muchas lecturas</strong></h3>
<p>Ejemplos:</p>
<ul>
<li><p>tablas Gold</p>
</li>
<li><p>modelos de agregación</p>
</li>
<li><p>capas analíticas puras</p>
</li>
<li><p>dashboards con baja latencia</p>
</li>
<li><p>Power BI Direct Lake</p>
</li>
</ul>
<p>Aquí CoW suele ser más eficiente.</p>
<h3 id="heading-2-problemas-de-compatibilidad"><strong>2. Problemas de compatibilidad</strong></h3>
<p>DV requiere:</p>
<ul>
<li><p>Delta Lake 2.3+</p>
</li>
<li><p>Reader version ≥ 3</p>
</li>
<li><p>Writer version ≥ 7</p>
</li>
</ul>
<h3 id="heading-3-fabric-copy-data-activity-limitacion-temporal"><strong>3. Fabric Copy Data Activity (limitación temporal)</strong></h3>
<p>Copy Data <strong>ignora</strong> Deletion Vectors → pueden reaparecer filas “borradas”.<br />Esto se resolverá (si no se ha resuelto ya) cuando Fabric actualice el soporte a esta funcionalidad.</p>
<h1 id="heading-conclusion">💡 Conclusión</h1>
<p>Los <strong>Deletion Vectors</strong> representan una innovación clave en Delta Lake que mejora la eficiencia de las operaciones de escritura y reduce la E/S del sistema de forma drástica. Sin embargo, su adopción implica entender claramente las implicaciones del modelo <strong>Merge-on-Read</strong>, especialmente en términos de rendimiento de lectura.</p>
<p>En escenarios con cargas frecuentes y actualizaciones parciales —especialmente en capas Bronze y Silver— los DV proporcionan mejoras significativas. En contrapartida, en modelos orientados a lectura intensiva como las capas Gold, puede ser preferible mantener el enfoque tradicional Copy-on-Write o aplicar un mantenimiento regular que elimine los vectores acumulados.</p>
<p>En resumen:</p>
<ul>
<li><p><strong>DV aceleran la escritura</strong></p>
</li>
<li><p><strong>Penalizan las lecturas si no hay mantenimiento</strong></p>
</li>
<li><p><strong>Ofrecen el mejor rendimiento total cuando se combinan con OPTIMIZE</strong>.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Análisis del precio de las criptomonedas en tiempo real con Microsoft Fabric – Parte 4: Data Activator]]></title><description><![CDATA[⚡ Data Activator - Alertas en tiempo real
En esta sección exploraremos cómo utilizar Data Activator en Microsoft Fabric para monitorizar datos en tiempo real y generar alertas automáticas basadas en condiciones definidas sobre nuestras tablas, vistas...]]></description><link>https://datagym.es/analisis-del-precio-de-las-criptomonedas-en-tiempo-real-con-microsoft-fabric-parte-4-data-activator</link><guid isPermaLink="true">https://datagym.es/analisis-del-precio-de-las-criptomonedas-en-tiempo-real-con-microsoft-fabric-parte-4-data-activator</guid><category><![CDATA[microsoft fabric]]></category><category><![CDATA[Real-time-intelligence]]></category><category><![CDATA[Cryptocurrency]]></category><category><![CDATA[binance]]></category><category><![CDATA[MedallionArchitecture]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Mon, 24 Nov 2025 18:34:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762886252197/4f418744-a61f-4cfb-926e-6595f80d2415.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-data-activator-alertas-en-tiempo-real">⚡ Data Activator - Alertas en tiempo real</h1>
<p>En esta sección exploraremos cómo utilizar <strong>Data Activator</strong> en Microsoft Fabric para <strong>monitorizar datos en tiempo real</strong> y generar <strong>alertas automáticas</strong> basadas en condiciones definidas sobre nuestras tablas, vistas o flujos de eventos.</p>
<p>Data Activator nos permite reaccionar inmediatamente ante cambios en los datos, enviando notificaciones, disparando workflows o activando acciones cuando se cumplen determinadas condiciones.</p>
<hr />
<h2 id="heading-objetivo">🎯 Objetivo</h2>
<p>Aprender a configurar alertas en tiempo real con Data Activator usando dos enfoques distintos:</p>
<ul>
<li><p><strong>Método 1:</strong> Conectar un Eventstream a un Activator y definir reglas automáticas de compra/venta basadas en el precio.</p>
</li>
<li><p><strong>Método 2:</strong> Crear una alerta directamente sobre un visual del dashboard en tiempo real, sin necesidad de utilizar el Eventstream dentro del Activator.</p>
</li>
</ul>
<hr />
<h1 id="heading-metodo-1-crear-alertas-conectando-el-eventstream-a-un-activator">🟦 Método 1: Crear alertas conectando el Eventstream a un Activator</h1>
<p>Este método permite trabajar con los datos <strong>directamente en tiempo real</strong>, incluso antes de que se almacenen en una tabla. Las alertas se aplican sobre los eventos que llegan en streaming, lo que ofrece la mayor inmediatez posible.</p>
<h2 id="heading-1-conectar-el-eventstream-a-data-activator">🔌 1. Conectar el Eventstream a Data Activator</h2>
<ol>
<li><p>Abre tu <strong>Eventstream</strong> donde recibes los datos de Binance.</p>
</li>
<li><p>Añade un nuevo <strong>destino</strong> y selecciona <strong>Activator</strong>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763923479229/eb87c6cd-b934-42e8-9099-7db0e57ecc58.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Une el eventstream <strong>es_Crypto</strong> con el destino <strong>Activator</strong>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763923723353/ff7b64ef-a99f-4b56-b7bd-3763a0f50535.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>En el menú derecho, asigna un nombre descriptivo y crea un nuevo Activator. En este ejemplo lo llamamos: <code>act_Crypto</code></p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763923734587/0c182ad0-d067-4b3e-910c-8d8027d8d88e.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Publica los cambios. El Eventstream debería verse así:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763923762980/3fb84e00-8ab0-4bb3-ba9b-b37ee2405e2e.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<p>A partir de este momento, cada evento que llega al Eventstream se envía automáticamente al Activator en tiempo real.</p>
<hr />
<h2 id="heading-2-crear-las-reglas-automaticas">🧩 2. Crear las reglas automáticas</h2>
<p>Una vez creado el Activator, ábrelo para comenzar a configurar las reglas.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764007914603/f6720786-edaa-418a-b97a-c0c98c95153b.png" alt class="image--center mx-auto" /></p>
<p>En el explorador lateral verás el Eventstream recibido y un gráfico de ejemplo con los eventos en tiempo real.</p>
<hr />
<h3 id="heading-regla-1-comprar-btceur-si-el-precio-70000">🔔 Regla 1: Comprar BTCEUR si el precio ≤ 70.000€</h3>
<ol>
<li><p>En Data Activator, selecciona <strong>New rule</strong> en el menú superior.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764007946990/c9606369-97f8-4262-a3ef-0ab7b7ce1e58.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Selecciona el flujo de eventos recibido, en este caso: <code>es_Crypto-stream</code>.</p>
</li>
<li><p>En <em>Condition</em>, definimos lo siguiente:</p>
<ul>
<li><p><strong>Condition1</strong></p>
<ul>
<li><p><strong>Operation</strong>: Text state --&gt; Is equal to</p>
</li>
<li><p><strong>Column</strong>: <code>tickerInfo.symbol</code></p>
</li>
<li><p><strong>Value</strong>: <code>BTCEUR</code></p>
</li>
<li><p><strong>Default type</strong>: None</p>
</li>
</ul>
</li>
</ul>
</li>
</ol>
<p>    Añade una segunda condición pulsando <code>Add condition</code>.</p>
<ul>
<li><p><strong>Condition2</strong></p>
<ul>
<li><p><strong>Operation</strong>: Numeric state --&gt; Is less than or equal to</p>
</li>
<li><p><strong>Column</strong>: <code>tickerInfo.price</code></p>
</li>
<li><p><strong>Value</strong>: <code>70000</code></p>
</li>
<li><p><strong>Default type</strong>: None</p>
</li>
</ul>
</li>
</ul>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764007983194/295f43db-f17b-4060-88de-fe6eec5cb807.png" alt class="image--center mx-auto" /></p>
<ol start="4">
<li><p>Configura la acción clicando en <strong>Edit action</strong>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764008008333/6851a462-4fdc-40e2-b385-6826c50fd039.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Renombra la regla a <code>Compra BTCEUR</code></p>
</li>
</ol>
<hr />
<h3 id="heading-regla-2-vender-btceur-si-el-precio-100000">🔔 Regla 2: Vender BTCEUR si el precio ≥ 100.000€</h3>
<ol>
<li><p>Crea una nueva regla.</p>
</li>
<li><p>Define la condición: symbol == "BTCEUR" and price &gt;= 100000</p>
</li>
<li><p>Configura la acción de igual forma que se ha realizado en la regla anterior.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764008042463/5820ea9e-3f69-448c-b0b0-f685f16aa80e.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-resultado">🎯 Resultado</h2>
<p>Tras crear ambas reglas:</p>
<ul>
<li><p>Si <strong>BTCEUR baja a 70.000€ o menos</strong> → Se dispara una alerta de compra.</p>
</li>
<li><p>Si <strong>BTCEUR sube a 100.000€ o más</strong> → Se dispara una alerta de venta.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764008061262/d0babaea-115c-44cf-ba6a-4597bf467887.png" alt class="image--center mx-auto" /></p>
<p>Todo esto ocurre <strong>en tiempo real</strong>, directamente desde los eventos del Eventstream.</p>
<hr />
<h1 id="heading-metodo-2-crear-alertas-desde-un-visual-del-dashboard-en-tiempo-real">🟦 Método 2: Crear alertas desde un visual del dashboard en tiempo real</h1>
<p>Este método es más directo y muy útil cuando ya dispones de un dashboard publicado y quieres generar alertas <strong>sin necesidad de conectar el Eventstream al Activator</strong>.</p>
<h2 id="heading-como-funciona">📊 ¿Cómo funciona?</h2>
<p>Data Activator detecta automáticamente los datos que alimentan un visual y permite crear una alerta vinculada a ese visual concreto.</p>
<hr />
<h2 id="heading-pasos-para-crear-la-alerta-desde-el-dashboard">🔧 Pasos para crear la alerta desde el dashboard</h2>
<ol>
<li><p>Abre el dashboard en tiempo real <code>rd_Crypto</code>.</p>
</li>
<li><p>Selecciona el visual de <strong>Nº de criptomonedas</strong> de la página principal. Puedes crear la alerta clicando en el icono del rayo o en los tres puntos y seleccionando Set Alert.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764008439528/5968a887-7a5e-48e3-adb0-80d9e3f852c0.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Define la alerta en el menú lateral derecho de la siguiente forma:</p>
<ul>
<li><p><strong>Run query every</strong>: 1 hour</p>
</li>
<li><p><strong>Condition</strong>: Is not equal</p>
</li>
<li><p><strong>Value</strong>: 2794</p>
</li>
<li><p><strong>Action</strong>: Send me an email</p>
</li>
<li><p><strong>Save location</strong>: selecciona el Activator creado anteriormente (<code>act_Crypto</code>).</p>
</li>
</ul>
</li>
</ol>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764008484813/eda44758-2f37-4699-a258-6b83788f4c5f.png" alt class="image--center mx-auto" /></p>
<ol start="5">
<li><p>Crea la alerta.</p>
</li>
<li><p>Vuelve a abrir el artefacto <code>act_Crypto</code> y verás un nuevo flujo de datos proveniente del dashboard con la alerta recién creada.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764008513430/c6395a46-1aed-4266-9700-82b121f3020a.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Aquí podrás editar, mejorar o añadir acciones adicionales a la alerta.</p>
</li>
</ol>
<hr />
<h1 id="heading-conclusion">Conclusión</h1>
<p>    Gracias a Data Activator, puedes monitorizar precios de criptomonedas en tiempo real y generar alertas tanto desde el propio Eventstream como desde los visuales del dashboard.</p>
]]></content:encoded></item><item><title><![CDATA[Análisis del precio de las criptomonedas en tiempo real con Microsoft Fabric – Parte 3: Visualización de datos]]></title><description><![CDATA[En esta sección abordaremos la fase de análisis y visualización de nuestro proyecto en tiempo real.El objetivo es transformar los datos procesados en la arquitectura Medallion en insights visuales que faciliten la toma de decisiones rápidas y basadas...]]></description><link>https://datagym.es/analisis-del-precio-de-las-criptomonedas-en-tiempo-real-con-microsoft-fabric-parte-3-visualizacion-de-datos</link><guid isPermaLink="true">https://datagym.es/analisis-del-precio-de-las-criptomonedas-en-tiempo-real-con-microsoft-fabric-parte-3-visualizacion-de-datos</guid><category><![CDATA[microsoft fabric]]></category><category><![CDATA[Real-time-intelligence]]></category><category><![CDATA[Cryptocurrency]]></category><category><![CDATA[binance]]></category><category><![CDATA[MedallionArchitecture]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Thu, 06 Nov 2025 14:02:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1762437863916/d5255570-db85-4af0-8c40-11913daa4a70.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>En esta sección abordaremos la <strong>fase de análisis y visualización</strong> de nuestro proyecto en tiempo real.<br />El objetivo es transformar los datos procesados en la arquitectura Medallion en <strong>insights visuales</strong> que faciliten la toma de decisiones rápidas y basadas en datos.</p>
<hr />
<h2 id="heading-objetivo"><strong>Objetivo</strong></h2>
<p>Crear un dashboard en <strong>tiempo real</strong> que muestre métricas clave del mercado de criptomonedas, aprovechando las consultas KQL y las capacidades de visualización de Microsoft Fabric.</p>
<hr />
<h2 id="heading-estructura-del-dashboard-en-tiempo-real">Estructura del Dashboard en Tiempo Real</h2>
<p>Nuestro dashboard en <strong>Microsoft Fabric</strong> estará compuesto por dos páginas principales, cada una con un propósito bien definido:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Página</strong></td><td><strong>Descripción</strong></td><td><strong>Tipo de análisis</strong></td></tr>
</thead>
<tbody>
<tr>
<td>🏠 Principal</td><td>Muestra las métricas globales del mercado de criptomonedas</td><td>Agregaciones generales y KPIs</td></tr>
<tr>
<td>💡 Detalles</td><td>Profundiza en una criptomoneda específica</td><td>Análisis individual, histórico y variaciones</td></tr>
</tbody>
</table>
</div><hr />
<h2 id="heading-creando-un-dashboard-en-tiempo-real">Creando un Dashboard en Tiempo Real</h2>
<h3 id="heading-crear-el-dashboard">Crear el dashboard</h3>
<ol>
<li><p>En el <strong>workspace de Microsoft Fabric</strong>, selecciona <strong>New &gt; Real-Time Dashboard</strong>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760892591776/c9c573f2-1f64-49df-8571-77568487ce6d.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Asigna un nombre y crea el dashboard.</p>
</li>
<li><p>Ábrelo: verás que aparece con una única página y la opción de añadir un <strong>tile</strong> para comenzar a visualizar datos.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760892624235/e7c7ae89-8175-4ee5-a483-3c49579eda50.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<hr />
<h3 id="heading-configuracion-inicial-de-la-pagina">Configuración inicial de la página</h3>
<ul>
<li><p>Renombra la página como <strong>Principal</strong>.</p>
</li>
<li><p>Haz clic en <strong>Add tile</strong> para añadir la primera visualización.</p>
</li>
</ul>
<p>Al no tener todavía orígenes de datos conectados, aparecerá un aviso. Procedemos entonces a configurarlo.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760892655134/85c3b09c-1b87-43f4-8932-87ad79158368.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-conectar-el-origen-de-datos">Conectar el origen de datos</h3>
<ol>
<li><p>Haz clic en <strong>Data source</strong> y selecciona <strong>Eventhouse / KQL Database</strong>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760892680561/6183e87b-0afb-4ca9-80ac-b974330fb22e.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Selecciona la base de datos KQL creada previamente (por ejemplo: <code>eh_Crypto</code>).</p>
</li>
<li><p>Deja los valores por defecto y confirma para añadir el origen de datos.</p>
</li>
</ol>
<p>Ahora ya podemos escribir consultas KQL para construir visuales.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760892697267/a5663803-0ee9-4b28-85e7-6fc1053c27bf.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-crear-el-primer-visual-ultima-fecha-de-actualizacion">Crear el primer visual: última fecha de actualización</h3>
<ol>
<li><p>En el editor, escribe la siguiente consulta para obtener la última fecha registrada:</p>
<pre><code class="lang-python"> vwCrypto
 | top <span class="hljs-number">1</span> by serverTime
 | project serverTime
</code></pre>
</li>
<li><p>Ejecuta la consulta para obtener el resultado.</p>
</li>
<li><p>Haz clic en <strong>Add visual</strong> para darle formato al resultado en lugar de mostrarlo como tabla.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760892740392/e73a21a1-341c-487d-9ef7-4f74d57c2efa.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<hr />
<h3 id="heading-configurar-el-visual">Configurar el visual</h3>
<p>En el panel de configuración a la derecha, aplica los siguientes ajustes:</p>
<ul>
<li><p><strong>Tile name</strong>: <code>Última actualización</code></p>
</li>
<li><p><strong>Visual type</strong>: <code>Stat</code></p>
</li>
<li><p><strong>Value column</strong>: <code>serverTime</code></p>
</li>
</ul>
<p>Haz clic en <strong>Apply changes</strong> para confirmar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994015472/e2dcfbd1-bf82-439b-8bbc-7a46c3441739.png" alt class="image--center mx-auto" /></p>
<p>Ya tenemos el primer visual en nuestro dashboard en tiempo real.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994037925/d49925f8-2096-4d93-bd8e-2bbe2df68926.png" alt class="image--center mx-auto" /></p>
<hr />
<h3 id="heading-visual-2-numero-total-de-criptomonedas">Visual 2: Número total de criptomonedas</h3>
<p>Para mostrar cuántas criptomonedas distintas tenemos registradas:</p>
<ol>
<li><p>Para añadir un nuevo visual, se necesita añadir un nuevo tile</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994071889/e6c71b9b-6e77-44ae-9be3-06bb4477d481.png" alt /></p>
</li>
<li><p>En el editor del tile, escribe la siguiente consulta:</p>
<pre><code class="lang-python"> materialized_view(<span class="hljs-string">'mvCryptoGoldLatest'</span>)
 | summarize Cryptocurrencies = count_distinct(symbol)
</code></pre>
</li>
<li><p>Haz clic en <strong>Add visual</strong>.</p>
</li>
<li><p>Configura el visual:</p>
<ul>
<li><p><strong>Tile name</strong>: <code>Nº de criptomonedas</code></p>
</li>
<li><p><strong>Visual type</strong>: <code>Stat</code></p>
</li>
<li><p><strong>Value column</strong>: <code>Cryptocurrencies</code></p>
</li>
</ul>
</li>
</ol>
<p>Resultado: un contador en tiempo real del número de criptomonedas disponibles.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994089642/1c838cdb-b413-4bb0-a995-149db46aa102.png" alt /></p>
<hr />
<h3 id="heading-visual-3-top-5-criptomonedas-por-precio-eur">Visual 3: Top 5 criptomonedas por precio (EUR)</h3>
<p>Este visual permite identificar rápidamente las criptomonedas más valiosas en euros.</p>
<ol>
<li><p>Añade una nueva consulta en el dashboard:</p>
<pre><code class="lang-python">  materialized_view(<span class="hljs-string">'mvCryptoGoldLatest'</span>)
  | where symbol endswith <span class="hljs-string">"EUR"</span>
  | top <span class="hljs-number">5</span> by price
</code></pre>
</li>
<li><p>Haz clic en <strong>Add visual</strong>.</p>
</li>
<li><p>Configura el visual con estos parámetros:</p>
<ul>
<li><p><strong>Tile name</strong>: <code>Top 5 criptomonedas por precio (EUR)</code></p>
</li>
<li><p><strong>Visual type</strong>: <code>Column chart</code></p>
</li>
<li><p><strong>X axis</strong>: <code>symbol</code></p>
</li>
<li><p><strong>Y axis</strong>: <code>price</code></p>
</li>
</ul>
</li>
</ol>
<p>Con esto obtendremos un gráfico de barras que muestra las 5 criptomonedas con mayor precio en EUR.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994116709/fb775e72-14c9-40e0-97ac-9d8dc8c107ef.png" alt /></p>
<hr />
<h3 id="heading-visual-4-distribucion-por-rango-de-precios">Visual 4: Distribución por rango de precios</h3>
<p>Para analizar cómo se distribuyen las criptomonedas en función de su precio, agrupamos en rangos.</p>
<ol>
<li><p>Escribe la consulta:</p>
<pre><code class="lang-python">  materialized_view(<span class="hljs-string">'mvCryptoGoldLatest'</span>)
  | extend priceRange = case(
      price &lt; <span class="hljs-number">1</span>, <span class="hljs-string">"&lt; 1€"</span>,
      price &lt; <span class="hljs-number">100</span>, <span class="hljs-string">"1€ - 100€"</span>,
      price &lt; <span class="hljs-number">1000</span>, <span class="hljs-string">"100€ - 1000€"</span>,
      <span class="hljs-string">"&gt; 1000€"</span>
  )
  | summarize count() by priceRange
</code></pre>
</li>
<li><p>Haz clic en <strong>Add visual</strong>.</p>
</li>
<li><p>Configura el visual con estos parámetros:</p>
<ul>
<li><p><strong>Tile name</strong>: <code>Distribución por rango de precios</code></p>
</li>
<li><p><strong>Visual type</strong>: <code>Pie chart</code></p>
</li>
<li><p><strong>Category column</strong>: <code>priceRange</code></p>
</li>
<li><p><strong>Numeric column</strong>: <code>count_</code></p>
</li>
<li><p><strong>Tooltip</strong>: <code>value</code></p>
</li>
</ul>
</li>
</ol>
<p>De esta manera obtenemos un gráfico circular que refleja qué proporción de criptomonedas se encuentra en cada rango de precios y si nos posicionamos en el gráfico, nos muestra el conteo de criptomonedas que hay en cada rango de precios.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994141403/489170f7-72c1-48f9-b89d-9db895b60f0c.png" alt /></p>
<hr />
<h3 id="heading-visual-5-logo-binance">Visual 5: Logo Binance</h3>
<p>Para personalizar el dashboard y hacerlo más identificable con la temática del proyecto, añadiremos el logo de Binance en la parte superior.</p>
<ol>
<li><p>En el dashboard en tiempo real, haz clic en "<strong>New text tile</strong>".</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994169474/53303053-0386-41a9-ae17-5109e0073ceb.png" alt /></p>
</li>
<li><p>En el editor del texto, inserta el siguiente código Markdown:</p>
<pre><code class="lang-Markdown"> ![<span class="hljs-string">Binance</span>](<span class="hljs-link">https://th.bing.com/th/id/R.92d4f7ef666ddb42051d90f0333df1cf?rik=Rw5e3HhVIbBAng&amp;riu=http%3a%2f%2ffreelogopng.com%2fimages%2fall_img%2f1681906406binance-icon-png.png&amp;ehk=zKWRuKmvAqHAESxhJN0LnJWOczq0vpRcKTKxNrZMaZQ%3d&amp;risl=&amp;pid=ImgRaw&amp;r=0 "Binance"</span>)
</code></pre>
</li>
<li><p>Ajusta el alineamiento y el tamaño del tile para que el logo se vea correctamente.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994188561/ede2518f-9783-4eec-8334-22bd8b662c57.png" alt /></p>
</li>
</ol>
<hr />
<h2 id="heading-pagina-de-detalles-por-criptomoneda">📑 Página de Detalles por criptomoneda</h2>
<p>Hasta ahora hemos construido la <strong>página principal</strong> con métricas globales del mercado.<br />El siguiente paso es crear una <strong>página de Detalles</strong>, donde podremos profundizar en la información de una criptomoneda específica.</p>
<hr />
<h3 id="heading-creando-la-pagina-de-detalles">🔧 Creando la página de Detalles</h3>
<ol>
<li><p>Dentro del <strong>Real-Time Dashboard</strong>, haz clic en <strong>Add page</strong>.</p>
</li>
<li><p>Asigna el nombre <strong>Detalles</strong>.</p>
</li>
<li><p>En esta nueva página, los visuales estarán filtrados por una única criptomoneda.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994251089/3046c164-4550-41b8-9898-28ae3ac3a227.png" alt /></p>
</li>
</ol>
<hr />
<h3 id="heading-creando-un-parametro-de-filtrado">Creando un parámetro de filtrado</h3>
<p>Para permitir seleccionar qué criptomoneda analizar (y también habilitar el <strong>drillthrough</strong> desde la página principal):</p>
<ol>
<li><p>En el menú superior del dashboard, selecciona <strong>Manage &gt; Parameters</strong>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994277086/ab778741-69c6-461f-bd85-dc915251bf50.png" alt /></p>
</li>
<li><p>En el menú lateral del dashboard, selecciona <strong>Add</strong>.</p>
</li>
<li><p>Configura el parámetro con las siguientes opciones:</p>
<ul>
<li><p><strong>Label:</strong> <code>Criptomoneda</code></p>
</li>
<li><p><strong>Parameter type:</strong> <code>Single selection</code></p>
</li>
<li><p><strong>Variable name</strong>: <code>Criptomoneda</code></p>
</li>
<li><p><strong>Data type</strong>: <code>string</code></p>
</li>
<li><p><strong>Show on page</strong>: <code>Detalles</code></p>
</li>
<li><p><strong>Source</strong>: <code>Query</code></p>
<ul>
<li><p><strong>Data source</strong>: Selecciona el eventhouse <code>eh_Crypto</code></p>
</li>
<li><p><strong>Query</strong>: <code>kql materialized_view('mvCryptoGoldLatest') distinct symbol</code></p>
</li>
<li><p><strong>Value column</strong>: <code>symbol</code></p>
</li>
</ul>
</li>
<li><p><strong>Default value:</strong> <code>BTCEUR</code></p>
</li>
</ul>
</li>
</ol>
<hr />
<h3 id="heading-creando-los-visuales">Creando los visuales</h3>
<h4 id="heading-logo-binance">🪙 <strong>Logo Binance</strong></h4>
<p>Al igual que en la página principal, comenzamos añadiendo un encabezado visual para mantener la coherencia del dashboard y reforzar la identidad del proyecto.</p>
<hr />
<h4 id="heading-visual-nombre-de-la-criptomoneda-seleccionada"><strong>Visual: Nombre de la criptomoneda seleccionada</strong></h4>
<p>El objetivo de este visual es mostrar dinámicamente el nombre de la criptomoneda seleccionada en la página de detalles.</p>
<ol>
<li><p>Crea un nuevo tile en el dashboard.</p>
</li>
<li><p>Escribe la siguiente consulta KQL para mostrar el símbolo de la criptomoneda seleccionada:</p>
<pre><code class="lang-python"> materialized_view(<span class="hljs-string">'mvCryptoGoldLatest'</span>)
 | where symbol == Criptomoneda
 | project symbol
</code></pre>
<blockquote>
<p>🔧 Criptomoneda es el parámetro dinámico del dashboard que hemos configurado anteriormente.</p>
</blockquote>
</li>
<li><p>Haz clic en <strong>Add visual</strong> y configura el visual con estos parámetros:</p>
<ul>
<li><p><strong>Hide Tile name</strong></p>
</li>
<li><p><strong>Visual type</strong>: <code>Stat</code></p>
</li>
</ul>
</li>
</ol>
<p>    Haz clic en Apply changes para guardar el visual.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994326816/ce49d95a-a45e-476a-82eb-ab51496b51ba.png" alt /></p>
<hr />
<h4 id="heading-visual-valor-maximo-del-rango-de-tiempo"><strong>Visual: Valor máximo del rango de tiempo</strong></h4>
<p>Este visual muestra el <strong>precio máximo alcanzado</strong> por la criptomoneda seleccionada dentro del rango de tiempo actual.</p>
<ol>
<li><p>Escribe la consulta:</p>
<pre><code class="lang-python"> vwCrypto
 | where symbol == Criptomoneda
 | where serverTime between (_startTime .. _endTime)
 | summarize max(price)
</code></pre>
<blockquote>
<p>🔧 <code>Criptomoneda</code> corresponde a la criptomoneda seleccionada. <code>_startTime</code> y <code>_endTime</code> son los parámetros automáticos del dashboard para el rango de tiempo.</p>
</blockquote>
</li>
<li><p>Haz clic en <strong>Add visual</strong>.</p>
</li>
<li><p>Configura el visual con estos parámetros:</p>
<ul>
<li><p><strong>Tile name</strong>: <code>Precio máximo</code></p>
</li>
<li><p><strong>Visual type</strong>: <code>Stat</code></p>
</li>
</ul>
</li>
</ol>
<hr />
<h4 id="heading-visual-valor-minimo-del-rango-de-tiempo"><strong>Visual: Valor mínimo del rango de tiempo</strong></h4>
<p>Este visual muestra el <strong>precio mínimo alcanzado</strong> por la criptomoneda seleccionada dentro del rango de tiempo actual.</p>
<ol>
<li><p>Escribe la consulta:</p>
<pre><code class="lang-python"> vwCrypto
 | where symbol == Criptomoneda
 | where serverTime between (_startTime .. _endTime)
 | summarize min(price)
</code></pre>
<blockquote>
<p>🔧 <code>Criptomoneda</code> corresponde a la criptomoneda seleccionada. <code>_startTime</code> y <code>_endTime</code> son los parámetros automáticos del dashboard para el rango de tiempo.</p>
</blockquote>
</li>
<li><p>Haz clic en <strong>Add visual</strong>.</p>
</li>
<li><p>Configura el visual con estos parámetros:</p>
<ul>
<li><p><strong>Tile name</strong>: <code>Precio mínimo</code></p>
</li>
<li><p><strong>Visual type</strong>: <code>Stat</code></p>
</li>
</ul>
</li>
</ol>
<hr />
<h4 id="heading-visual-precio-actual"><strong>Visual: Precio actual</strong></h4>
<p>Este visual muestra el <strong>precio más reciente (actual)</strong> de la criptomoneda seleccionada.</p>
<ol>
<li><p>Escribe la consulta:</p>
<pre><code class="lang-python"> mvCryptoGoldLatest
 | where symbol == Criptomoneda
 | top <span class="hljs-number">1</span> by serverTime desc
 | project current_price = price
</code></pre>
</li>
<li><p>Haz clic en <strong>Add visual</strong>.</p>
</li>
<li><p>Configura el visual con estos parámetros:</p>
<ul>
<li><p><strong>Tile name</strong>: <code>Precio actual</code></p>
</li>
<li><p><strong>Visual type</strong>: <code>Stat</code></p>
</li>
</ul>
</li>
</ol>
<hr />
<h4 id="heading-visual-variacion-porcentual-entre-valor-maximo-y-minimo"><strong>Visual: Variación porcentual entre valor máximo y mínimo</strong></h4>
<p>Este visual muestra la <strong>variación porcentual entre el valor máximo y el valor mínimo</strong> de la criptomoneda en el rango de tiempo seleccionado.</p>
<ol>
<li><p>Escribe la consulta:</p>
<pre><code class="lang-python"> vwCrypto
 | where symbol == Criptomoneda
 | where serverTime between (_startTime.._endTime)
 | summarize 
    max_price = max(price),
    min_price = min(price)
 | extend variation_pct = ((max_price - min_price) / min_price) * <span class="hljs-number">100</span>
 | project variation_pct
</code></pre>
</li>
<li><p>Haz clic en <strong>Add visual</strong>.</p>
</li>
<li><p>Configura el visual con estos parámetros:</p>
<ul>
<li><p><strong>Tile name</strong>: <code>Variación % (Máx vs Mín)</code></p>
</li>
<li><p><strong>Visual type</strong>: <code>Stat</code></p>
</li>
<li><p><strong>Text size</strong>: <code>Small</code></p>
</li>
</ul>
</li>
</ol>
<hr />
<h4 id="heading-visual-variacion-porcentual-entre-el-valor-actual-y-el-ultimo-valor-del-rango-de-tiempo"><strong>Visual: Variación porcentual entre el valor actual y el último valor del rango de tiempo</strong></h4>
<p>Este visual calcula la variación porcentual entre el precio actual (obtenido desde la vista materializada) y el último valor registrado dentro del rango de tiempo seleccionado.</p>
<p>Esto permite ver si la criptomoneda ha subido o bajado recientemente.</p>
<ol>
<li><p>Escribe la consulta:</p>
<pre><code class="lang-python"> let current_price = 
    mvCryptoGoldLatest
    | where symbol == Criptomoneda;
 let last_historical = 
    vwCrypto
    | where symbol == Criptomoneda
    | where serverTime between (_startTime .. _endTime)
    | top <span class="hljs-number">1</span> by serverTime asc
    | project lh_symbol = symbol, lh_serverTime = serverTime, lh_price = price;
 current_price
 | join kind=inner last_historical on $left.symbol == $right.lh_symbol
 | extend variation_pct = ((price - lh_price) / price) * <span class="hljs-number">100</span>
 | project variation_pct
</code></pre>
</li>
<li><p>Haz clic en <strong>Add visual</strong>.</p>
</li>
<li><p>Configura el visual con estos parámetros:</p>
<ul>
<li><p><strong>Tile name</strong>: <code>Variación % (Actual vs Último)</code></p>
</li>
<li><p><strong>Visual type</strong>: <code>Stat</code></p>
</li>
<li><p><strong>Text size</strong>: <code>Small</code></p>
</li>
</ul>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994427926/1aefc6f8-0d85-4cec-a785-04fee16a1d7e.png" alt /></p>
<hr />
<h2 id="heading-aplicando-formato-condicional-a-los-visuales">🎨 Aplicando Formato Condicional a los Visuales</h2>
<p>El formato condicional permite destacar visualmente los cambios en las métricas clave, ayudando a identificar tendencias positivas o negativas de un vistazo.</p>
<p>En el caso de los precios o variaciones porcentuales de criptomonedas, aplicar color en función del resultado mejora la interpretación inmediata del dashboard.</p>
<hr />
<h3 id="heading-formato-condicional-en-el-visual-variacion-actual-vs-ultimo"><strong>Formato condicional en el visual “Variación % (Actual vs Último)</strong></h3>
<ol>
<li><p>Selecciona el visual “Variación % (Actual vs Último)”.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994503716/ef400ef1-4e80-4078-bd97-89d0f735f7ad.png" alt /></p>
</li>
<li><p>Desplázate hasta la sección Conditional formatting (Formato condicional) y asegúrate que está habilitada.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994512312/4acb69b6-feb7-4463-aa77-a3444f5a9993.png" alt /></p>
</li>
<li><p>Añade una regla:</p>
<ul>
<li><p><strong>Color style</strong>: <code>Light</code></p>
</li>
<li><p><strong>Column</strong>: <code>variation_pct</code></p>
</li>
<li><p><strong>Operator</strong>: <code>&gt;</code></p>
</li>
<li><p><strong>Value</strong>: <code>0</code></p>
</li>
<li><p><strong>Color</strong>: <code>Green</code></p>
</li>
<li><p><strong>Icon</strong>: <code>⬆️</code></p>
</li>
</ul>
</li>
</ol>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994520364/0bf4bc99-817f-40c7-a0a8-fbd53c6f9f4c.png" alt /></p>
<ol start="4">
<li><p>Haz clic en guardar.</p>
</li>
<li><p>Crea dos reglas más:</p>
<ul>
<li><p><strong>Valor == 0</strong>: Con color Azul sin icono.</p>
</li>
<li><p><strong>Valor &lt; 0</strong>: Con color Rojo y con el icono de la felcha hacia abajo.</p>
</li>
</ul>
</li>
</ol>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994531635/2a4802a9-414e-461c-9439-52a5d262fa98.png" alt /></p>
<p>Realiza el mismo ejercicio para el visual <code>Variación % (Actual vs Último)</code>.</p>
<p>El dashboard debería de quedar así:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760994544369/3255426e-4c7e-461b-9aa0-ae72363f8244.png" alt /></p>
<hr />
<h2 id="heading-configurar-el-auto-refresh">🔄 Configurar el Auto Refresh</h2>
<p>Una de las grandes ventajas de los dashboards en tiempo real de Microsoft Fabric es su capacidad para actualizar automáticamente los datos sin intervención manual. Esto garantiza que las métricas y visuales reflejen siempre la información más reciente procedente de las vistas materializadas o tablas KQL.</p>
<h3 id="heading-pasos-para-configurar-el-auto-refresh">⚙️ Pasos para configurar el Auto Refresh</h3>
<ol>
<li>En la barra superior, haz clic en <strong>Manage</strong> y selecciona <strong>Auto refresh</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760995254215/226507c4-36a0-42dc-9c62-26cfbb5fa2c8.png" alt /></p>
<ol start="2">
<li>Activa la opción Enable y define:</li>
</ol>
<ul>
<li><p><strong>Minimun time interval</strong>: <em>Allow all refresh intervals</em></p>
</li>
<li><p><strong>Default refresh rate</strong>: <em>Continuous^</em></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760995260930/65f7baba-f3b6-42bb-acae-3d9b5104b22b.png" alt /></p>
</li>
</ul>
<ol start="3">
<li>Haz clic en <strong>Apply</strong> para aplicar los cambios.</li>
</ol>
<p>Una vez configurado el auto refresh, todos los visuales del dashboard se actualizarán automáticamente con los últimos datos disponibles.</p>
]]></content:encoded></item><item><title><![CDATA[Análisis del precio de las criptomonedas en tiempo real con Microsoft Fabric – Parte 2: Transformación y preparación analítica de datos]]></title><description><![CDATA[🎯 Objetivo
En esta fase abordaremos la transformación, limpieza y preparación analítica de los datos obtenidos en la capa Bronze.El propósito es construir las capas Silver y Gold dentro de nuestra arquitectura Medallion, garantizando datos consisten...]]></description><link>https://datagym.es/analisis-del-precio-de-las-criptomonedas-en-tiempo-real-con-microsoft-fabric-parte-2-transformacion-y-preparacion-analitica-de-datos</link><guid isPermaLink="true">https://datagym.es/analisis-del-precio-de-las-criptomonedas-en-tiempo-real-con-microsoft-fabric-parte-2-transformacion-y-preparacion-analitica-de-datos</guid><category><![CDATA[microsoft fabric]]></category><category><![CDATA[Real-time-intelligence]]></category><category><![CDATA[Cryptocurrency]]></category><category><![CDATA[binance]]></category><category><![CDATA[MedallionArchitecture]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Fri, 24 Oct 2025 10:11:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760889058538/b9fe7763-06b2-4bbc-8d62-32b6d8c6b7dd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-objetivo"><strong>🎯 Objetivo</strong></h1>
<p>En esta fase abordaremos la <strong>transformación, limpieza y preparación analítica de los datos</strong> obtenidos en la capa Bronze.<br />El propósito es construir las capas <strong>Silver</strong> y <strong>Gold</strong> dentro de nuestra arquitectura Medallion, garantizando datos <strong>consistentes, enriquecidos y optimizados</strong> para el análisis.</p>
<h1 id="heading-capa-silver-transformacion-y-enriquecimiento-de-datos">🥈 Capa Silver - Transformación y enriquecimiento de datos</h1>
<p>La <strong>capa Silver</strong> representa el siguiente paso tras la ingestión de datos crudos en la base de datos KQL. Su objetivo es <strong>transformar, limpiar, enriquecer y estructurar</strong> los datos provenientes de la capa RAW para que estén listos para análisis más complejos o visualizaciones.</p>
<p>En esta etapa se aplican transformaciones como:</p>
<ul>
<li><p>Conversión de formatos de fecha/hora.</p>
</li>
<li><p>Extracción de campos anidados (por ejemplo, desde JSON).</p>
</li>
<li><p>Tipado correcto de columnas (por ejemplo, <code>price</code> como <code>real</code>).</p>
</li>
<li><p>Enriquecimiento de datos con campos adicionales.</p>
</li>
<li><p>Eliminación de duplicados.</p>
</li>
</ul>
<p>Además, gracias a las <strong>Update Policies</strong>, estas transformaciones se ejecutan de forma <strong>automática y en tiempo real</strong>, cada vez que nuevos datos se insertan en la tabla RAW.</p>
<h2 id="heading-transformaciones-en-tiempo-real-con-update-policies">🔁 Transformaciones en tiempo real con Update Policies</h2>
<h3 id="heading-que-son-las-update-policies">🧠 ¿Qué son las Update Policies?</h3>
<p>Las <strong>Update Policies</strong> en KQL permiten definir reglas que se ejecutan automáticamente cuando una tabla origen recibe nuevos datos. Estas reglas aplican transformaciones predefinidas y almacenan los resultados en una tabla destino, facilitando la creación de capas como Silver o Gold.</p>
<p>Son especialmente útiles para:</p>
<ul>
<li><p>Automatizar procesos de transformación.</p>
</li>
<li><p>Aplicar lógica de negocio sin depender de pipelines externos.</p>
</li>
<li><p>Mantener capas sincronizadas sin esfuerzo adicional.</p>
</li>
</ul>
<p>📚 <a target="_blank" href="https://learn.microsoft.com/en-us/kusto/management/update-policy?view=microsoft-fabric">Documentación oficial</a></p>
<p><img src="https://learn.microsoft.com/en-us/kusto/management/media/updatepolicy/update-policy-overview.png?view=microsoft-fabric" alt="Diagram shows an overview of the update policy." /></p>
<hr />
<p>Para poder lanzar consultas kql, utilizaremos un nuevo artefacto llamado KQL Queryset. Este artefacto lo podemos crear en nuestra área de trabajo o utilizar el que viene por defecto cuando creamos el Eventhouse.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760889819324/391df2b2-2410-429e-9fcd-97baa1e168e3.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-1-crear-la-tabla-destino-cryptosilver">1. Crear la tabla destino (<code>Crypto_Silver</code>)</h3>
<pre><code class="lang-python">.create table Crypto_Silver (
    serverTime: datetime,
    symbol: string,
    price: real
) 
<span class="hljs-keyword">with</span> (folder = <span class="hljs-string">"Silver"</span>)
</code></pre>
<h3 id="heading-2-crear-una-funcion-de-transformacion">2. Crear una función de transformación</h3>
<p>Esta función convierte el <code>serverTime</code> desde milisegundos Unix a <code>datetime</code>, analiza el campo JSON <code>tickerInfo</code> y extrae el <code>symbol</code> y el <code>price</code> como columnas limpias.</p>
<pre><code class="lang-python">.create-<span class="hljs-keyword">or</span>-alter function LoadCryptoToSilver {
    Crypto_RAW
    | extend serverTime = unixtime_milliseconds_todatetime(serverTime), j = parse_json(tickerInfo)
    | extend symbol = tostring(j.symbol), price = toreal(j.price)
    | project serverTime, symbol, price
}
</code></pre>
<h3 id="heading-3-crear-y-activar-la-update-policy">3. Crear y activar la Update Policy</h3>
<p>Con esta política, cualquier nuevo dato que entre en <code>Crypto_RAW</code> activará automáticamente la función anterior y los resultados se escribirán en <code>Crypto_Silver</code>.</p>
<pre><code class="lang-python">.alter table Crypto_Silver policy update 
```[
    {
        <span class="hljs-string">"IsEnabled"</span>: true,
        <span class="hljs-string">"Source"</span>: <span class="hljs-string">"Crypto_RAW"</span>,
        <span class="hljs-string">"Query"</span>: <span class="hljs-string">"LoadCryptoToSilver"</span>,
        <span class="hljs-string">"IsTransactional"</span>: true,
        <span class="hljs-string">"PropagateIngestionProperties"</span>: false
    }
]```
</code></pre>
<h3 id="heading-validacion">✅ Validación</h3>
<p>Una vez configurado:</p>
<ul>
<li><p>Puedes consultar la tabla <code>Crypto_Silver</code> para ver los datos limpios, convertidos y estructurados.</p>
</li>
<li><p>La transformación ocurre en tiempo real sin intervención manual.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760889964268/60599377-fed4-49c1-8e4e-dc3b49c42b70.png" alt class="image--center mx-auto" /></p>
<hr />
<h1 id="heading-capa-gold-agregacion-y-preparacion-analitica">🥇 Capa Gold - Agregación y preparación analítica</h1>
<p>La <strong>capa Gold</strong> representa la última fase de nuestra arquitectura Medallion, enfocada en <strong>proveer datos listos para análisis, visualización y toma de decisiones</strong>.</p>
<p>En esta etapa, trabajamos sobre los datos transformados y enriquecidos de la capa Silver, y generamos entidades optimizadas para responder a necesidades analíticas específicas.</p>
<p>En nuestro proyecto, vamos a seguir un <strong>doble enfoque</strong>:</p>
<ol>
<li><p><strong>Vista materializada</strong> → para obtener de forma optimizada el <strong>último valor</strong> de cada criptomoneda cuyo precio sea mayor a 0.</p>
</li>
<li><p><strong>Función y tabla silver</strong> → para consultar el <strong>histórico</strong> filtrado y así poder analizar la evolución de precios y tendencias.</p>
</li>
</ol>
<hr />
<h2 id="heading-que-es-una-vista-materializada">¿Qué es una vista materializada?</h2>
<p>Una <strong>vista materializada</strong> en Kusto (KQL) es una estructura optimizada que almacena físicamente los resultados de una consulta. A diferencia de una vista tradicional —que recalcula los datos en cada ejecución—, la vista materializada <strong>mantiene los resultados precalculados y actualizados automáticamente</strong> según los cambios en la tabla origen.</p>
<h3 id="heading-ventajas">✅ Ventajas</h3>
<ul>
<li><p>Rendimiento optimizado en consultas frecuentes o complejas.</p>
</li>
<li><p>Datos precalculados listos para su uso en dashboards o KPIs.</p>
</li>
<li><p>Menor carga de procesamiento sobre las tablas base.</p>
</li>
</ul>
<p>📚 <a target="_blank" href="https://learn.microsoft.com/es-es/kusto/management/materialized-views/materialized-view-overview?view=microsoft-fabric">Documentación oficial - Vista materializada</a></p>
<hr />
<h3 id="heading-vista-materializada-ultimo-valor-por-criptomoneda">Vista materializada: Último valor por criptomoneda</h3>
<p>Para optimizar el acceso a los precios más recientes, creamos una vista materializada llamada <code>mvCryptoGoldLatest</code> que devuelve el último registro disponible de cada criptomoneda con precio superior a 0.</p>
<pre><code class="lang-python">.create-<span class="hljs-keyword">or</span>-alter materialized-view <span class="hljs-keyword">with</span> (backfill = true) mvCryptoGoldLatest on table Crypto_Silver
{
    Crypto_Silver
    | where price &gt; <span class="hljs-number">0</span>
    | summarize arg_max(serverTime, *) by symbol
}
</code></pre>
<ul>
<li><p><code>backfill=true</code>: rellena la vista con los datos históricos existentes.</p>
</li>
<li><p><code>arg_max(serverTime, *)</code>: selecciona el registro con la fecha más reciente (serverTime) para cada símbolo (symbol).</p>
</li>
</ul>
<p>🔹 Casos de uso:</p>
<ul>
<li><p>Obtener la foto actual del mercado de criptomonedas.</p>
</li>
<li><p>Mostrar los últimos valores en tarjetas o KPIs dentro de dashboards.</p>
</li>
<li><p>Evitar valores nulos o sin precio.</p>
</li>
</ul>
<hr />
<h2 id="heading-funcion-historico-filtrado">Función: Histórico filtrado</h2>
<p>Para análisis históricos y estudios de tendencias, creamos una <strong>vista normal</strong> llamada <code>vwCrypto</code> que devuelve todas las criptomonedas cuyo precio sea mayor a 0.</p>
<pre><code class="lang-python">.create-<span class="hljs-keyword">or</span>-alter function <span class="hljs-keyword">with</span>(view=true) vwCrypto()
{
    Crypto_Silver
    | where price &gt; <span class="hljs-number">0</span>
}
</code></pre>
<p>🔹 Casos de uso:</p>
<ul>
<li><p>Analizar la evolución temporal del precio de una criptomoneda.</p>
</li>
<li><p>Calcular métricas de volatilidad, medias móviles o comparativas históricas.</p>
</li>
</ul>
<hr />
<h2 id="heading-consultando-los-datos-en-tiempo-real">📊 Consultando los datos en tiempo real</h2>
<p>En la capa Gold, disponemos de tres formas principales de acceder a los datos:</p>
<ul>
<li><p>Directamente desde la tabla Silver</p>
</li>
<li><p>Mediante vista materializada (<code>mvCryptoGoldLatest</code>)</p>
</li>
<li><p>A través de la función (<code>vwCrypto</code>)</p>
</li>
</ul>
<h3 id="heading-formas-de-consultar-una-vista-materializada">🔍 Formas de consultar una vista materializada</h3>
<p>En Kusto, existen dos maneras de consultar una vista materializada, dependiendo de tus necesidades de rendimiento o consistencia de datos:</p>
<h4 id="heading-consultar-toda-la-vista"><strong>Consultar toda la vista</strong></h4>
<p>Puedes consultar la vista materializada directamente por su nombre, como si fuera una tabla normal:</p>
<pre><code class="lang-python">mvCryptoGoldLatest
</code></pre>
<p>Esta consulta combina automáticamente:</p>
<ul>
<li><p>La parte materializada (ya precalculada y almacenada).</p>
</li>
<li><p>Los registros recientes de la tabla de origen que aún no han sido materializados (.delta).</p>
</li>
</ul>
<p>✅ Ventajas:</p>
<ul>
<li>Siempre devuelve los datos más actualizados, incluyendo los registros recién ingeridos.</li>
</ul>
<p>⚠️ Consideraciones:</p>
<ul>
<li><p>Puede tener menor rendimiento, ya que necesita materializar parte del delta en tiempo de consulta.</p>
</li>
<li><p>El rendimiento depende de la antigüedad de la vista y de los filtros aplicados.</p>
</li>
</ul>
<p><strong>Consultar solo la parte materializada</strong></p>
<p>También puedes usar la función <code>materialized_view()</code> para consultar únicamente la parte ya materializada:</p>
<pre><code class="lang-python">materialized_view(<span class="hljs-string">'mvCryptoGoldLatest'</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760892116561/8227379a-794a-4ad1-a0aa-66fb4f9eeee0.png" alt class="image--center mx-auto" /></p>
<p>✅ Ventajas:</p>
<ul>
<li><p>Ofrece el mejor rendimiento posible, al leer solo los datos ya materializados.</p>
</li>
<li><p>Ideal para dashboards en tiempo real o escenarios de telemetría, donde prima la rapidez.</p>
</li>
</ul>
<p>⚠️ Consideraciones:</p>
<ul>
<li><p>No garantiza que se incluyan los registros más recientes aún no materializados.</p>
</li>
<li><p>Puede haber una ligera latencia entre la ingesta de datos y su aparición en los resultados.</p>
</li>
</ul>
<p>📚 <a target="_blank" href="https://learn.microsoft.com/es-es/kusto/management/materialized-views/materialized-view-overview?view=microsoft-fabric&amp;preserve-view=true#materialized-views-queries">Documentación oficial - Consultas sobre vistas materializadas</a></p>
<hr />
<h3 id="heading-vista-normal-para-historico">Vista normal para histórico</h3>
<p>La vista <code>vwCrypto()</code> permite consultar <strong>todo el histórico de precios</strong> de criptomonedas con un valor mayor a 0. Es ideal para analizar la evolución temporal, tendencias o realizar cálculos estadísticos como variaciones porcentuales, medias móviles o volatilidad</p>
<pre><code class="lang-python">vwCrypto
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760892129451/cc0eaa03-6bbb-43bf-9e13-245206b44486.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-formas-de-consultar-una-vista-normal-funcion-kql">🔍 Formas de consultar una vista normal (función KQL)</h3>
<p>A diferencia de las vistas materializadas, las vistas normales o funciones con <code>view=true</code> no almacenan físicamente los datos, sino que <strong>ejecutan la consulta en tiempo real</strong> cada vez que se utilizan.</p>
<p>Esto las hace muy útiles para escenarios donde se requiere flexibilidad y actualización continua, aunque con un pequeño coste en rendimiento frente a las materializadas.</p>
<h4 id="heading-consultar-directamente-la-vista"><strong>Consultar directamente la vista</strong></h4>
<p>Puedes invocar la vista simplemente escribiendo su nombre o función, como cualquier tabla:</p>
<pre><code class="lang-python">vwCrypto
</code></pre>
<p>o</p>
<pre><code class="lang-python">vwCrypto()
</code></pre>
<p>✅ Ventajas:</p>
<ul>
<li><p>Siempre devuelve los datos más recientes desde la tabla de origen.</p>
</li>
<li><p>Permite aplicar filtros, joins o agregaciones de forma dinámica.</p>
</li>
<li><p>Ideal para análisis exploratorios o consultas personalizadas en dashboards.</p>
</li>
</ul>
<p>⚠️ Consideraciones:</p>
<ul>
<li><p>Cada ejecución vuelve a procesar la lógica definida en la vista.</p>
</li>
<li><p>Puede tener un mayor coste computacional en vistas con transformaciones complejas.</p>
</li>
</ul>
<h4 id="heading-integrar-la-vista-dentro-de-otras-consultas-kql"><strong>Integrar la vista dentro de otras consultas KQL</strong></h4>
<p>Una práctica muy común es usar la vista <code>vwCrypto()</code> como fuente de datos dentro de consultas más complejas o cálculos derivados:</p>
<pre><code class="lang-python">vwCrypto()
| where symbol == <span class="hljs-string">"BTCEUR"</span>
| summarize avgPrice = avg(price) by bin(serverTime, <span class="hljs-number">1</span>h)
</code></pre>
<p>✅ Ventajas:</p>
<ul>
<li><p>Permite encadenar transformaciones y análisis sobre los datos ya filtrados.</p>
</li>
<li><p>Simplifica la lectura del código al reutilizar lógica definida en una única vista.</p>
</li>
</ul>
<p>⚠️ Consideraciones:</p>
<ul>
<li><p>Al no estar materializada, cada ejecución recalcula los resultados.</p>
</li>
<li><p>En escenarios de alto volumen de datos o consultas frecuentes, puede ser preferible usar una vista materializada.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Análisis del precio de las criptomonedas en tiempo real con Microsoft Fabric – Parte 1: Ingesta de datos con un Eventstream]]></title><description><![CDATA[🎯 Objetivo
En este primer paso diseñaremos la arquitectura general del proyecto, crearemos los elementos necesarios en Microsoft Fabric para recibir y almacenar datos en tiempo real, y desarrollaremos un script en Python para capturar datos de cript...]]></description><link>https://datagym.es/analisis-del-precio-de-las-criptomonedas-en-tiempo-real-con-microsoft-fabric-parte-1-ingesta-de-datos-con-un-eventstream</link><guid isPermaLink="true">https://datagym.es/analisis-del-precio-de-las-criptomonedas-en-tiempo-real-con-microsoft-fabric-parte-1-ingesta-de-datos-con-un-eventstream</guid><category><![CDATA[Cryptocurrency]]></category><category><![CDATA[binance]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[Real-time-intelligence]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Thu, 16 Oct 2025 18:54:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760374194916/4a3b6172-42e7-4900-95f5-94556bb1b0e4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-objetivo"><strong>🎯 Objetivo</strong></h1>
<p>En este primer paso diseñaremos la arquitectura general del proyecto, crearemos los elementos necesarios en Microsoft Fabric para recibir y almacenar datos en tiempo real, y desarrollaremos un script en Python para capturar datos de criptomonedas desde la API pública de Binance.</p>
<hr />
<h1 id="heading-arquitectura-del-proyecto"><strong>🧱 Arquitectura del proyecto</strong></h1>
<p>A continuación, se muestra un esquema de alto nivel de la solución:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760377461080/356e2600-56a8-4c05-8884-080de38a7baf.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-componentes-principales"><strong>Componentes principales:</strong></h2>
<ul>
<li><p><strong>Eventstream</strong>: canal de ingesta en tiempo real que recibe eventos externos.</p>
</li>
<li><p><strong>Eventhouse</strong>: base de datos KQL optimizada para almacenar eventos.</p>
</li>
<li><p><strong>Script Python</strong>: encargado de consultar la API de Binance y enviar eventos al Eventstream.</p>
</li>
</ul>
<hr />
<h1 id="heading-crear-los-componentes-en-microsoft-fabric"><strong>⚙️ Crear los componentes en Microsoft Fabric</strong></h1>
<h2 id="heading-1-crear-un-eventstream-y-configurarlo"><strong>1. Crear un Eventstream y configurarlo</strong></h2>
<ol>
<li><p>Ve a tu workspace en Microsoft Fabric.</p>
</li>
<li><p>Haz clic en <strong>Nuevo &gt; Eventstream</strong>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760377611746/25b6c79d-a682-496d-a9e4-ea7f2588ce74.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Asigna un nombre, por ejemplo: <code>es_Crypto</code>.</p>
</li>
<li><p>En la pantalla principal, selecciona el origen de datos <strong>Custom endpoint</strong>. Este tipo de origen permite recibir datos desde fuentes personalizadas a través de protocolos compatibles como Azure Event Hub, Kafka o AMQP.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760387213553/290f8326-4b9c-4747-924f-073e9b3dba1a.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Asigna un nombre al origen (por ejemplo, <code>binance-input</code>) y añádelo.</p>
</li>
<li><p>Publica los cambios del eventstream.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760387245579/614c5255-9bab-4d11-9fcf-b1a26aa3cfb8.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<hr />
<h2 id="heading-2-crear-un-eventhouse"><strong>2. Crear un Eventhouse</strong></h2>
<ol>
<li><p>En el mismo workspace, selecciona <strong>Nuevo &gt; Eventhouse</strong>. Este será el destino donde se almacenarán los eventos recibidos desde el Eventstream.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760387635088/7d473a39-3fda-4b63-9db6-d25ef796d937.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Dale un nombre, como <code>eh_Crypto</code>.</p>
</li>
<li><p>No es necesario crear manualmente una tabla aún, ya que se generará automáticamente al conectar el Eventstream en el siguiente paso.</p>
</li>
</ol>
<hr />
<h1 id="heading-script-python-para-obtener-y-enviar-datos"><strong>🐍 Script Python para obtener y enviar datos</strong></h1>
<p>Este script permite simular eventos financieros en tiempo real conectándose a la API pública de Binance y enviando datos a un Azure Event Hub.</p>
<p>Cada segundo realiza las siguientes acciones:</p>
<ul>
<li><p>🔄 Consulta los datos de precios (<code>ticker/price</code>) y la hora del servidor desde Binance.</p>
</li>
<li><p>🧱 Reestructura los datos en formato JSON incluyendo <code>symbol</code>, <code>price</code> y <code>serverTime</code>.</p>
</li>
<li><p>📤 Agrupa los eventos en lotes y los envía al <strong>Azure Event Hub</strong> especificado.</p>
</li>
<li><p>🛡️ Maneja errores de red, datos incompletos o problemas al construir lotes.</p>
</li>
<li><p>🔁 Ejecuta estas acciones de manera continua dentro de un bucle asincrónico (<code>asyncio</code>).</p>
</li>
</ul>
<blockquote>
<p>💡 Ideal para pruebas o demostraciones de ingesta de datos en tiempo real desde una fuente externa.</p>
</blockquote>
<p>🔒 <strong>Asegúrate de actualizar las variables</strong> <code>EVENT_HUB_NAME</code> <strong>y</strong> <code>EVENT_HUB_CONNECTION_STR</code> <strong>con los valores proporcionados por Fabric al crear el custom endpoint.</strong></p>
<p>Al configurar el origen del eventstream, Microsoft Fabric genera automáticamente:</p>
<ul>
<li><p>El <strong>nombre del Event Hub</strong></p>
</li>
<li><p>El <strong>Connection string (primary key)</strong> con autenticación SAS Key</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760638320533/22493db7-adc1-4279-bce2-9dbb7a86213e.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-prerequisitos">📦 Prerequisitos</h2>
<pre><code class="lang-bash">pip install azure-eventhub
</code></pre>
<p>El script lo tienes disponible aquí:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> urllib.request
<span class="hljs-keyword">from</span> urllib.error <span class="hljs-keyword">import</span> URLError, HTTPError
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">import</span> asyncio
<span class="hljs-keyword">from</span> azure.eventhub <span class="hljs-keyword">import</span> EventData
<span class="hljs-keyword">from</span> azure.eventhub.aio <span class="hljs-keyword">import</span> EventHubProducerClient
<span class="hljs-keyword">import</span> time
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime

<span class="hljs-comment"># --- Configuración ---</span>
<span class="hljs-comment"># URLs de la API de Binance</span>
ticker_price_url = <span class="hljs-string">"https://api.binance.com/api/v3/ticker/price"</span>
server_time_url = <span class="hljs-string">"https://api.binance.com/api/v3/time"</span>

<span class="hljs-comment"># --- Configuración de Azure Event Hubs ---</span>
<span class="hljs-comment"># **IMPORTANTE:** Reemplaza estos valores con tu cadena de conexión y nombre de Event Hub.</span>
EVENT_HUB_NAME = <span class="hljs-string">"es_XXX"</span>
EVENT_HUB_CONNECTION_STR = <span class="hljs-string">"Endpoint=sb://XXX.servicebus.windows.net/;SharedAccessKeyName=key_XXX;SharedAccessKey=XXX;EntityPath=es_XXX"</span>

<span class="hljs-comment"># --- Configuración del Intervalo ---</span>
<span class="hljs-comment"># Define cada cuántos segundos quieres ejecutar el proceso</span>
SEND_INTERVAL_SECONDS = <span class="hljs-number">1</span>

<span class="hljs-comment"># --- Función para obtener datos de la API ---</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_api_data</span>(<span class="hljs-params">url</span>):</span>
    <span class="hljs-string">"""Obtiene datos de una URL y los decodifica como JSON."""</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># print(f"Fetching data from: {url}") # Descomentar si necesitas logs detallados</span>
        <span class="hljs-keyword">with</span> urllib.request.urlopen(url, timeout=<span class="hljs-number">10</span>) <span class="hljs-keyword">as</span> response:
            <span class="hljs-keyword">if</span> response.status == <span class="hljs-number">200</span>:
                <span class="hljs-keyword">return</span> json.loads(response.read().decode(<span class="hljs-string">'utf-8'</span>))
            <span class="hljs-keyword">else</span>:
                print(<span class="hljs-string">f"Error: Received status code <span class="hljs-subst">{response.status}</span> from <span class="hljs-subst">{url}</span>"</span>)
                <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>
    <span class="hljs-keyword">except</span> (HTTPError, URLError) <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"Error fetching data from <span class="hljs-subst">{url}</span>: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"An unexpected error occurred while fetching data from <span class="hljs-subst">{url}</span>: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>


<span class="hljs-comment"># --- Función principal asíncrona para procesar y enviar datos (sin cambios) ---</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process_and_send_data</span>(<span class="hljs-params">producer</span>):</span>
    <span class="hljs-comment"># 1. Obtener la hora del servidor y los precios</span>
    server_time_data = fetch_api_data(server_time_url)
    ticker_price_data = fetch_api_data(ticker_price_url)

    <span class="hljs-comment"># Validar que se obtuvieron los datos</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> server_time_data <span class="hljs-keyword">or</span> <span class="hljs-string">'serverTime'</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> server_time_data:
        print(<span class="hljs-string">"Error: No se pudo obtener la hora del servidor de Binance."</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> ticker_price_data <span class="hljs-keyword">or</span> <span class="hljs-keyword">not</span> isinstance(ticker_price_data, list):
        print(<span class="hljs-string">"Error: No se pudieron obtener los datos de precios o el formato es incorrecto."</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>

    server_time = server_time_data[<span class="hljs-string">'serverTime'</span>]
    <span class="hljs-comment"># print(f"Server time obtained: {server_time}") # Log menos verboso</span>
    <span class="hljs-comment"># print(f"Number of tickers received: {len(ticker_price_data)}")</span>

    <span class="hljs-comment"># 2. Reestructurar los datos</span>
    events_to_send = []
    <span class="hljs-keyword">for</span> ticker <span class="hljs-keyword">in</span> ticker_price_data:
        <span class="hljs-keyword">if</span> <span class="hljs-string">'symbol'</span> <span class="hljs-keyword">in</span> ticker <span class="hljs-keyword">and</span> <span class="hljs-string">'price'</span> <span class="hljs-keyword">in</span> ticker:
            restructured_event = {
                <span class="hljs-string">"serverTime"</span>: server_time,
                <span class="hljs-string">"tickerInfo"</span>: {
                    <span class="hljs-string">"symbol"</span>: ticker[<span class="hljs-string">'symbol'</span>],
                    <span class="hljs-string">"price"</span>: ticker[<span class="hljs-string">'price'</span>]
                }
            }
            events_to_send.append(restructured_event)
        <span class="hljs-comment"># else: # No loguear cada ticker inválido para no llenar la consola</span>
            <span class="hljs-comment"># print(f"Skipping invalid ticker data: {ticker}")</span>

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> events_to_send:
        print(<span class="hljs-string">"No valid events were restructured to be sent in this cycle."</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span> <span class="hljs-comment"># Considerar esto un éxito parcial o fallo según el caso; aquí lo marcamos como no exitoso</span>

    <span class="hljs-comment"># print(f"Successfully restructured {len(events_to_send)} events.")</span>

    <span class="hljs-comment"># 3. Enviar datos a Event Hub usando el productor existente</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># El productor se crea y gestiona en main_loop</span>
        event_data_batch = <span class="hljs-keyword">await</span> producer.create_batch()
        event_count_in_batch = <span class="hljs-number">0</span>

        <span class="hljs-keyword">for</span> event_payload <span class="hljs-keyword">in</span> events_to_send:
            event_body_str = json.dumps(event_payload)
            event_data = EventData(event_body_str)
            <span class="hljs-keyword">try</span>:
                event_data_batch.add(event_data)
                event_count_in_batch += <span class="hljs-number">1</span>
            <span class="hljs-keyword">except</span> ValueError: <span class="hljs-comment"># Batch full</span>
                <span class="hljs-keyword">if</span> event_count_in_batch &gt; <span class="hljs-number">0</span>:
                    <span class="hljs-comment"># print(f"Batch full ({event_count_in_batch} events). Sending batch...")</span>
                    <span class="hljs-keyword">await</span> producer.send_batch(event_data_batch)
                    <span class="hljs-comment"># print("Batch sent.")</span>
                <span class="hljs-keyword">else</span>:
                     print(<span class="hljs-string">f"Warning: Single event is too large to fit in a batch: <span class="hljs-subst">{len(event_body_str)}</span> bytes."</span>)
                     <span class="hljs-comment"># Decide si quieres saltar este evento o manejarlo de otra forma</span>
                     <span class="hljs-keyword">continue</span> <span class="hljs-comment"># Saltar este evento y continuar con el siguiente</span>

                <span class="hljs-comment"># Crear nuevo lote y añadir el evento actual</span>
                event_data_batch = <span class="hljs-keyword">await</span> producer.create_batch()
                event_data_batch.add(event_data)
                event_count_in_batch = <span class="hljs-number">1</span>

        <span class="hljs-comment"># Enviar el último lote si contiene eventos</span>
        <span class="hljs-keyword">if</span> event_count_in_batch &gt; <span class="hljs-number">0</span>:
            <span class="hljs-comment"># print(f"Sending final batch ({event_count_in_batch} events)...")</span>
            <span class="hljs-keyword">await</span> producer.send_batch(event_data_batch)
            <span class="hljs-comment"># print("Final batch sent.")</span>

        print(<span class="hljs-string">f"Successfully sent <span class="hljs-subst">{len(events_to_send)}</span> events to Event Hub '<span class="hljs-subst">{EVENT_HUB_NAME}</span>'."</span>)
        <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>

    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(<span class="hljs-string">f"An error occurred during Event Hub send: <span class="hljs-subst">{e}</span>"</span>)
        <span class="hljs-comment"># Aquí podrías añadir lógica para reintentar o manejar errores específicos de Event Hubs</span>
        <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>


<span class="hljs-comment"># --- Bucle principal asíncrono ---</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main_loop</span>():</span>
    <span class="hljs-string">"""Bucle principal que ejecuta el proceso cada X segundos."""</span>

    <span class="hljs-comment"># Crear el productor una vez fuera del bucle para reutilizar la conexión</span>
    producer = EventHubProducerClient.from_connection_string(
        conn_str=EVENT_HUB_CONNECTION_STR,
        eventhub_name=EVENT_HUB_NAME
    )

    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        start_time = time.time()
        print(<span class="hljs-string">f"--- [<span class="hljs-subst">{datetime.now().strftime(<span class="hljs-string">'%Y-%m-%d %H:%M:%S'</span>)}</span>] Starting data fetch and send cycle ---"</span>)

        <span class="hljs-keyword">try</span>:
            <span class="hljs-comment"># Pasar el productor a la función</span>
            success = <span class="hljs-keyword">await</span> process_and_send_data(producer)
            <span class="hljs-keyword">if</span> success:
                print(<span class="hljs-string">f"--- Cycle finished successfully ---"</span>)
            <span class="hljs-keyword">else</span>:
                print(<span class="hljs-string">f"--- Cycle finished with errors (check logs above) ---"</span>)

        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            <span class="hljs-comment"># Captura errores inesperados en el ciclo principal</span>
            print(<span class="hljs-string">f"--- FATAL ERROR in cycle: <span class="hljs-subst">{e}</span> ---"</span>)
            <span class="hljs-comment"># Podrías decidir salir del bucle o reintentar crear el productor aquí</span>
            <span class="hljs-comment"># Por ahora, solo logueamos y continuamos esperando el intervalo</span>

        end_time = time.time()
        elapsed_time = end_time - start_time
        wait_time = max(<span class="hljs-number">0</span>, SEND_INTERVAL_SECONDS - elapsed_time) <span class="hljs-comment"># Calcular cuánto esperar realmente</span>

        print(<span class="hljs-string">f"--- Cycle duration: <span class="hljs-subst">{elapsed_time:<span class="hljs-number">.2</span>f}</span> seconds. Waiting for <span class="hljs-subst">{wait_time:<span class="hljs-number">.2</span>f}</span> seconds before next cycle ---"</span>)
        <span class="hljs-keyword">await</span> asyncio.sleep(wait_time) <span class="hljs-comment"># Esperar el tiempo restante del intervalo</span>

<span class="hljs-comment"># --- Punto de entrada ---</span>
<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    <span class="hljs-keyword">if</span> EVENT_HUB_CONNECTION_STR == <span class="hljs-string">"TU_CADENA_DE_CONEXION_EVENT_HUB"</span> <span class="hljs-keyword">or</span> EVENT_HUB_NAME == <span class="hljs-string">"TU_NOMBRE_DE_EVENT_HUB"</span>:
        print(<span class="hljs-string">"ERROR: Por favor, actualiza las variables EVENT_HUB_CONNECTION_STR y EVENT_HUB_NAME con tus valores reales."</span>)
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">try</span>:
            print(<span class="hljs-string">f"Starting continuous data sending every <span class="hljs-subst">{SEND_INTERVAL_SECONDS}</span> seconds..."</span>)
            print(<span class="hljs-string">f"Target Event Hub: <span class="hljs-subst">{EVENT_HUB_NAME}</span>"</span>)
            print(<span class="hljs-string">"Press Ctrl+C to stop."</span>)
            asyncio.run(main_loop())
        <span class="hljs-keyword">except</span> KeyboardInterrupt:
            print(<span class="hljs-string">"\nExecution stopped by user (Ctrl+C)."</span>)
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            print(<span class="hljs-string">f"\nAn unexpected error stopped the execution: <span class="hljs-subst">{e}</span>"</span>)
            exit(<span class="hljs-number">1</span>)
</code></pre>
<hr />
<h1 id="heading-ejecucion-del-script-y-conexion-con-el-eventstream">🚀 Ejecución del script y conexión con el Eventstream</h1>
<h2 id="heading-ejecucion-del-script-en-local">▶️ Ejecución del script en local</h2>
<p>Una vez configurado correctamente el script, se puede ejecutar desde consola:</p>
<pre><code class="lang-bash">python src/scripts/binance-api-ticker-price.py
</code></pre>
<p>El script comenzará a realizar ciclos cada segundo, extrayendo datos de la API pública de Binance y enviándolos al Azure Event Hub configurado. Se mostrará información por consola sobre los ciclos y el número de eventos enviados.</p>
<hr />
<h2 id="heading-comprobacion-de-recepcion-de-eventos-en-microsoft-fabric">✅ Comprobación de recepción de eventos en Microsoft Fabric</h2>
<p>Para comprobar que los datos están llegando correctamente:</p>
<ol>
<li><p>Accede a tu Eventstream en Microsoft Fabric.</p>
</li>
<li><p>En el menú superior, haz clic en "En directo / Live".</p>
</li>
<li><p>Deberías comenzar a ver eventos con la siguiente estructura:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760638767907/28f3d27d-2d7e-499e-91e9-77e2e571848e.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-conexion-del-eventstream-con-un-eventhouse-kql-database">🔗 Conexión del Eventstream con un Eventhouse (KQL database)</h2>
<p>Para persistir y consultar los datos, se puede conectar el Eventstream con un Eventhouse de la siguiente manera:</p>
<ol>
<li><p>En el Eventstream, haz clic en "Add destination". Lo puedes hacer desde el menú superior o desde la interfaz gráfica.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760638860000/21cedf17-2f53-47ed-a523-37dd2c91ba82.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Selecciona Eventhouse.</p>
</li>
<li><p>Configuramos el destino:</p>
<ul>
<li><p><strong>Data ingestion mode</strong>: Direct ingestion</p>
</li>
<li><p><strong>Destination name</strong>: Crypto-Eventhouse</p>
</li>
<li><p><strong>Seleccionamos el workspace y eventhouse creado previamente.</strong></p>
</li>
</ul>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760638926391/69b1106f-2b8d-483a-983b-29b1c841dad5.png" alt class="image--center mx-auto" /></p>
<p> Guarda la configuración.</p>
</li>
<li><p>Publica los cambios del eventstream.</p>
<p> Una vez se hayan publicado los cambios, aparecerá el destino Eventhouse en rojo indicando que no está configurado.</p>
</li>
<li><p>Clicamos en configurar.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760638986310/52c77d31-5a8a-43ae-b64a-33bb2e4ff331.png" alt class="image--center mx-auto" /></p>
<p> Creamos una nueva tabla con el nombre <code>Crypto_RAW</code></p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760639006085/c63e22de-2ea8-4ae4-a517-e36dfef6fc73.png" alt class="image--center mx-auto" /></p>
<p> En la siguiente ventana, veremos una previsualización de los datos y como serán almacenados en la tabla del Eventhouse.</p>
<p> Si cambiamos el valor de Nested levels a 2, podemos ver como los datos que vienen en formato JSON son extraídos correctamente, es decir, el precio de la criptomoneda por un lado y el identificador de la criptomoneda por otro. Además, el valor de serverTime también se ha convertido automaticamente a formato datetime.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760639065974/02850ae5-c843-455a-a554-f289085e57db.png" alt class="image--center mx-auto" /></p>
<p> Esto es interesante conocerlo porque supone una modificación de los datos en tiempo real conforme se van recibiendo para almacenarlos correctamente en el Eventhouse. En nuestro caso, queremos almacenar los datos en crudo para limpiarlos y modificarlos más adelante en la capa silver.</p>
</li>
<li><p>Volvemos a modificar el valor de <strong>Nested Levels a 1</strong></p>
</li>
<li><p>Clicamos en el icono del lápiz de la esquina derecha para modificar las columnas.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760639098262/262ca3a7-8ae7-413e-955c-d57c91c2e3ca.png" alt class="image--center mx-auto" /></p>
<p>Configuramos de la siguiente forma:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760639229047/6abd4b99-33c9-4c67-baac-3aad34457b1d.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Aplicamos los cambios.</p>
</li>
<li><p>Finalizamos la configuración.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760639259775/dc064c2d-8713-4bad-9e18-72041840638d.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<p>A partir de este momento, todos los eventos que lleguen al Eventstream se almacenarán en la tabla definida dentro del Eventhouse.</p>
<hr />
<h1 id="heading-visualizacion-de-los-datos-en-el-eventhouse">📊 Visualización de los datos en el Eventhouse</h1>
<p>Una vez conectado el <strong>Eventstream</strong> con un <strong>Eventhouse</strong> y creada la tabla, es fundamental validar que los datos se están recibiendo y almacenando correctamente. Esto puede hacerse fácilmente mediante una consulta KQL desde la base de datos.</p>
<p>Desde el área de trabajo de Microsoft Fabric:</p>
<ol>
<li><p>Accede a tu <strong>KQL Database (Eventhouse)</strong>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760639318338/075f19d3-d4f1-4267-a972-9d8473b86fdd.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Selecciona la tabla <code>Crypto_RAW</code>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760639348898/aa027c0c-1a6c-42fa-8594-e161ba345164.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Verificamos que los datos se están almacenando correctamente desde la previsualización.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760639336004/e7db7847-e0a6-4db5-8cce-efed3ed3c839.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Actualiza el environment de tus notebooks de manera programática]]></title><description><![CDATA[Cuando trabajamos con muchos notebooks en Microsoft Fabric, mantener actualizado el environment asociado puede ser tedioso si tenemos que hacerlo manualmente uno por uno.
Por suerte, gracias a la librería Semantic-Link-Labs, podemos automatizar esta ...]]></description><link>https://datagym.es/actualiza-el-environment-de-tus-notebooks-de-manera-programatica</link><guid isPermaLink="true">https://datagym.es/actualiza-el-environment-de-tus-notebooks-de-manera-programatica</guid><category><![CDATA[microsoft fabric]]></category><category><![CDATA[semantic link labs]]></category><category><![CDATA[semantic-link]]></category><category><![CDATA[PySpark]]></category><category><![CDATA[notebook]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Mon, 08 Sep 2025 17:13:01 GMT</pubDate><content:encoded><![CDATA[<p>Cuando trabajamos con muchos notebooks en Microsoft Fabric, mantener actualizado el <em>environment</em> asociado puede ser tedioso si tenemos que hacerlo manualmente uno por uno.</p>
<p>Por suerte, gracias a la librería <strong>Semantic-Link-Labs</strong>, podemos automatizar esta tarea y actualizar el <em>environment</em> de nuestros notebooks de manera sencilla y programática.</p>
<p>Semantic-Link-Labs nos ofrece varias funciones muy útiles para esta tarea:</p>
<ul>
<li><p><code>list_notebooks</code> → lista los notebooks dentro de un área de trabajo.</p>
</li>
<li><p><code>list_environments</code> → muestra los entornos definidos en el workspace (o en otro que especifiques).</p>
</li>
<li><p><code>get_notebook_definition</code> → obtiene la definición completa de un notebook.</p>
</li>
<li><p><code>update_notebook_definition</code> → actualiza un notebook con los cambios que necesitemos.</p>
</li>
</ul>
<p>Para la prueba, tengo en un área de trabajo un environment y un notebook asociado a este</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757332450590/1fe3b2c7-436a-472d-85ee-bdfd1ba8f278.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757332494910/54da192c-f91b-4bb4-9c15-851abd23c083.png" alt class="image--center mx-auto" /></p>
<p>Lo primero es instalar la librería en nuestro notebook:</p>
<pre><code class="lang-python">%pip install semantic-link-labs
</code></pre>
<h1 id="heading-obteniendo-la-definicion-del-notebook">Obteniendo la definición del notebook</h1>
<p>Utilizamos la función <code>get_notebook_definition</code> para ver el contenido del notebook:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> sempy_labs <span class="hljs-keyword">as</span> labs

labs.get_notebook_definition(notebook_name=<span class="hljs-string">"Notebook 1"</span>, format=<span class="hljs-string">"ipynb"</span>)
</code></pre>
<p>La salida será un JSON parecido a este:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"cells"</span>: [
        {
            <span class="hljs-attr">"cell_type"</span>: <span class="hljs-string">"code"</span>,
            <span class="hljs-attr">"source"</span>: [
                <span class="hljs-string">"# Welcome to your new notebook\\n"</span>,
                <span class="hljs-string">"# Type here in the cell editor to add code!\\n"</span>
            ],
            <span class="hljs-attr">"outputs"</span>: [],
            <span class="hljs-attr">"execution_count"</span>: <span class="hljs-literal">null</span>,
            <span class="hljs-attr">"metadata"</span>: {
                <span class="hljs-attr">"microsoft"</span>: {
                    <span class="hljs-attr">"language"</span>: <span class="hljs-string">"python"</span>,
                    <span class="hljs-attr">"language_group"</span>: <span class="hljs-string">"synapse_pyspark"</span>
                }
            },
            <span class="hljs-attr">"id"</span>: <span class="hljs-string">"9e2b1999-7e8f-4163-a6cd-33a4fe23ffff"</span>
        }
    ],
    <span class="hljs-attr">"metadata"</span>: {
        <span class="hljs-attr">"kernel_info"</span>: {
            <span class="hljs-attr">"name"</span>: <span class="hljs-string">"synapse_pyspark"</span>
        },
        <span class="hljs-attr">"kernelspec"</span>: {
            <span class="hljs-attr">"name"</span>: <span class="hljs-string">"synapse_pyspark"</span>,
            <span class="hljs-attr">"display_name"</span>: <span class="hljs-string">"synapse_pyspark"</span>
        },
        <span class="hljs-attr">"language_info"</span>: {
            <span class="hljs-attr">"name"</span>: <span class="hljs-string">"python"</span>
        },
        <span class="hljs-attr">"microsoft"</span>: {
            <span class="hljs-attr">"language"</span>: <span class="hljs-string">"python"</span>,
            <span class="hljs-attr">"language_group"</span>: <span class="hljs-string">"synapse_pyspark"</span>,
            <span class="hljs-attr">"ms_spell_check"</span>: {
                <span class="hljs-attr">"ms_spell_check_language"</span>: <span class="hljs-string">"en"</span>
            }
        },
        <span class="hljs-attr">"nteract"</span>: {
            <span class="hljs-attr">"version"</span>: <span class="hljs-string">"nteract-front-end@1.0.0"</span>
        },
        <span class="hljs-attr">"spark_compute"</span>: {
            <span class="hljs-attr">"compute_id"</span>: <span class="hljs-string">"/trident/default"</span>,
            <span class="hljs-attr">"session_options"</span>: {
                <span class="hljs-attr">"conf"</span>: {
                    <span class="hljs-attr">"spark.synapse.nbs.session.timeout"</span>: <span class="hljs-string">"1200000"</span>
                }
            }
        },
        <span class="hljs-attr">"dependencies"</span>: {
            <span class="hljs-attr">"environment"</span>: {
                <span class="hljs-attr">"environmentId"</span>: <span class="hljs-string">"49277563-ad74-49a1-b791-247964afa14a"</span>,
                <span class="hljs-attr">"workspaceId"</span>: <span class="hljs-string">"69445ea5-e0e6-456d-810b-a291e9b8cae9"</span>
            }
        }
    },
    <span class="hljs-attr">"nbformat"</span>: <span class="hljs-number">4</span>,
    <span class="hljs-attr">"nbformat_minor"</span>: <span class="hljs-number">5</span>
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757343295391/e5dda192-255d-431c-9a90-341ae3482f6b.png" alt class="image--center mx-auto" /></p>
<p>La parte que nos interesa está dentro de <code>dependencies</code>, donde se encuentra el <strong><em>environment</em></strong> asignado al notebook.<br />Si tuvieras un Lakehouse vinculado, también aparecería aquí.</p>
<h1 id="heading-eliminando-el-environment">Eliminando el environment</h1>
<p>Si lo que queremos es cambiar el environment al predeterminado, basta con eliminar la referencia actual:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> sempy_labs <span class="hljs-keyword">as</span> labs
<span class="hljs-keyword">import</span> json

notebook_name = <span class="hljs-string">"Notebook 1"</span>

definition = labs.get_notebook_definition(notebook_name=notebook_name, format=<span class="hljs-string">"ipynb"</span>)
definition = json.loads(definition)

<span class="hljs-keyword">if</span> (
    <span class="hljs-string">"metadata"</span> <span class="hljs-keyword">in</span> definition <span class="hljs-keyword">and</span>
    <span class="hljs-string">"dependencies"</span> <span class="hljs-keyword">in</span> definition[<span class="hljs-string">"metadata"</span>] <span class="hljs-keyword">and</span>
    <span class="hljs-string">"environment"</span> <span class="hljs-keyword">in</span> definition[<span class="hljs-string">"metadata"</span>][<span class="hljs-string">"dependencies"</span>]
):
    print(<span class="hljs-string">f"Actualizando notebook: <span class="hljs-subst">{notebook_name}</span>"</span>)
    definition[<span class="hljs-string">"metadata"</span>][<span class="hljs-string">"dependencies"</span>][<span class="hljs-string">"environment"</span>] = {}

    updated_definition_str = json.dumps(definition)

    labs.update_notebook_definition(name =notebook_name, notebook_content =updated_definition_str, format=<span class="hljs-string">"ipynb"</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757347521806/1d7e1fb3-5435-4fe6-93a5-3a431b0c162f.png" alt class="image--center mx-auto" /></p>
<p>Al abrir el notebook de nuevo, verás que aparece el environment por defecto.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757347565990/e9cfb4df-1dbe-4a09-8682-11eb5b6f7cb1.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-asignando-un-nuevo-environment">Asignando un nuevo environment</h1>
<p>También podemos cambiar el notebook para que apunte a otro environment (incluso en otra área de trabajo):</p>
<pre><code class="lang-python">notebook_name = <span class="hljs-string">"Notebook 1"</span>
environment_id = <span class="hljs-string">"38d5bb5e-897e-44d7-927c-e902590da88c"</span>
workspace_id = <span class="hljs-string">"e42acae6-20f9-4929-a0b8-345b096b0217"</span>

definition = labs.get_notebook_definition(notebook_name=notebook_name, format=<span class="hljs-string">"ipynb"</span>)
definition = json.loads(definition)

<span class="hljs-keyword">if</span> (
    <span class="hljs-string">"metadata"</span> <span class="hljs-keyword">in</span> definition <span class="hljs-keyword">and</span>
    <span class="hljs-string">"dependencies"</span> <span class="hljs-keyword">in</span> definition[<span class="hljs-string">"metadata"</span>] <span class="hljs-keyword">and</span>
    <span class="hljs-string">"environment"</span> <span class="hljs-keyword">in</span> definition[<span class="hljs-string">"metadata"</span>][<span class="hljs-string">"dependencies"</span>]
):
    print(<span class="hljs-string">f"Actualizando notebook: <span class="hljs-subst">{notebook_name}</span>"</span>)

    definition[<span class="hljs-string">"metadata"</span>][<span class="hljs-string">"dependencies"</span>][<span class="hljs-string">"environment"</span>] = {<span class="hljs-string">"environmentId"</span>: environment_id, <span class="hljs-string">"workspaceId"</span>: workspace_id}
    updated_definition_str = json.dumps(definition)

    labs.update_notebook_definition(name =notebook_name, notebook_content =updated_definition_str, format=<span class="hljs-string">"ipynb"</span>)
</code></pre>
<p>Al reabrir el notebook, verás el nuevo <em>environment</em> asignado.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757348748830/89905f9e-69ce-4db1-9259-846a4c7a63fd.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-como-puedo-obtener-el-environmentid">¿Cómo puedo obtener el environmentId?</h2>
<p>Con la función <code>list_environments</code> podemos listar fácilmente todos los entornos disponibles:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757348858338/42355654-16a7-4904-b7cb-b86559048ed2.png" alt class="image--center mx-auto" /></p>
<p>Si no pasamos ningún parámetro, se buscarán los environments en el área de trabajo actual.<br />También podemos especificar otro workspace si queremos reutilizar environments definidos en otro lugar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757348947499/deaee0c9-833d-4e72-8896-07c2816d233d.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-automatizacion-para-todos-los-notebooks-de-un-area-de-trabajo">Automatización para todos los notebooks de un área de trabajo</h1>
<p>Si trabajamos en proyectos grandes con decenas o cientos de notebooks, lo ideal es automatizar el proceso. Con este ejemplo, actualizamos en bloque todos los notebooks de un área de trabajo:</p>
<pre><code class="lang-python">notebooks_df = labs.list_notebooks()

<span class="hljs-keyword">for</span> _, row <span class="hljs-keyword">in</span> notebooks_df.iterrows():
    notebook_id = row[<span class="hljs-string">"Notebook Id"</span>]
    notebook_name = row[<span class="hljs-string">"Notebook Name"</span>]
    print(<span class="hljs-string">f"<span class="hljs-subst">{notebook_id}</span> - <span class="hljs-subst">{notebook_name}</span>"</span>)

    definition = labs.get_notebook_definition(notebook_name=notebook_name, format=<span class="hljs-string">"ipynb"</span>)
    definition = json.loads(definition)

    <span class="hljs-keyword">if</span> (
        <span class="hljs-string">"metadata"</span> <span class="hljs-keyword">in</span> definition <span class="hljs-keyword">and</span>
        <span class="hljs-string">"dependencies"</span> <span class="hljs-keyword">in</span> definition[<span class="hljs-string">"metadata"</span>] <span class="hljs-keyword">and</span>
        <span class="hljs-string">"environment"</span> <span class="hljs-keyword">in</span> definition[<span class="hljs-string">"metadata"</span>][<span class="hljs-string">"dependencies"</span>]
    ):
        print(<span class="hljs-string">f"Actualizando notebook: <span class="hljs-subst">{notebook_name}</span>"</span>)

        definition[<span class="hljs-string">"metadata"</span>][<span class="hljs-string">"dependencies"</span>][<span class="hljs-string">"environment"</span>] = {}
        updated_definition_str = json.dumps(definition)

        labs.update_notebook_definition(
            name =notebook_name,
            notebook_content =updated_definition_str,
            format=<span class="hljs-string">"ipynb"</span>
        )
    <span class="hljs-keyword">else</span>:
        print(<span class="hljs-string">"Este notebook no tiene environment definido."</span>)
</code></pre>
]]></content:encoded></item><item><title><![CDATA[¿Cuántos datos están siendo almacenados en mi OneLake?]]></title><description><![CDATA[Si estás metido en el mundo de Microsoft Fabric, seguro que ya conoces OneLake. Piensa en él como el "OneDrive de tus datos": un sitio único y para todos donde guardar la información de la empresa. Suena genial, ¿verdad? Centralizarlo todo facilita u...]]></description><link>https://datagym.es/cuantos-datos-estan-siendo-almacenados-en-mi-onelake</link><guid isPermaLink="true">https://datagym.es/cuantos-datos-estan-siendo-almacenados-en-mi-onelake</guid><category><![CDATA[PySpark]]></category><category><![CDATA[onelake]]></category><category><![CDATA[semantic-link]]></category><category><![CDATA[microsoftfabric]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Mon, 12 May 2025 16:47:57 GMT</pubDate><content:encoded><![CDATA[<p>Si estás metido en el mundo de Microsoft Fabric, seguro que ya conoces OneLake. Piensa en él como el "OneDrive de tus datos": un sitio único y para todos donde guardar la información de la empresa. Suena genial, ¿verdad? Centralizarlo todo facilita un montón la vida para organizar, compartir y no tener datos repetidos por todas partes. Pero claro, a medida que metes más y más proyectos y datos, te empiezas a preguntar: ¿Cuánto espacio estoy usando realmente y qué es lo que más pesa?</p>
<p>Y ojo, que responder a eso tiene su miga. Con un montón de workspaces, cada uno con sus cosas (Lakehouses, Warehouses, Modelos Semánticos, Bases de Datos KQL...), saber exactamente qué ocupa cada cosa es un pequeño lío.</p>
<p>Para abordar este desafío, he desarrollado un notebook de PySpark dentro de Microsoft Fabric que nos permite automatizar este proceso, ofreciendo una visión detallada del uso del almacenamiento en OneLake.</p>
<h1 id="heading-el-desafio-ver-claro-entre-tanta-cosa">El desafío: Ver claro entre tanta cosa</h1>
<p>OneLake es la casa de muchos tipos de artefactos de Fabric. Algunos, como las Lakehouses, son como carpetas y archivos que puedes ver y tocar fácilmente. Pero otros, como los Modelos Semánticos o los Warehouses, son un poco más "caja negra"; su tamaño no es solo sumar archivos, sino que Fabric los maneja a su manera.</p>
<p>Ir mirando esto a mano, workspace por workspace, es una locura, y más si tu empresa tiene unos cuantos.</p>
<h1 id="heading-la-solucion-un-notebook-de-pyspark-al-rescate">La solución: Un notebook de PySpark al rescate</h1>
<p>El notebook aprovecha el poder de PySpark en el entorno de Microsoft Fabric y las capacidades de la librería <code>sempy</code> para interactuar programáticamente con los workspaces y artefactos. El proceso general que sigue el notebook es el siguiente:</p>
<ul>
<li><p><strong>Listar Workspaces:</strong> Utiliza <code>sempy.fabric.list_workspaces()</code> para obtener una lista de todos los workspaces a los que el usuario tiene acceso.</p>
</li>
<li><p><strong>Listar artefactos por workspace:</strong> Para cada workspace, emplea <code>sempy.fabric.list_items()</code> para inventariar todos los artefactos.</p>
</li>
<li><p><strong>Determinar el tipo de artefacto:</strong> Identifica el tipo de cada artefacto (<code>Lakehouse</code>, <code>Warehouse</code>, <code>SemanticModel</code>, <code>KQLDatabase</code>, etc.).</p>
</li>
<li><p><strong>Calcular el tamaño:</strong></p>
<ul>
<li><p><strong>Modelos Semánticos:</strong> Intenta obtener el tamaño utilizando la función <code>sempy_labs.get_semantic_model_size()</code>. Es importante destacar que el usuario debe disponer de permisos para ejecutar esta función. Si esta función falla o no devuelve un tamaño, se registra un marcador.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746897632586/b75ec149-12b3-453c-aa23-b068956cf189.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Artefactos basados en archivos (Lakehouse, Warehouse, etc.):</strong> Para artefactos cuyo almacenamiento es directamente accesible como archivos y carpetas en OneLake (a través de rutas ABFSS), el notebook implementa una función recursiva (<code>get_file_details_recursive</code>). Esta función utiliza <a target="_blank" href="http://mssparkutils.fs.ls"><code>notebookutils.fs.ls</code></a><code>()</code> para navegar por la estructura de directorios del artefacto y sumar el tamaño de cada archivo individual.</p>
</li>
<li><p><strong>Otros tipos de artefactos:</strong> Para tipos de artefactos donde el tamaño no se obtiene directamente de los archivos ABFSS (por ejemplo, <code>Report</code>, <code>Notebook</code>, <code>DataPipeline</code>), el script actualmente los omite o permite añadir un marcador de tamaño cero, ya que su "tamaño" en OneLake suele ser despreciable en comparación con los artefactos de datos.</p>
</li>
</ul>
</li>
<li><p><strong>Almacenar en una tabla delta:</strong> Finalmente, el DataFrame se guarda en una tabla Delta dentro de un Lakehouse.</p>
</li>
</ul>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> sempy_labs <span class="hljs-keyword">as</span> labs
<span class="hljs-keyword">import</span> sempy.fabric <span class="hljs-keyword">as</span> fabric
<span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd
<span class="hljs-keyword">from</span> pyspark.sql.types <span class="hljs-keyword">import</span> *
<span class="hljs-keyword">from</span> pyspark.sql.functions <span class="hljs-keyword">import</span> col

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_file_details_recursive</span>(<span class="hljs-params">folder_path</span>):</span>
    <span class="hljs-string">"""
    Escanea recursivamente una carpeta en OneLake y devuelve una lista con detalles
    (ruta completa ABFSS, tamaño en bytes) de cada archivo encontrado.

    Utiliza mssparkutils.fs.ls para listar el contenido.

    Args:
        folder_path (str): La ruta ABFSS de la carpeta a escanear.
                           Ej: "abfss://&lt;workspace_id&gt;@onelake.dfs.fabric.microsoft.com/&lt;item_id&gt;/Files/"

    Returns:
        list: Una lista de diccionarios [{'path': str, 'size_bytes': int}],
              o una lista vacía si ocurre un error al listar la carpeta
              o si la carpeta está vacía o no contiene archivos directamente.
              Los errores en subcarpetas se registran pero no detienen el escaneo general.
    """</span>

    file_details_list = []
    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># print(f"DE<span class="hljs-doctag">BUG:</span> Escaneando carpeta: {folder_path}") # Descomentar para depuración detallada</span>
        items = notebookutils.fs.ls(folder_path)

        <span class="hljs-keyword">for</span> item <span class="hljs-keyword">in</span> items:
            <span class="hljs-comment"># Asegurarse de que la ruta del item es completa (ABFSS)</span>
            item_path_full = item.path
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> item_path_full.startswith(<span class="hljs-string">"abfss://"</span>):
                 <span class="hljs-comment"># Si la ruta no es completa, intentar reconstruirla (puede no ser siempre necesario/correcto)</span>
                 <span class="hljs-comment"># Esto es una suposición basada en cómo a veces se devuelven las rutas</span>
                 <span class="hljs-keyword">if</span> folder_path.endswith(<span class="hljs-string">'/'</span>):
                     item_path_full = folder_path + item.name
                 <span class="hljs-keyword">else</span>:
                     item_path_full = folder_path + <span class="hljs-string">'/'</span> + item.name

            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> item.isDir:
                <span class="hljs-comment"># Es un archivo, añadir sus detalles</span>
                file_details_list.append({<span class="hljs-string">'path'</span>: item_path_full, <span class="hljs-string">'size_bytes'</span>: item.size})
            <span class="hljs-keyword">else</span>:
                <span class="hljs-comment"># Es un directorio, llamar recursivamente si no es la misma carpeta (evitar bucles)</span>
                <span class="hljs-comment"># Comprobamos la ruta completa normalizada para evitar errores por barras finales</span>
                current_folder_normalized = folder_path.rstrip(<span class="hljs-string">'/'</span>)
                item_folder_normalized = item_path_full.rstrip(<span class="hljs-string">'/'</span>)

                <span class="hljs-keyword">if</span> item_folder_normalized != current_folder_normalized:
                    sub_dir_files = get_file_details_recursive(item_path_full)
                    <span class="hljs-keyword">if</span> sub_dir_files:
                        file_details_list.extend(sub_dir_files)

    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-comment"># Imprime el error específico de esta carpeta pero permite que el proceso general continúe.</span>
        <span class="hljs-comment"># No se añadirán archivos de esta ruta específica si falla el 'ls'.</span>
        <span class="hljs-comment">#print(f"WARN: Error al escanear la carpeta '{folder_path}': {e}. Omitiendo esta ruta.")</span>
        print(<span class="hljs-string">f"WARN: Error al escanear la carpeta '<span class="hljs-subst">{folder_path}</span>'. Omitiendo esta ruta."</span>)
        <span class="hljs-keyword">return</span> [] <span class="hljs-comment"># Devuelve lista vacía en caso de error en esta carpeta específica</span>

    <span class="hljs-keyword">return</span> file_details_list

<span class="hljs-comment"># Lista para almacenar los datos de todos los archivos encontrados</span>
all_files_data = []

<span class="hljs-comment"># Contador para mostrar progreso</span>
processed_workspaces = <span class="hljs-number">0</span>
total_workspaces = <span class="hljs-number">0</span>

print(<span class="hljs-string">"Iniciando escaneo de workspaces..."</span>)

<span class="hljs-keyword">try</span>:
    <span class="hljs-comment"># Obtener todos los workspaces accesibles</span>
    workspaces_pd = fabric.list_workspaces()
    total_workspaces = len(workspaces_pd)
    print(<span class="hljs-string">f"Se encontraron <span class="hljs-subst">{total_workspaces}</span> áreas de trabajo accesibles."</span>)

    <span class="hljs-comment"># Iterar sobre cada workspace encontrado</span>
    <span class="hljs-keyword">for</span> ws_index, ws_row <span class="hljs-keyword">in</span> workspaces_pd.iterrows():
        workspace_name = ws_row[<span class="hljs-string">'Name'</span>]
        workspace_id = ws_row[<span class="hljs-string">'Id'</span>]
        processed_workspaces += <span class="hljs-number">1</span>
        print(<span class="hljs-string">f"\n[<span class="hljs-subst">{processed_workspaces}</span>/<span class="hljs-subst">{total_workspaces}</span>] 📂 Procesando Workspace: '<span class="hljs-subst">{workspace_name}</span>' (ID: <span class="hljs-subst">{workspace_id}</span>)"</span>)

        <span class="hljs-keyword">try</span>:
            <span class="hljs-comment"># Listar todos los artefactos (items) dentro del workspace actual</span>
            items_pd = fabric.list_items(workspace=workspace_id)
            print(<span class="hljs-string">f"  -&gt; Se encontraron <span class="hljs-subst">{len(items_pd)}</span> artefactos escaneables en '<span class="hljs-subst">{workspace_name}</span>'."</span>)

            <span class="hljs-comment"># Iterar sobre cada artefacto del workspace</span>
            <span class="hljs-keyword">for</span> item_index, item_row <span class="hljs-keyword">in</span> items_pd.iterrows():
                artifact_type = item_row[<span class="hljs-string">'Type'</span>]
                artifact_name = item_row[<span class="hljs-string">'Display Name'</span>]
                artifact_id = item_row[<span class="hljs-string">'Id'</span>]

                <span class="hljs-comment"># Construir la ruta raíz ABFSS para el artefacto</span>
                <span class="hljs-comment"># Nota: La estructura interna puede variar. Generalmente '/Files' o '/Tables' son puntos de entrada comunes.</span>
                <span class="hljs-comment">#       Probamos escanear desde la raíz del artefacto.</span>
                artifact_root_path = <span class="hljs-string">f"abfss://<span class="hljs-subst">{workspace_id}</span>@onelake.dfs.fabric.microsoft.com/<span class="hljs-subst">{artifact_id}</span>"</span>

                print(<span class="hljs-string">f"    -&gt; 🔍 Escaneando artefacto: '<span class="hljs-subst">{artifact_name}</span>' (Tipo: <span class="hljs-subst">{artifact_type}</span>, ID: <span class="hljs-subst">{artifact_id}</span>)"</span>)

                <span class="hljs-comment"># Inicializar la lista de archivos para ESTE artefacto en CADA iteración</span>
                files_in_artifact = []

                <span class="hljs-comment"># Variable para almacenar el tamaño si se obtiene de forma especial (p.ej. Semantic Model)</span>
                special_size_bytes = <span class="hljs-number">-1</span> <span class="hljs-comment"># Usar -1 como indicador inicial (no determinado/error)</span>

                <span class="hljs-keyword">try</span>:
                    <span class="hljs-comment"># Obtener la lista de archivos y sus tamaños recursivamente</span>
                    <span class="hljs-keyword">if</span> artifact_type == <span class="hljs-string">'SemanticModel'</span>:
                        print(<span class="hljs-string">f"      -&gt; Intentando obtener tamaño para Semantic Model '<span class="hljs-subst">{artifact_name}</span>'..."</span>)
                        size = labs.get_semantic_model_size(artifact_id, workspace_id)
                        <span class="hljs-keyword">if</span> size <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
                            special_size_bytes = int(size)

                        print(<span class="hljs-string">f"      =&gt; Tamaño obtenido de sempy_labs: <span class="hljs-subst">{special_size_bytes}</span> bytes."</span>)

                        all_files_data.append({
                                <span class="hljs-string">"WorkspaceID"</span>: workspace_id, 
                                <span class="hljs-string">"WorkspaceName"</span>: workspace_name,
                                <span class="hljs-string">"ArtifactType"</span>: artifact_type, 
                                <span class="hljs-string">"ArtifactName"</span>: artifact_name, 
                                <span class="hljs-string">"ArtifactID"</span>: artifact_id,
                                <span class="hljs-string">"ScannedRootPath"</span>: artifact_root_path,
                                <span class="hljs-string">"FilePath"</span>: <span class="hljs-string">f"<span class="hljs-subst">{artifact_root_path}</span>"</span>,
                                <span class="hljs-string">"SizeBytes"</span>: special_size_bytes,
                                <span class="hljs-string">"SizeMB"</span>: float(special_size_bytes / (<span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>)) <span class="hljs-keyword">if</span> special_size_bytes &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-number">0.0</span>
                        })
                    <span class="hljs-keyword">else</span>:
                        files_in_artifact = get_file_details_recursive(artifact_root_path + <span class="hljs-string">"/"</span>) <span class="hljs-comment"># Añadir '/' por si acaso es necesario</span>

                    <span class="hljs-keyword">if</span> files_in_artifact <span class="hljs-keyword">and</span> artifact_type != <span class="hljs-string">'SemanticModel'</span>:
                        print(<span class="hljs-string">f"      =&gt; Se encontraron <span class="hljs-subst">{len(files_in_artifact)}</span> archivos para '<span class="hljs-subst">{artifact_name}</span>'."</span>)
                        <span class="hljs-comment"># Procesar y añadir la información de cada archivo a la lista global</span>
                        <span class="hljs-keyword">for</span> file_detail <span class="hljs-keyword">in</span> files_in_artifact:
                            file_size_bytes = int(file_detail[<span class="hljs-string">'size_bytes'</span>])
                            <span class="hljs-comment"># Evitar división por cero si el tamaño es 0</span>
                            file_size_mb = float(file_size_bytes / (<span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>)) <span class="hljs-keyword">if</span> file_size_bytes &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-number">0.0</span>

                            all_files_data.append({
                                <span class="hljs-string">"WorkspaceID"</span>: workspace_id,
                                <span class="hljs-string">"WorkspaceName"</span>: workspace_name,
                                <span class="hljs-string">"ArtifactType"</span>: artifact_type,
                                <span class="hljs-string">"ArtifactName"</span>: artifact_name,
                                <span class="hljs-string">"ArtifactID"</span>: artifact_id,
                                <span class="hljs-string">"ScannedRootPath"</span>: artifact_root_path, <span class="hljs-comment"># Ruta base desde donde se escaneó</span>
                                <span class="hljs-string">"FilePath"</span>: file_detail[<span class="hljs-string">'path'</span>],       <span class="hljs-comment"># Ruta completa del archivo</span>
                                <span class="hljs-string">"SizeBytes"</span>: file_size_bytes,
                                <span class="hljs-string">"SizeMB"</span>: file_size_mb
                            })
                    <span class="hljs-keyword">else</span>:
                        <span class="hljs-comment"># Si no se encontraron archivos (puede ser normal para ciertos tipos o si está vacío)</span>
                        print(<span class="hljs-string">f"      =&gt; No se encontraron archivos accesibles vía ABFSS para '<span class="hljs-subst">{artifact_name}</span>' o está vacío."</span>)

                <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> scan_error:
                    <span class="hljs-comment"># Captura errores durante el escaneo de un artefacto específico</span>
                    print(<span class="hljs-string">f"      =&gt; ERROR escaneando el artefacto '<span class="hljs-subst">{artifact_name}</span>' en '<span class="hljs-subst">{artifact_root_path}</span>'."</span>)
                    <span class="hljs-comment">#print(f"      =&gt; ERROR escaneando el artefacto '{artifact_name}' en '{artifact_root_path}'. Error: {scan_error}")</span>
                    all_files_data.append({
                                <span class="hljs-string">"WorkspaceID"</span>: workspace_id,
                                <span class="hljs-string">"WorkspaceName"</span>: workspace_name,
                                <span class="hljs-string">"ArtifactType"</span>: artifact_type,
                                <span class="hljs-string">"ArtifactName"</span>: artifact_name,
                                <span class="hljs-string">"ArtifactID"</span>: artifact_id,
                                <span class="hljs-string">"ScannedRootPath"</span>: artifact_root_path,
                                <span class="hljs-string">"FilePath"</span>: <span class="hljs-string">"ERROR_SCANNING_ARTIFACT"</span>,
                                <span class="hljs-string">"SizeBytes"</span>: <span class="hljs-number">-1</span>, <span class="hljs-comment"># Indicador de error</span>
                                <span class="hljs-string">"SizeMB"</span>: <span class="hljs-number">-1.0</span>
                            })

        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> item_error:
            <span class="hljs-comment"># Captura errores al listar los artefactos de un workspace</span>
            print(<span class="hljs-string">f"  -&gt; ERROR al listar artefactos para el workspace '<span class="hljs-subst">{workspace_name}</span>': <span class="hljs-subst">{item_error}</span>"</span>)
            <span class="hljs-keyword">continue</span> <span class="hljs-comment"># Continuar con el siguiente workspace</span>

<span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> ws_error:
    print(<span class="hljs-string">f"FATAL: Error crítico al obtener la lista de workspaces: <span class="hljs-subst">{ws_error}</span>"</span>)

print(<span class="hljs-string">f"\n📊 Escaneo completado. Se recopilaron <span class="hljs-subst">{len(all_files_data)}</span> registros de archivos."</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746898488201/b7f2d575-4014-4d01-aee4-0ee2e3ee4801.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746898516511/e777dfa6-1c4a-4965-8de3-a617d03b06b4.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746898555227/715956df-3d7e-4fd0-b6fc-ee79bbee0f7b.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746900047094/de535650-e2b5-49c1-a498-774afa8112c3.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746963926003/f62bf47d-ac86-4396-9840-df14d2f436e5.png" alt class="image--center mx-auto" /></p>
<p><strong>Notebook completo</strong>: <a target="_blank" href="https://github.com/kilianbs/blogs/blob/main/Microsoft%20Fabric/notebooks/Cuantos%20datos%20est%C3%A1n%20siendo%20almacenados%20en%20OneLake.ipynb">GitHub</a></p>
<h1 id="heading-beneficios">Beneficios</h1>
<p>Esta tabla es un recurso valioso para:</p>
<ul>
<li><p><strong>Gobernanza del almacenamiento:</strong> Identificar qué workspaces, artefactos o incluso qué rutas específicas dentro de un Lakehouse están consumiendo más espacio.</p>
</li>
<li><p><strong>Mantenimiento y limpieza:</strong> Detectar datos obsoletos, innecesariamente grandes o duplicados que podrían ser archivados o eliminados.</p>
</li>
<li><p><strong>Informes y dashboards:</strong> Conectar Power BI directamente a esta tabla Delta para crear visualizaciones interactivas del uso del almacenamiento.</p>
</li>
<li><p><strong>Automatización:</strong> Al ser un notebook, puede programarse su ejecución periódica para tener un seguimiento continuo del crecimiento del almacenamiento.</p>
</li>
</ul>
<h1 id="heading-consideraciones-y-limitaciones">Consideraciones y Limitaciones</h1>
<p>Es importante tener en cuenta algunos aspectos:</p>
<ul>
<li><p>La función <code>get_semantic_model_size</code> puede no devolver el tamaño si el usuario no tiene los permisos suficientes.</p>
</li>
<li><p><strong>Tamaño lógico vs. físico:</strong> Para artefactos como Modelos Semánticos en modo Direct Lake o Warehouses, la suma de los tamaños de los archivos Delta subyacentes accesibles vía ABFSS puede no coincidir exactamente con el tamaño lógico que Fabric gestiona internamente o reporta en otras interfaces. El notebook proporciona el tamaño de lo "visible" y accesible desde Spark a través del sistema de archivos.</p>
</li>
<li><p><strong>Permisos:</strong> La identidad que ejecuta el notebook (el usuario o un Service Principal) necesita los permisos adecuados para listar workspaces, ítems dentro de esos workspaces y, crucialmente, acceder a las rutas ABFSS de los artefactos.</p>
</li>
<li><p><strong>Rendimiento:</strong> En entornos con una cantidad masiva de workspaces, artefactos y archivos, la ejecución del notebook podría llevar un tiempo considerable. Se podrían explorar optimizaciones adicionales si esto se convierte en un problema.</p>
</li>
<li><p><strong>Tipos de Artefactos:</strong> El script se enfoca en artefactos que almacenan volúmenes significativos de datos. Otros tipos, como informes o flujos de datos, generalmente no se escanean por su tamaño de archivo individual.</p>
</li>
</ul>
<h1 id="heading-referencias">Referencias</h1>
<p><a target="_blank" href="https://learn.microsoft.com/en-us/fabric/data-engineering/notebook-utilities#file-system-utilities">NotebookUtils (former MSSparkUtils) for Fabric - Microsoft Fabric | Microsoft Learn</a></p>
<p><a target="_blank" href="https://www.fourmoo.com/2024/04/24/how-much-data-is-being-stored-in-my-fabric-onelake-lakehouse-files-and-tables/">How much data is being stored in my Fabric OneLake (Lakehouse files and tables) - FourMoo | Microsoft Fabric | Power BI</a></p>
<p><a target="_blank" href="https://fabric.guru/calculating-folder-size-in-the-lakehouse">Lakehouse Folder Size Calculation</a></p>
]]></content:encoded></item><item><title><![CDATA[Nuevo conector de datos meteorológicos en Tiempo Real]]></title><description><![CDATA[En la última publicación de las novedades de Microsoft Fabric, en el blog de Marzo, en la parte de Real-Time Intelligence anunciaron nuevos conectores de origen para el eventstream.

Entre estos nuevos 5 orígenes encontramos el conector de Real-Time ...]]></description><link>https://datagym.es/nuevo-conector-de-datos-meteorologicos-en-tiempo-real</link><guid isPermaLink="true">https://datagym.es/nuevo-conector-de-datos-meteorologicos-en-tiempo-real</guid><category><![CDATA[realtime]]></category><category><![CDATA[Real-time-intelligence]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[KQL]]></category><category><![CDATA[eventhouse]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Sun, 27 Apr 2025 16:15:35 GMT</pubDate><content:encoded><![CDATA[<p>En la última publicación de las novedades de Microsoft Fabric, en el blog de Marzo, en la parte de Real-Time Intelligence anunciaron nuevos conectores de origen para el eventstream.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745267501224/9de2c12e-a334-4903-bdab-ea19260cf8d1.png" alt class="image--center mx-auto" /></p>
<p>Entre estos nuevos 5 orígenes encontramos el conector de <strong>Real-Time Weather</strong> <strong>(origen meteorológico en tiempo real).</strong> Este conector nos permite obtener datos meteorológicos de diferentes ubicaciones seleccionando una ciudad específica o coordenadas de latitud y longitud para recibir información meteorológica.</p>
<p>Los datos que se obtienen son de temperatura, humedad y velocidad del viento.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745268264000/94aa5911-8fcc-4abe-8a21-d7ec7d451bc5.png" alt class="image--center mx-auto" /></p>
<p>Una vez seleccionado el conector, se debe especificar la localización de la que queremos obtener los datos meteorológicos</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745268395165/fa3b8b65-6904-4666-acc9-0498fee61335.png" alt class="image--center mx-auto" /></p>
<p>Los mayoría de los datos están en formato JSON</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745270521979/4ec98ec9-53e2-41bd-bb36-0571c247a0c0.png" alt class="image--center mx-auto" /></p>
<p>Aunque esto sabemos que no es problema con KQL y podemos realizar las transformaciones necesarias en tiempo real para desgranar la información de estas columnas</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745271383837/d2812bd5-8275-4a1b-82a4-ab7807d80f33.png" alt class="image--center mx-auto" /></p>
<p>De esta forma, podemos construir un Dashboard con información meteorológica en tiempo real de nuestra zona de una manera rápida y sencilla :)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745353483996/32b3a01a-c5ad-4aee-a139-1a0d5b71e3b3.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-referencias">Referencias</h1>
<p><a target="_blank" href="https://blog.fabric.microsoft.com/en-us/blog/fabric-march-2025-feature-summary?ft=All#post-20656-_Toc193974234">Fabric March 2025 Feature Summary | Microsoft Fabric Blog | Microsoft Fabric</a></p>
<p><a target="_blank" href="https://blog.fabric.microsoft.com/en-us/blog/fabric-march-2025-feature-summary?ft=All#post-20656-_Toc193974234">New Eventstream sources: MQTT, Solace PubSub+, Azure Data Explorer, Weather</a> <a target="_blank" href="https://blog.fabric.microsoft.com/en-us/blog/new-eventstream-sources-mqtt-solace-pubsub-azure-data-explorer-weather-event-grid?ft=All">&amp; Azure Event Grid  | Microsoft Fabric Blog | Microsoft Fabric</a></p>
<p><a target="_blank" href="https://learn.microsoft.com/es-es/fabric/real-time-intelligence/event-streams/add-source-real-time-weather">Adición de un origen meteorológico en tiempo real a un eventstream - Microsoft Fabric | Microsoft Learn</a></p>
]]></content:encoded></item><item><title><![CDATA[Semantic Link: Automatiza la creación de tu arquitectura Medallion]]></title><description><![CDATA[En mi artículo anterior, exploramos cómo automatizar la creación de la arquitectura Medallion en Microsoft Fabric utilizando PowerShell y la API de Microsoft Fabric (Microsoft Fabric API + PowerShell: Automatiza la creación de tu arquitectura Medalli...]]></description><link>https://datagym.es/semantic-link-automatiza-la-creacion-de-tu-arquitectura-medallion</link><guid isPermaLink="true">https://datagym.es/semantic-link-automatiza-la-creacion-de-tu-arquitectura-medallion</guid><category><![CDATA[semantic-link]]></category><category><![CDATA[PySpark]]></category><category><![CDATA[microsoft fabric]]></category><category><![CDATA[MedallionArchitecture]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Sun, 06 Apr 2025 14:35:19 GMT</pubDate><content:encoded><![CDATA[<p>En mi artículo anterior, exploramos cómo automatizar la creación de la arquitectura Medallion en Microsoft Fabric utilizando PowerShell y la API de Microsoft Fabric (<a target="_blank" href="https://datagym.es/microsoft-fabric-api-powershell-automatiza-la-creacion-de-tu-arquitectura-medallion">Microsoft Fabric API + PowerShell: Automatiza la creación de tu arquitectura Medallion</a>). Hoy, te traigo una nueva versión del proceso, pero esta vez utilizando <strong>Semantic Link</strong> y un <strong>notebook en Microsoft Fabric</strong>.</p>
<p>El código tiene la misma funcionalidad que el anterior, automatizar la creación de las áreas de trabajo y lakehouses necesarios para nuestra arquitectura medallion. Además, también tienes la posibilidad de almacenar los secretos en Azure Key Vault.</p>
<p>La configuración es sencilla. En una celda encontrarás todas las variables necesarias que deberás de configurar con tus valores. En el caso de que <strong>azureKeyVault</strong> sea <strong>True</strong>, deberás de establecer valor en las siguientes variables y deberás crear una app registration con permisos de Azure Key Vault para poder almacenar los secretos. Los valores los puedes poner directamente en el código.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743949054202/78802b65-2d69-4cbc-9f6e-f05fb7202968.png" alt class="image--center mx-auto" /></p>
<p>La variable medallionInOneWorkspace indica si queremos crear una sola área de trabajo y todos los lakehouses de cada capa en él. En caso contrario, se creará un área de trabajo y lakehouse por capa.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743947421689/a7adebd6-ede0-42dc-bd68-ca86bd4a1f56.png" alt class="image--center mx-auto" /></p>
<p>El código lo podéis encontrar en GitHub a través del siguiente enlace:</p>
<p><a target="_blank" href="https://github.com/kilianbs/blogs/blob/main/Microsoft%20Fabric/Semantic%20Link%20\(SemPy\)_%20Automatiza%20la%20creaci%C3%B3n%20de%20tu%20arquitectura%20Medallion.ipynb">blogs/Microsoft Fabric/Semantic Link (SemPy)_ Automatiza la creación de tu arquitectura Medallion.ipynb at main · kilianbs/blogs</a></p>
]]></content:encoded></item><item><title><![CDATA[Update Policies en KQL: Transformación de datos en tiempo real]]></title><description><![CDATA[Las update policies (directivas de actualización) en KQL son una funcionalidad de Microsoft Fabric que te permite transformar y enriquecer los datos cada vez que se insertan datos en una tabla. En lugar de tener que ejecutar transformaciones posterio...]]></description><link>https://datagym.es/update-policies-en-kql-transformacion-de-datos-en-tiempo-real</link><guid isPermaLink="true">https://datagym.es/update-policies-en-kql-transformacion-de-datos-en-tiempo-real</guid><category><![CDATA[Update policy]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[realtime]]></category><category><![CDATA[Real-time-intelligence]]></category><category><![CDATA[KQL]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Fri, 07 Mar 2025 15:52:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741362822322/c2c3ccdf-cc94-4515-80a4-de43768d6191.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Las update policies (directivas de actualización) en KQL son una funcionalidad de Microsoft Fabric que te permite transformar y enriquecer los datos cada vez que se insertan datos en una tabla. En lugar de tener que ejecutar transformaciones posteriores, las update policies aplican una lógica predefinida automáticamente cada vez que se realiza una inserción.</p>
<p>Cuando se define una update policy en una tabla de destino, se asocia una consulta KQL que se ejecuta automáticamente cada vez que se insertan nuevos registros en la tabla origen. Esta consulta transforma los datos según la lógica establecida y los inserta en la tabla de destino.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741040755712/1fd637ca-7182-4239-8d32-125a8d88e796.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-update-policies-en-accion">Update policies en acción</h1>
<p>Como siempre, con ejemplos prácticos se entiende todo mucho mejor.</p>
<p>Supongamos que tenemos una tabla llamada <code>ISSGeoLoc</code> con el siguiente esquema:</p>
<pre><code class="lang-plaintext">.create table ISSGeoLoc (timestamp: int, iss_position: dynamic)
</code></pre>
<p>El campo <code>iss_position</code> contiene información en formato JSON, y queremos extraer ciertos atributos y guardarlos en una tabla estructurada. El campo timestamp viene en formato Unix y queremos transformarlo en datetime.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741360128218/17a89a92-f1b0-44c0-9a1e-27e459588a88.png" alt class="image--center mx-auto" /></p>
<p>Creamos la tabla de destino <code>SilverISSGeoLoc</code>:</p>
<pre><code class="lang-plaintext">.create table SilverISSGeoLoc (Timestamp:datetime, latitude: real, longitude:real)
</code></pre>
<p>A continuación, creamos la función. Aquí es donde se define la consulta con la lógica que aplicaremos a cada dato que se inserte en la tabla origen.</p>
<pre><code class="lang-plaintext">.create function LoadISSGeoLocToSilver {
    ISSGeoLoc
    | extend timestamp = unixtime_seconds_todatetime(timestamp), j = parse_json(iss_position)
    | extend latitude = toreal(j.latitude), longitude = toreal(j.longitude)
    | project timestamp, latitude, longitude
}
</code></pre>
<p>Ahora, establecemos la directiva de actualización para invocar la función que hemos creado:</p>
<pre><code class="lang-plaintext">.alter table SilverISSGeoLoc policy update
@'[{ "IsEnabled": true, "Source": "ISSGeoLoc", "Query": "LoadISSGeoLocToSilver", "IsTransactional": true, "PropagateIngestionProperties": false}]'
</code></pre>
<ul>
<li><p><strong>IsEnabled</strong>: Una directiva de actualización puede estar habilitada o deshabilitada. Esto es bueno cuando se trabaja con cambios en la política de actualización, ya que a veces necesitamos una pausa en el proceso de carga para hacer los cambios.</p>
</li>
<li><p><strong>Source</strong>: Nombre de la tabla que desencadena la invocación de la política de actualización</p>
</li>
<li><p><strong>Query</strong>: Referencia a la función KQL que se ha definido. Se puede escribir la consulta KQL aquí pero a la larga, el mantenimiento no será fácil.</p>
</li>
<li><p><strong>IsTransactional</strong>: Establece si la política de actualización es transaccional o no, por defecto es false. Si esto se establece a true, entonces si ocurre un error en la carga, toda la transacción se revertirá y no se cargará nada.</p>
</li>
<li><p><strong>PropagateIngestionProperties</strong>: Establece si las propiedades especificadas durante la ingesta en la tabla de origen, como las etiquetas de extensión y la hora de creación, se aplican a la tabla de destino.</p>
</li>
</ul>
<p>Con esta configuración, cada vez que se inserten registros en <code>ISSGeoLoc</code>, la update policy transformará los datos y los insertará automáticamente en <code>SilverISSGeoLoc</code>.</p>
<p>Para verificar los datos:</p>
<pre><code class="lang-python">SilverISSGeoLoc
| take <span class="hljs-number">100</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741360468237/1c31c9c5-a860-4ba3-a0c4-d2696a761b3a.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-ventajas-y-consideraciones-de-las-update-policies-en-kql">Ventajas y consideraciones de las update policies en KQL</h1>
<h2 id="heading-ventajas">Ventajas</h2>
<ul>
<li><p><strong>Automatización de transformaciones:</strong> Una vez definidas, las update policies se ejecutan automáticamente, eliminando la necesidad de tareas de transformación posteriores.</p>
</li>
<li><p><strong>Eficiencia y rendimiento:</strong> Reducen la latencia al aplicar transformaciones en el momento de la inserción, evitando procesos por lotes adicionales.</p>
</li>
<li><p><strong>Simplificación de arquitectura:</strong> Al minimizar la necesidad de pipelines ETL separados, la infraestructura de datos se vuelve más simple y fácil de mantener.</p>
</li>
<li><p><strong>Datos siempre preparados:</strong> Las tablas derivadas siempre contienen datos transformados y listos para su análisis.</p>
</li>
</ul>
<h2 id="heading-consideraciones">Consideraciones</h2>
<ul>
<li><p><strong>Consumo de recursos:</strong> Dado que las transformaciones se realizan al insertar datos, es importante monitorear el impacto en el rendimiento, especialmente en cargas masivas.</p>
</li>
<li><p><strong>Complejidad de la lógica:</strong> Update policies demasiado complejas pueden ralentizar la inserción de datos; conviene mantenerlas lo más eficientes posible.</p>
</li>
<li><p><strong>Dependencia de la tabla origen:</strong> Cambios en el esquema de la tabla origen pueden afectar la update policy y requerir ajustes.</p>
</li>
</ul>
<h1 id="heading-referencias">Referencias</h1>
<p><a target="_blank" href="https://learn.microsoft.com/en-us/kusto/management/update-policy?view=microsoft-fabric">Update policy overview - Kusto | Microsoft Learn</a></p>
]]></content:encoded></item><item><title><![CDATA[¿Qué es Liquid Clustering y por qué es un game changer?]]></title><description><![CDATA[Liquid Clustering es una técnica introducida por Delta Lake diseñada para superar las limitaciones de las estrategias de particionamiento y ordenamiento de datos, como Z-Ordering. Esta técnica se centra en maximizar la eficiencia del almacenamiento y...]]></description><link>https://datagym.es/que-es-liquid-clustering-y-por-que-es-un-game-changer</link><guid isPermaLink="true">https://datagym.es/que-es-liquid-clustering-y-por-que-es-un-game-changer</guid><category><![CDATA[liquid clustering]]></category><category><![CDATA[deltalake]]></category><category><![CDATA[PySpark]]></category><category><![CDATA[microsoft fabric]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Tue, 25 Feb 2025 16:13:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1738526982816/527aec04-133f-4194-b37e-d9a1d0f99a21.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Liquid Clustering</strong> es una técnica introducida por <strong>Delta Lake</strong> diseñada para superar las limitaciones de las estrategias de particionamiento y ordenamiento de datos, como <em>Z-Ordering</em>. Esta técnica se centra en maximizar la eficiencia del almacenamiento y la organización de datos al minimizar la necesidad de configuraciones manuales y tareas recurrentes para reescribir los archivos de datos existentes.</p>
<p>Tanto con <strong>particionamiento, Z-Order o Liquid Clustering</strong>, lo que buscamos es una <strong>mayor velocidad en las consultas y un almacenamiento de datos más eficiente</strong>. Sin embargo, la gran diferencia de esta última es la flexibilidad que ofrece. Con Liquid Clustering no es necesario saber cuales van a ser las columnas que los usuarios van a utilizar para filtrar datos y además, al cambiar las claves escogidas, no necesita reescribir los datos de la tabla como en el caso de Z-Order.</p>
<p>Además, Liquid Clustering utiliza un algoritmo dinámico basado en las estadísticas del archivo para optimizar la distribución de los datos y minimizar los problemas causados por desbalanceo de archivos o skew.</p>
<blockquote>
<p>Liquid Clustering está disponible para Delta Lake 3.1.0 y versiones superiores. Runtime Version 1.3 de Spark en Microsoft Fabric.</p>
</blockquote>
<h1 id="heading-como-almacena-y-lee-liquid-clustering-los-archivos-parquet"><strong>¿Cómo almacena y lee Liquid Clustering los archivos Parquet?</strong></h1>
<ol>
<li><p><strong>Almacenamiento</strong>:</p>
<ul>
<li><p>Cuando activas Liquid Clustering, Delta Lake organiza los datos en clústeres utilizando <strong>rangos de valores</strong> (no carpetas físicas como en las particiones).</p>
</li>
<li><p>Estos clústeres están identificados por <strong>estadísticas de archivo</strong> (mínimos, máximos, etc…) que se almacenan en el <em>transaction log</em> de Delta.</p>
</li>
</ul>
</li>
<li><p><strong>Lectura</strong>:</p>
<ul>
<li><p>Durante una consulta, Liquid Clustering utiliza las estadísticas almacenadas para hacer un <strong>pruning de archivos eficiente</strong>, es decir, lee únicamente los archivos relevantes para los valores solicitados en la consulta.</p>
</li>
<li><p>Esto elimina la necesidad de escanear archivos completos innecesariamente y reduce significativamente el tiempo de respuesta.</p>
</li>
</ul>
</li>
</ol>
<h1 id="heading-cuando-utilizar-liquid-clustering">¿Cuándo utilizar Liquid Clustering?</h1>
<p>Liquid Clustering es especialmente útil cuando:</p>
<ul>
<li><p>Se insertan nuevos datos regularmente a la tabla</p>
</li>
<li><p>Existen datos con alta cardinalidad o sin una partición clara</p>
</li>
<li><p>Los patrones de consulta cambian con el tiempo</p>
</li>
<li><p>Se necesita soportar escrituras concurrentes</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">❗</div>
<div data-node-type="callout-text">Liquid Clustering no es compatible con la partición estilo Hive ni con Z-Order</div>
</div>

<h1 id="heading-beneficios-de-liquid-clustering"><strong>Beneficios de Liquid Clustering</strong></h1>
<ol>
<li><p><strong>Eficiencia en consultas</strong>:</p>
<ul>
<li>Mejora el rendimiento al reducir la cantidad de archivos que deben ser leídos gracias a las estadísticas de rangos.</li>
</ul>
</li>
<li><p><strong>Manejo de cardinalidad alta</strong>:</p>
<ul>
<li>Es ideal para datos donde las particiones tradicionales generarían demasiadas carpetas pequeñas, lo que afectaría el rendimiento.</li>
</ul>
</li>
<li><p><strong>Evolución automática</strong>:</p>
<ul>
<li>Permite reorganizar los datos con operaciones de optimización, manteniéndolos eficientemente organizados a medida que crecen.</li>
</ul>
</li>
<li><p><strong>Flexibilidad</strong>:</p>
<ul>
<li>No necesitas decidir de antemano una estructura de partición, lo que simplifica el diseño inicial.</li>
</ul>
</li>
<li><p><strong>Evita problemas de skew</strong>:</p>
<ul>
<li>Rebalancea los datos automáticamente para evitar concentraciones de valores en pocos archivos.</li>
</ul>
</li>
</ol>
<h1 id="heading-como-utilizar-liquid-clustering">¿Cómo utilizar Liquid Clustering?</h1>
<h2 id="heading-creando-una-tabla-vacia">Creando una tabla vacía</h2>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> liquid_clustering_table
(
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">int</span>,
    valor <span class="hljs-built_in">int</span>,
    categoria <span class="hljs-keyword">string</span>,
    fecha <span class="hljs-built_in">date</span>
)
CLUSTER <span class="hljs-keyword">BY</span>
(
    categoria,
    fecha
);
</code></pre>
<h2 id="heading-utilizando-ctas">Utilizando CTAS</h2>
<pre><code class="lang-python">spark.sql(<span class="hljs-string">"CREATE TABLE liquid_clustering_table CLUSTER BY (categoria, fecha) AS SELECT * FROM base_table"</span>)
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Solo se permiten 4 columnas por las que clusterizar</div>
</div>

<h2 id="heading-modificacion-de-las-columnas-clusterizadas">Modificación de las columnas clusterizadas</h2>
<p>En el caso de que se quieran modificar las columnas, se puede hacer con el siguiente comando:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> liquid_clustering_table CLUSTER <span class="hljs-keyword">BY</span> (categoria)
</code></pre>
<p>Cuando se cambian las columnas de clustering, todas las nuevas escrituras de datos y operaciones OPTIMIZE seguirán las nuevas columnas de clustering. Los datos existentes no se reescriben.</p>
<h2 id="heading-desactivar-liquid-clustering">Desactivar Liquid Clustering</h2>
<p>También se puede <strong>desactivar</strong> la funcionalidad Liquid Clustering pero esta operación no reescribe los datos que ya han sido clusterizados, sino que evita que los nuevos datos y las operaciones OPTIMIZE utilicen las columnas clusterizadas para organizar los datos.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> liquid_clustering_table CLUSTER <span class="hljs-keyword">BY</span> <span class="hljs-keyword">NONE</span>
</code></pre>
<h1 id="heading-demo-time">Demo Time!</h1>
<p>He realizado una comparativa con diferentes técnicas para visualizar el impacto de cada una de ellas a la hora de consultar los datos. Las comparativas son:</p>
<ul>
<li><p>Tabla delta</p>
</li>
<li><p>Tabla delta con particionado</p>
</li>
<li><p>Tabla delta con Z-Order</p>
</li>
<li><p>Tabla delta con Liquid Clustering</p>
</li>
</ul>
<h2 id="heading-comparativas">Comparativas</h2>
<h3 id="heading-dataset">Dataset</h3>
<p>Los datos que he utilizado son los de Yellow Taxi Trip Data que se pueden obtener desde la siguiente web: <a target="_blank" href="https://www.nyc.gov/site/tlc/about/raw-data.page">Raw Data - TLC</a></p>
<p>El conjunto de datos es de 443 millones de registros con la siguiente estructura:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738525506953/169859dd-8fbf-48fe-91d2-abf512be1c98.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738525588486/e66cf6c3-949f-457b-bcdd-ddf544589873.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-creacion-de-las-tablas">Creación de las tablas</h3>
<pre><code class="lang-python"><span class="hljs-comment"># Crear tabla Delta sin partición</span>
df.write.format(<span class="hljs-string">"delta"</span>).mode(<span class="hljs-string">"overwrite"</span>).saveAsTable(<span class="hljs-string">"nyc_yellow_taxi_trip_data_no_partition"</span>)

<span class="hljs-comment"># Crear tabla Delta particionada por nyc_year</span>
df.write.format(<span class="hljs-string">"delta"</span>).partitionBy(<span class="hljs-string">"nyc_year"</span>).mode(<span class="hljs-string">"overwrite"</span>).saveAsTable(<span class="hljs-string">"nyc_yellow_taxi_trip_data_partitioned"</span>)
</code></pre>
<pre><code class="lang-python"><span class="hljs-comment"># Crear tabla Delta con partición y Z-Order</span>
df.write.format(<span class="hljs-string">"delta"</span>).mode(<span class="hljs-string">"overwrite"</span>).saveAsTable(<span class="hljs-string">"nyc_yellow_taxi_trip_data_zorder"</span>)

spark.sql(<span class="hljs-string">f"OPTIMIZE nyc_yellow_taxi_trip_data_zorder ZORDER BY (nyc_year)"</span>)
</code></pre>
<pre><code class="lang-python"><span class="hljs-comment"># Crear tabla Delta para Liquid Clustering</span>
spark.sql(<span class="hljs-string">"CREATE TABLE nyc_yellow_taxi_trip_data_liquid_clustering CLUSTER BY (nyc_year, PULocationID, DOLocationID, passenger_count) AS SELECT * FROM nyc_yellow_taxi_trip_data_no_partition"</span>)

spark.sql(<span class="hljs-string">"OPTIMIZE nyc_yellow_taxi_trip_data_liquid_clustering"</span>)
</code></pre>
<p>Comprobamos que se ha creado correctamente la tabla</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738525894986/40d70c9a-f4f4-4e55-baa1-57436755fbeb.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-comparativa-1">Comparativa 1</h3>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> time <span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> matplotlib.pyplot <span class="hljs-keyword">as</span> plt

<span class="hljs-comment"># Crear una función para medir los tiempos de consulta</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">measure_query_time</span>(<span class="hljs-params">query</span>):</span>
    start_time = time()
    spark.sql(query).collect()  <span class="hljs-comment"># Ejecutar la consulta</span>
    end_time = time()
    <span class="hljs-keyword">return</span> end_time - start_time

<span class="hljs-comment"># Diccionario para almacenar los tiempos</span>
query_times = {}

<span class="hljs-comment"># Consultas</span>
queries = {
    <span class="hljs-string">"simple_query"</span>: <span class="hljs-string">"SELECT count(1) as nrows FROM nyc_yellow_taxi_trip_data_no_partition WHERE nyc_year = 2018"</span>,
    <span class="hljs-string">"partition_query"</span>: <span class="hljs-string">"SELECT count(1) as nrows FROM nyc_yellow_taxi_trip_data_partitioned WHERE nyc_year = 2018"</span>,
    <span class="hljs-string">"zorder_query"</span>: <span class="hljs-string">"SELECT count(1) as nrows FROM nyc_yellow_taxi_trip_data_zorder WHERE nyc_year = 2018"</span>,
    <span class="hljs-string">"liquid_clustering_query"</span>: <span class="hljs-string">"SELECT count(1) as nrows FROM nyc_yellow_taxi_trip_data_liquid_clustering WHERE nyc_year = 2018"</span>,
}

<span class="hljs-comment"># Ejecutar las consultas y medir tiempos</span>
<span class="hljs-keyword">for</span> query_name, query <span class="hljs-keyword">in</span> queries.items():
    query_times[query_name] = measure_query_time(query)

<span class="hljs-comment"># Mostrar los resultados</span>
<span class="hljs-keyword">for</span> query_name, exec_time <span class="hljs-keyword">in</span> query_times.items():
    print(<span class="hljs-string">f"<span class="hljs-subst">{query_name}</span>: <span class="hljs-subst">{exec_time:<span class="hljs-number">.2</span>f}</span> segundos"</span>)

<span class="hljs-comment"># Crear un gráfico comparativo</span>
plt.figure(figsize=(<span class="hljs-number">10</span>, <span class="hljs-number">6</span>))
plt.bar(query_times.keys(), query_times.values(), color=[<span class="hljs-string">'blue'</span>, <span class="hljs-string">'orange'</span>, <span class="hljs-string">'green'</span>, <span class="hljs-string">'red'</span>])
plt.ylabel(<span class="hljs-string">"Tiempo de ejecución (segundos)"</span>)
plt.title(<span class="hljs-string">"Comparativa de tiempos de consulta"</span>)
plt.show()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738525996996/5c1bfaeb-f876-4a52-a54c-60c500e212cb.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-comparativa-2">Comparativa 2</h3>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> time <span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> matplotlib.pyplot <span class="hljs-keyword">as</span> plt

<span class="hljs-comment"># Crear una función para medir los tiempos de consulta</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">measure_query_time</span>(<span class="hljs-params">query</span>):</span>
    start_time = time()
    spark.sql(query).collect()  <span class="hljs-comment"># Ejecutar la consulta</span>
    end_time = time()
    <span class="hljs-keyword">return</span> end_time - start_time

<span class="hljs-comment"># Diccionario para almacenar los tiempos</span>
query_times = {}

<span class="hljs-comment"># Consultas</span>
queries = {
    <span class="hljs-string">"simple_query"</span>: <span class="hljs-string">"SELECT nyc_year, PULocationID, SUM(passenger_count) as passenger_count FROM nyc_yellow_taxi_trip_data_no_partition GROUP BY nyc_year, PULocationID"</span>,
    <span class="hljs-string">"partition_query"</span>: <span class="hljs-string">"SELECT nyc_year, PULocationID, SUM(passenger_count) as passenger_count FROM nyc_yellow_taxi_trip_data_partitioned GROUP BY nyc_year, PULocationID"</span>,
    <span class="hljs-string">"zorder_query"</span>: <span class="hljs-string">"SELECT nyc_year, PULocationID, SUM(passenger_count) as passenger_count FROM nyc_yellow_taxi_trip_data_zorder GROUP BY nyc_year, PULocationID"</span>,
    <span class="hljs-string">"liquid_clustering_query"</span>: <span class="hljs-string">"SELECT nyc_year, PULocationID, SUM(passenger_count) as passenger_count FROM nyc_yellow_taxi_trip_data_liquid_clustering GROUP BY nyc_year, PULocationID"</span>,
}

<span class="hljs-comment"># Ejecutar las consultas y medir tiempos</span>
<span class="hljs-keyword">for</span> query_name, query <span class="hljs-keyword">in</span> queries.items():
    query_times[query_name] = measure_query_time(query)

<span class="hljs-comment"># Mostrar los resultados</span>
<span class="hljs-keyword">for</span> query_name, exec_time <span class="hljs-keyword">in</span> query_times.items():
    print(<span class="hljs-string">f"<span class="hljs-subst">{query_name}</span>: <span class="hljs-subst">{exec_time:<span class="hljs-number">.2</span>f}</span> segundos"</span>)

<span class="hljs-comment"># Crear un gráfico comparativo</span>
plt.figure(figsize=(<span class="hljs-number">10</span>, <span class="hljs-number">6</span>))
plt.bar(query_times.keys(), query_times.values(), color=[<span class="hljs-string">'blue'</span>, <span class="hljs-string">'orange'</span>, <span class="hljs-string">'green'</span>, <span class="hljs-string">'red'</span>])
plt.ylabel(<span class="hljs-string">"Tiempo de ejecución (segundos)"</span>)
plt.title(<span class="hljs-string">"Comparativa de tiempos de consulta"</span>)
plt.show()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738526071483/fd1d11ff-9738-42f6-b39c-0f62c909dd0c.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-comparativa-3">Comparativa 3</h3>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> time <span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> matplotlib.pyplot <span class="hljs-keyword">as</span> plt

<span class="hljs-comment"># Crear una función para medir los tiempos de consulta</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">measure_query_time</span>(<span class="hljs-params">query</span>):</span>
    start_time = time()
    spark.sql(query).collect()  <span class="hljs-comment"># Ejecutar la consulta</span>
    end_time = time()
    <span class="hljs-keyword">return</span> end_time - start_time

<span class="hljs-comment"># Diccionario para almacenar los tiempos</span>
query_times = {}

<span class="hljs-comment"># Consultas</span>
queries = {
    <span class="hljs-string">"simple_query"</span>: <span class="hljs-string">"SELECT COUNT(1) as nrows FROM nyc_yellow_taxi_trip_data_no_partition WHERE passenger_count &gt; 2 AND PULocationID = 264"</span>,
    <span class="hljs-string">"partition_query"</span>: <span class="hljs-string">"SELECT COUNT(1) as nrows FROM nyc_yellow_taxi_trip_data_partitioned WHERE passenger_count &gt; 2 AND PULocationID = 264"</span>,
    <span class="hljs-string">"zorder_query"</span>: <span class="hljs-string">"SELECT COUNT(1) as nrows FROM nyc_yellow_taxi_trip_data_zorder WHERE passenger_count &gt; 2 AND PULocationID = 264"</span>,
    <span class="hljs-string">"liquid_clustering_query"</span>: <span class="hljs-string">"SELECT COUNT(1) as nrows FROM nyc_yellow_taxi_trip_data_liquid_clustering WHERE passenger_count &gt; 2 AND PULocationID = 264"</span>,
}

<span class="hljs-comment"># Ejecutar las consultas y medir tiempos</span>
<span class="hljs-keyword">for</span> query_name, query <span class="hljs-keyword">in</span> queries.items():
    query_times[query_name] = measure_query_time(query)

<span class="hljs-comment"># Mostrar los resultados</span>
<span class="hljs-keyword">for</span> query_name, exec_time <span class="hljs-keyword">in</span> query_times.items():
    print(<span class="hljs-string">f"<span class="hljs-subst">{query_name}</span>: <span class="hljs-subst">{exec_time:<span class="hljs-number">.2</span>f}</span> segundos"</span>)

<span class="hljs-comment"># Crear un gráfico comparativo</span>
plt.figure(figsize=(<span class="hljs-number">10</span>, <span class="hljs-number">6</span>))
plt.bar(query_times.keys(), query_times.values(), color=[<span class="hljs-string">'blue'</span>, <span class="hljs-string">'orange'</span>, <span class="hljs-string">'green'</span>, <span class="hljs-string">'red'</span>])
plt.ylabel(<span class="hljs-string">"Tiempo de ejecución (segundos)"</span>)
plt.title(<span class="hljs-string">"Comparativa de tiempos de consulta"</span>)
plt.show()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738526521859/ba7cac05-d4c9-47e2-9f64-44fb8e3d7653.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-comparativa-4">Comparativa 4</h3>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> time <span class="hljs-keyword">import</span> time
<span class="hljs-keyword">import</span> matplotlib.pyplot <span class="hljs-keyword">as</span> plt

<span class="hljs-comment"># Crear una función para medir los tiempos de consulta</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">measure_query_time</span>(<span class="hljs-params">query</span>):</span>
    start_time = time()
    spark.sql(query).collect()  <span class="hljs-comment"># Ejecutar la consulta</span>
    end_time = time()
    <span class="hljs-keyword">return</span> end_time - start_time

<span class="hljs-comment"># Diccionario para almacenar los tiempos</span>
query_times = {}

<span class="hljs-comment"># Consultas</span>
queries = {
    <span class="hljs-string">"simple_query"</span>: <span class="hljs-string">"SELECT * FROM nyc_yellow_taxi_trip_data_no_partition WHERE PULocationID = 150 AND DOLocationID = 50 AND passenger_count &gt; 1"</span>,
    <span class="hljs-string">"partition_query"</span>: <span class="hljs-string">"SELECT * FROM nyc_yellow_taxi_trip_data_partitioned WHERE PULocationID = 150 AND  DOLocationID = 50 AND passenger_count &gt; 1"</span>,
    <span class="hljs-string">"zorder_query"</span>: <span class="hljs-string">"SELECT * FROM nyc_yellow_taxi_trip_data_zorder WHERE PULocationID = 150 AND  DOLocationID = 50 AND passenger_count &gt; 1"</span>,
    <span class="hljs-string">"liquid_clustering_query"</span>: <span class="hljs-string">"SELECT * FROM nyc_yellow_taxi_trip_data_liquid_clustering WHERE PULocationID = 150 AND  DOLocationID = 50 AND passenger_count &gt; 1"</span>,
}

<span class="hljs-comment"># Ejecutar las consultas y medir tiempos</span>
<span class="hljs-keyword">for</span> query_name, query <span class="hljs-keyword">in</span> queries.items():
    query_times[query_name] = measure_query_time(query)

<span class="hljs-comment"># Mostrar los resultados</span>
<span class="hljs-keyword">for</span> query_name, exec_time <span class="hljs-keyword">in</span> query_times.items():
    print(<span class="hljs-string">f"<span class="hljs-subst">{query_name}</span>: <span class="hljs-subst">{exec_time:<span class="hljs-number">.2</span>f}</span> segundos"</span>)

<span class="hljs-comment"># Crear un gráfico comparativo</span>
plt.figure(figsize=(<span class="hljs-number">10</span>, <span class="hljs-number">6</span>))
plt.bar(query_times.keys(), query_times.values(), color=[<span class="hljs-string">'blue'</span>, <span class="hljs-string">'orange'</span>, <span class="hljs-string">'green'</span>, <span class="hljs-string">'red'</span>])
plt.ylabel(<span class="hljs-string">"Tiempo de ejecución (segundos)"</span>)
plt.title(<span class="hljs-string">"Comparativa de tiempos de consulta"</span>)
plt.show()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738526256909/28ac1449-678c-4c36-8b1b-05f158f31cf4.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-conclusiones">Conclusiones</h1>
<p>Como se ha podido observar, cuando todas las consultas contienen la columna utilizada por todas las técnicas, en este caso la columna nyc_year, podemos observar que Liquid Clustering está por detrás del particionado y Z-Order, aunque no por una diferencia significativa. Sin embargo, cuando esta columna desaparece de la ecuación de las consultas, cosa que es muy probable que ocurra, Liquid Clustering destaca sobre las demás.</p>
<p>En términos generales, utilizar Liquid Clustering ofrece una mayor eficiencia y flexibilidad en comparación con las otras técnicas, lo que se traduce en mejoras significativas en el rendimiento de las consultas y el mantenimiento de datos. Por lo que, se recomienda utilizar esta técnica para todas las tablas Delta que su tamaño sea inferior a 10TB, que en ese caso es más recomendable utilizar particiones en conjunto con Z-Order.</p>
]]></content:encoded></item><item><title><![CDATA[Microsoft Fabric API + PowerShell: Automatiza la creación de tu arquitectura Medallion]]></title><description><![CDATA[En cada proyecto, siempre hay tareas repetitivas. Automatizarlas no solo optimiza el tiempo, sino que también simplifica el trabajo y mejora la eficiencia.
En proyectos con Microsoft Fabric y siguiendo las buenas prácticas con la famosa arquitectura ...]]></description><link>https://datagym.es/microsoft-fabric-api-powershell-automatiza-la-creacion-de-tu-arquitectura-medallion</link><guid isPermaLink="true">https://datagym.es/microsoft-fabric-api-powershell-automatiza-la-creacion-de-tu-arquitectura-medallion</guid><category><![CDATA[fabricapi]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[api]]></category><category><![CDATA[Powershell]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Sun, 09 Feb 2025 08:46:25 GMT</pubDate><content:encoded><![CDATA[<p>En cada proyecto, siempre hay tareas repetitivas. Automatizarlas no solo optimiza el tiempo, sino que también simplifica el trabajo y mejora la eficiencia.</p>
<p>En proyectos con Microsoft Fabric y siguiendo las buenas prácticas con la famosa arquitectura medallion, lo primero que hacemos es crear las áreas de trabajo y lakehouses de cada capa. El siguiente código realiza estas tareas de forma automática y además, tiene la opción de guardar los secretos en Azure Key Vault.</p>
<p>El código necesita que las siguientes variables estén definidas para que se ejecute con éxito, en caso contrario, aparecerá un mensaje de error comentando la variable que se necesita configurar.</p>
<ul>
<li><p><strong>$tenantId</strong></p>
</li>
<li><p><strong>$subscriptionId</strong></p>
</li>
<li><p><strong>$projectName</strong> → Nombre que se asignará a las áreas de trabajo</p>
</li>
<li><p><strong>$layers</strong> → Definición de las distintas capas que tendrá nuestro proyecto. Los valores se añadirán al nombre de las áreas de trabajo (projectName_layer)</p>
</li>
</ul>
<blockquote>
<p>Las variables tenantId y subscriptionId son necesarias porque de momento la conexión se realiza con usuario y no como service principal.</p>
</blockquote>
<p>También existen unas variables que se pueden configurar dependiendo de lo siguiente:</p>
<ul>
<li><p><strong>$medallionInOneWorkspace</strong>: por defecto, su valor es <em>false</em>. Esta variable especifica si la arquitectura medallion se crea en distintas áreas de trabajo o en uno solo. Crear todo en una área de trabajo puede tener sentido para pruebas, donde el área de trabajo se llamará con el nombre de la variable $projectName y se crearán tantos lakehouses como capas definidas en la variable $layers</p>
</li>
<li><p><strong>$azureKeyVault</strong>: por defecto, su valor es <em>false</em>. Si quieres almacenar los secretos en Azure Key Vault, debes configurar el valor a true y establecer el nombre del key vault donde se van a almacenar.</p>
</li>
</ul>
<p>La variable <strong>$capacityId</strong> es “opcional”, si no sabes el id de la capacidad puedes dejarla en blanco y se listarán las capacidades disponibles para que selecciones la deseada</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739042881769/5206d64a-4046-44e5-a44f-243e146adafa.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-ejemplo-de-resultado">Ejemplo de resultado</h1>
<p>Ejecución para crear una arquitectura medallion de 3 capas almacenando los secretos en Azure Key Vault.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739043053279/9f4e5624-9e33-4f0b-940a-dfd852156e53.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739043058736/ac7b8733-bed0-4959-8c8f-e4887cbf6674.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739090921371/1ec6aca5-0818-4a35-b2f1-ec5eaa16da3a.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-codigo">Código</h1>
<pre><code class="lang-python"><span class="hljs-comment">######################################################################################################################################</span>
<span class="hljs-comment">## Asegúrese de que los módulos Az están instalados en su sistema ejecutando 'Install-Module Az'</span>
<span class="hljs-comment">######################################################################################################################################</span>

$tenantId = <span class="hljs-string">"tenantId"</span>
$subscriptionId = <span class="hljs-string">"subscriptionId"</span>
$capacityId = <span class="hljs-string">""</span>
$projectName = <span class="hljs-string">"projectName"</span>
$layers = @(<span class="hljs-string">"01_Bronze"</span>,<span class="hljs-string">"02_Silver"</span>,<span class="hljs-string">"03_Gold"</span>) <span class="hljs-comment"># Cambiar nombre de las capas y añadir o quitar según necesidad</span>
$medallionInOneWorkspace = $false
$azureKeyVault = $false
$azureKeyVaultName = <span class="hljs-string">"azureKeyVaultName"</span>

<span class="hljs-keyword">if</span> (-<span class="hljs-keyword">not</span> $tenantId) {
    Write-Error <span class="hljs-string">"El parámetro 'tenantId' es obligatorio. Por favor, configúralo en el script antes de ejecutarlo."</span>
    exit <span class="hljs-number">1</span> 
}
elseif (-<span class="hljs-keyword">not</span> $subscriptionId){
    Write-Error <span class="hljs-string">"El parámetro 'subscriptionId' es obligatorio. Por favor, configúralo en el script antes de ejecutarlo."</span>
    exit <span class="hljs-number">1</span> 
}
elseif (-<span class="hljs-keyword">not</span> $projectName){
    Write-Error <span class="hljs-string">"El parámetro 'projectName' es obligatorio. Por favor, configúralo en el script antes de ejecutarlo."</span>
    exit <span class="hljs-number">1</span> 
}
elseif (-<span class="hljs-keyword">not</span> $layers -<span class="hljs-keyword">or</span> $layers.Count -eq <span class="hljs-number">0</span>){
    Write-Error <span class="hljs-string">"El parámetro 'layers' es obligatorio y debe contener al menos un elemento."</span>
    exit <span class="hljs-number">1</span> 
}

<span class="hljs-keyword">if</span>($azureKeyVault -<span class="hljs-keyword">and</span> -<span class="hljs-keyword">not</span> $azureKeyVaultName)
{
    Write-Error <span class="hljs-string">"Error: Se requiere el parámetro 'azureKeyVaultName' cuando 'azureKeyVault' está habilitado (true). Configure 'azureKeyVaultName' y vuelva a ejecutar el script."</span>
    exit <span class="hljs-number">1</span>
}


<span class="hljs-comment"># URL base de la api de Microsoft Fabric</span>
$baseFabricUrl = <span class="hljs-string">"https://api.fabric.microsoft.com"</span>

<span class="hljs-comment"># Inicio de sesión en Fabric</span>
Connect-AzAccount -TenantId $tenantId -Subscription $subscriptionId | Out-Null

<span class="hljs-comment"># Obtenemos el token</span>
$fabricToken = (Get-AzAccessToken -ResourceUrl $baseFabricUrl).Token

<span class="hljs-comment"># Crear cabeceras para las llamadas a la API</span>
$headerParams = @{<span class="hljs-string">'Authorization'</span>=<span class="hljs-string">"Bearer {0}"</span> -f $fabricToken}
$contentType = @{<span class="hljs-string">'Content-Type'</span> = <span class="hljs-string">"application/json"</span>}


$seleccionCapacidad = $false
$opcionesCapacidades = @()

<span class="hljs-keyword">if</span> (-<span class="hljs-keyword">not</span> $capacityId) {
    Write-Host <span class="hljs-string">"Es necesario especificar el id de la capacidad que se va a utilizar. A continuación se muestran las capacidades disponibles, selecciona cual quieres utilizar:"</span>
    Write-Host <span class="hljs-string">""</span>
    $capacitiesUri = <span class="hljs-string">"{0}/v1/capacities"</span> -f $baseFabricUrl
    $capacitiesList = Invoke-RestMethod -Headers $headerParams -ContentType $contentType -Method GET -Uri $capacitiesUri

    foreach ($capacity <span class="hljs-keyword">in</span> $capacitiesList.value) {
        Write-Host <span class="hljs-string">"ID de la capacidad: $($capacity.id)"</span>
        Write-Host <span class="hljs-string">"Nombre de la capacidad: $($capacity.displayName)"</span>
        Write-Host <span class="hljs-string">"SKU: $($capacity.sku)"</span>
        Write-Host <span class="hljs-string">"Region: $($capacity.region)"</span>
        Write-Host <span class="hljs-string">"Estado: $($capacity.state)"</span>
        Write-Host <span class="hljs-string">""</span>
        $opcionesCapacidades += $capacity.displayName
    }

    $opcionesCapacidades += <span class="hljs-string">"Salir"</span>
    $valorSeleccionado = $null

    <span class="hljs-keyword">while</span> (-<span class="hljs-keyword">not</span> $seleccionCapacidad) {
        <span class="hljs-comment"># Mostramos las opciones</span>
        Write-Host <span class="hljs-string">"Por favor, escribe el nombre de la capacidad que quieres utilizar:"</span>
        $opcionesCapacidades | ForEach-Object { Write-Host <span class="hljs-string">"- $_"</span> }

        <span class="hljs-comment"># Pedimos seleccionar la capacidad</span>
        $valorSeleccionado = Read-Host <span class="hljs-string">"Ingrese su elección"</span>

        <span class="hljs-comment"># Validar la selección</span>
        <span class="hljs-keyword">if</span> ($opcionesCapacidades -contains $valorSeleccionado) {
            <span class="hljs-keyword">if</span> ($valorSeleccionado -eq <span class="hljs-string">"Salir"</span>) {
                Write-Host <span class="hljs-string">"Has decidido finalizar la ejecución. Saliendo..."</span> -ForegroundColor Red
                Exit
            } <span class="hljs-keyword">else</span> {
                Write-Host <span class="hljs-string">"Has seleccionado: $valorSeleccionado"</span> -ForegroundColor Green
                foreach ($capacity <span class="hljs-keyword">in</span> $capacitiesList.value){
                    <span class="hljs-keyword">if</span>($capacity.displayName -eq $valorSeleccionado){
                        $capacityId = $capacity.id
                        Write-Host <span class="hljs-string">"El ID de la capacidad seleccionada es: $($capacity.id)"</span> -ForegroundColor Green
                    }
                }
                $seleccionCapacidad = $true

            }
        } <span class="hljs-keyword">else</span> {
            Write-Host <span class="hljs-string">"Selección no válida. Inténtalo de nuevo."</span> -ForegroundColor Yellow
        }
    }

}

Write-Host <span class="hljs-string">""</span>

$workspacesDisponibles = @()
$workspaceId = <span class="hljs-string">""</span>


<span class="hljs-comment"># Si la variable es true, generamos todo en un área de trabajo</span>
<span class="hljs-keyword">if</span> ($medallionInOneWorkspace)
{
    <span class="hljs-comment">######################################################################################################################################</span>
    <span class="hljs-comment">## ÁREA DE TRABAJO</span>
    <span class="hljs-comment">##</span>
    <span class="hljs-comment">## Se comprueba si existe el área de trabajo. Si existe, obtenemos el workspaceId, sino, creamos el área de trabajo</span>
    <span class="hljs-comment">## y obtenemos el workspaceId.</span>
    <span class="hljs-comment">##</span>
    <span class="hljs-comment">######################################################################################################################################</span>

    Write-Host <span class="hljs-string">"El script está configurado para crear todo en una área de trabajo"</span>
    Write-Host <span class="hljs-string">"Inicializando la creación del área de trabajo..."</span>
    Write-Host <span class="hljs-string">""</span>

    <span class="hljs-comment"># Listamos las áreas de trabajo</span>
    $workspacesUri = <span class="hljs-string">"{0}/v1/workspaces"</span> -f $baseFabricUrl
    $workspacesList = Invoke-RestMethod -Headers $headerParams -ContentType $contentType -Method GET -Uri $workspacesUri
    foreach ($workspace <span class="hljs-keyword">in</span> $workspacesList.value) 
    {
        $workspacesDisponibles += $workspace.displayName
    }

    <span class="hljs-keyword">if</span> ($workspacesDisponibles -contains $projectName)
    {
        Write-Host <span class="hljs-string">"El workspace $($projectName) ya existe. Se crearán los objetos sobre esta área de trabajo."</span>
        foreach ($workspace <span class="hljs-keyword">in</span> $workspacesList.value) 
        {
            <span class="hljs-keyword">if</span>($workspace.displayName -eq $projectName)
            {
                $workspaceId = $workspace.id
                Write-Host <span class="hljs-string">"Workspace Name: $($workspace.displayName)"</span> -ForegroundColor Cyan
                Write-Host <span class="hljs-string">"Workspace ID: $($workspace.id)"</span> -ForegroundColor Cyan
                Write-Host <span class="hljs-string">"Capacity ID: $($workspace.capacityId)"</span> -ForegroundColor Cyan
                Write-Host <span class="hljs-string">""</span>
            }
        }

        <span class="hljs-keyword">if</span>($azureKeyVault)
        {
            <span class="hljs-comment"># Establecer los valores de los secretos del Workspace al KeyVault</span>
            $body = @{
                <span class="hljs-string">"value"</span> = $workspace.id
            } | ConvertTo-Json -Depth <span class="hljs-number">1</span>

            <span class="hljs-keyword">try</span>{
                <span class="hljs-comment">#Invoke-RestMethod -Headers $headerParams -Method Put -Uri "$($vaultUri)/secrets/fabric-workspace-id" -Body $body</span>
                $secureStringValue = ConvertTo-SecureString -String $workspaceId -AsPlainText -Force
                Set-AzKeyVaultSecret -VaultName $azureKeyVaultName -Name <span class="hljs-string">"$($projectName)-workspace-id"</span>.ToLower().Replace(<span class="hljs-string">"_"</span>, <span class="hljs-string">"-"</span>) -SecretValue $secureStringValue
            }
            catch {
                Write-Host <span class="hljs-string">"Error al establecer el valor del secreto: $($_.Exception.Message)"</span> -ForegroundColor Red
                exit <span class="hljs-number">1</span> 
            }
        }        
    }
    <span class="hljs-keyword">else</span>
    {
        Write-Host <span class="hljs-string">"Creando área de trabajo $($projectName)..."</span>
        $body = @{
            <span class="hljs-string">"displayName"</span> = $projectName;
            <span class="hljs-string">"capacityId"</span> = $capacityId
        } | ConvertTo-Json -Depth <span class="hljs-number">10</span>

        <span class="hljs-keyword">try</span> {
            $response = Invoke-RestMethod -Headers $headerParams -Method POST -Uri $workspacesUri -Body $body -ContentType <span class="hljs-string">"application/json"</span>
            Write-Host <span class="hljs-string">"Área de trabajo creada con éxito:"</span> -ForegroundColor Green
            Write-Host <span class="hljs-string">"ID del área de trabajo: $($response.id)"</span> -ForegroundColor Green
            Write-Host <span class="hljs-string">""</span>
            $workspaceId = $response.id

            <span class="hljs-keyword">if</span>($azureKeyVault)
            {
                <span class="hljs-comment"># Establecer los valores de los secretos del Workspace al KeyVault</span>
                $body = @{
                    <span class="hljs-string">"value"</span> = $response.id
                } | ConvertTo-Json -Depth <span class="hljs-number">1</span>

                <span class="hljs-keyword">try</span>{
                    <span class="hljs-comment">#Invoke-RestMethod -Headers $headerParams -Method Put -Uri "$($vaultUri)/secrets/fabric-workspace-id" -Body $body</span>
                    $secureStringValue = ConvertTo-SecureString -String $response.id -AsPlainText -Force
                    Set-AzKeyVaultSecret -VaultName $azureKeyVaultName -Name <span class="hljs-string">"$($projectName)-workspace-id"</span>.ToLower().Replace(<span class="hljs-string">"_"</span>, <span class="hljs-string">"-"</span>) -SecretValue $secureStringValue
                }
                catch {
                    Write-Host <span class="hljs-string">"Error al establecer el valor del secreto: $($_.Exception.Message)"</span> -ForegroundColor Red
                    exit <span class="hljs-number">1</span> 
                }
            }
        } 
        catch {
            Write-Host <span class="hljs-string">"Error al crear el área de trabajo: $($_.Exception.Message)"</span> -ForegroundColor Red
            exit <span class="hljs-number">1</span> 
        }

    }

    <span class="hljs-comment">######################################################################################################################################</span>
    <span class="hljs-comment">## LAKEHOUSE</span>
    <span class="hljs-comment">##</span>
    <span class="hljs-comment">## Se crean tantos lakehouse como capas se hayan definido con la nomenclatura (projectName)_(layer)</span>
    <span class="hljs-comment">##</span>
    <span class="hljs-comment">######################################################################################################################################</span>

    Write-Host <span class="hljs-string">"Inicializando la creación de los lakehouses..."</span>
    Write-Host <span class="hljs-string">""</span>

    $lakehousesUri = <span class="hljs-string">"{0}/v1/workspaces/{1}/lakehouses"</span> -f $baseFabricUrl, $workspaceId
    $lakehousesList = Invoke-RestMethod -Headers $headerParams -ContentType $contentType -Method GET -Uri $lakehousesUri
    $lakehousesExistentes = @()

    <span class="hljs-keyword">try</span> {
        $lakehousesList = Invoke-RestMethod -Headers $headerParams -ContentType $contentType -Method GET -Uri $lakehousesUri
        <span class="hljs-keyword">if</span> (-<span class="hljs-keyword">not</span> $lakehousesList) 
        {
            Write-Host <span class="hljs-string">"La API no devolvió ningún lakehouse."</span> -ForegroundColor Yellow
        } 
        <span class="hljs-keyword">else</span> 
        {
            foreach ($lakehouse <span class="hljs-keyword">in</span> $lakehousesList.value) {
                $lakehousesExistentes += $lakehouse.displayName
            }
        }
    } 
    catch {
        Write-Host <span class="hljs-string">"Error al obtener los lakehouses: $($_.Exception.Message)"</span> -ForegroundColor Red
    }

    foreach ($layer <span class="hljs-keyword">in</span> $layers) 
    {
        $lakehouseName = <span class="hljs-string">"$projectName`_$layer`_lh"</span>

        <span class="hljs-keyword">if</span>($lakehousesExistentes -notcontains $lakehouseName)
        {
            Write-Host <span class="hljs-string">"Creando lakehouse $($lakehouseName)..."</span>
            $body = @{
                <span class="hljs-string">"displayName"</span> = $lakehouseName
            } | ConvertTo-Json -Depth <span class="hljs-number">10</span>

            <span class="hljs-keyword">try</span> {
                $response = Invoke-RestMethod -Headers $headerParams -Method POST -Uri $lakehousesUri -Body $body -ContentType <span class="hljs-string">"application/json"</span>
                Write-Host <span class="hljs-string">"Lakehouse $($lakehouseName) creado con éxito:"</span> -ForegroundColor Green
                Write-Host <span class="hljs-string">"ID del lakehouse: $($response.id)"</span> -ForegroundColor Green

                <span class="hljs-keyword">if</span>($azureKeyVault)
                {
                    $body = @{
                        <span class="hljs-string">"value"</span> = $response.id
                    } | ConvertTo-Json -Depth <span class="hljs-number">1</span>

                    <span class="hljs-keyword">try</span>{
                        $secureStringValue = ConvertTo-SecureString -String $response.id -AsPlainText -Force
                        Set-AzKeyVaultSecret -VaultName $azureKeyVaultName -Name <span class="hljs-string">"$projectName-$layer-lh-id"</span>.ToLower().Replace(<span class="hljs-string">"_"</span>, <span class="hljs-string">"-"</span>) -SecretValue $secureStringValue
                    }
                    catch {
                        Write-Host <span class="hljs-string">"Error al establecer el valor del secreto: $($_.Exception.Message)"</span> -ForegroundColor Red
                        exit <span class="hljs-number">1</span> 
                    }
                }
            } 
            catch {
                Write-Host <span class="hljs-string">"Error al crear el lakehouse: $($_.Exception.Message)"</span> -ForegroundColor Red
            }
        }
        <span class="hljs-keyword">else</span>{
            Write-Host <span class="hljs-string">"El lakehouse $($lakehouseName) ya existe."</span> -ForegroundColor Yellow
        }
        Write-Host <span class="hljs-string">""</span>
    }

}
<span class="hljs-comment"># Si la variable es false, generamos cada capa en un área de trabajo distinta</span>
<span class="hljs-keyword">else</span>
{
    <span class="hljs-comment">######################################################################################################################################</span>
    <span class="hljs-comment">## ÁREA DE TRABAJO</span>
    <span class="hljs-comment">##</span>
    <span class="hljs-comment">## Se comprueba si existe el área de trabajo. Si existe, obtenemos el workspaceId, sino, creamos el área de trabajo</span>
    <span class="hljs-comment">## y obtenemos el workspaceId.</span>
    <span class="hljs-comment">##</span>
    <span class="hljs-comment">######################################################################################################################################</span>

    Write-Host <span class="hljs-string">"El script está configurado para crear un área de trabajo para cada capa"</span>
    Write-Host <span class="hljs-string">"Inicializando la creación del área de trabajo..."</span>
    Write-Host <span class="hljs-string">""</span>

    <span class="hljs-comment"># Listamos las áreas de trabajo</span>
    $workspacesUri = <span class="hljs-string">"{0}/v1/workspaces"</span> -f $baseFabricUrl
    $workspacesList = Invoke-RestMethod -Headers $headerParams -ContentType $contentType -Method GET -Uri $workspacesUri
    foreach ($workspace <span class="hljs-keyword">in</span> $workspacesList.value) 
    {
        $workspacesDisponibles += $workspace.displayName
    }

    foreach ($layer <span class="hljs-keyword">in</span> $layers) {
        $WorkspaceName = <span class="hljs-string">"$projectName`_$layer"</span>

        <span class="hljs-keyword">if</span> ($workspacesDisponibles -contains $WorkspaceName) {
            Write-Host <span class="hljs-string">"El workspace $($WorkspaceName) ya existe. Se crearán los objetos sobre esta área de trabajo."</span>
            foreach ($workspace <span class="hljs-keyword">in</span> $workspacesList.value) 
            {
                <span class="hljs-keyword">if</span>($workspace.displayName -eq $WorkspaceName)
                {
                    $workspaceId = $workspace.id
                    Write-Host <span class="hljs-string">"Workspace Name: $($workspace.displayName)"</span> -ForegroundColor Cyan
                    Write-Host <span class="hljs-string">"Workspace ID: $($workspace.id)"</span> -ForegroundColor Cyan
                    Write-Host <span class="hljs-string">"Capacity ID: $($workspace.capacityId)"</span> -ForegroundColor Cyan
                    Write-Host <span class="hljs-string">""</span>
                }
            }

            <span class="hljs-keyword">if</span>($azureKeyVault)
            {
                <span class="hljs-comment"># Establecer los valores de los secretos del Workspace al KeyVault</span>
                $body = @{
                    <span class="hljs-string">"value"</span> = $workspace.id
                } | ConvertTo-Json -Depth <span class="hljs-number">1</span>

                <span class="hljs-keyword">try</span>{
                    $secureStringValue = ConvertTo-SecureString -String $workspaceId -AsPlainText -Force
                    Set-AzKeyVaultSecret -VaultName $azureKeyVaultName -Name <span class="hljs-string">"$($projectName)-$($layer)-workspace-id"</span>.ToLower().Replace(<span class="hljs-string">"_"</span>, <span class="hljs-string">"-"</span>) -SecretValue $secureStringValue
                }
                catch {
                    Write-Host <span class="hljs-string">"Error al establecer el valor del secreto: $($_.Exception.Message)"</span> -ForegroundColor Red
                    exit <span class="hljs-number">1</span> 
                }
            }
        } <span class="hljs-keyword">else</span> {
            Write-Host <span class="hljs-string">"Creando área de trabajo $($WorkspaceName)..."</span>
            $body = @{
                <span class="hljs-string">"displayName"</span> = $WorkspaceName;
                <span class="hljs-string">"capacityId"</span> = $capacityId
            } | ConvertTo-Json -Depth <span class="hljs-number">10</span>

            <span class="hljs-keyword">try</span> {
                $response = Invoke-RestMethod -Headers $headerParams -Method POST -Uri $workspacesUri -Body $body -ContentType <span class="hljs-string">"application/json"</span>
                Write-Host <span class="hljs-string">"Área de trabajo creada con éxito:"</span> -ForegroundColor Green
                Write-Host <span class="hljs-string">"ID del área de trabajo: $($response.id)"</span> -ForegroundColor Green
                Write-Host <span class="hljs-string">""</span>
                $workspaceId = $response.id

                <span class="hljs-keyword">if</span>($azureKeyVault)
                {
                    <span class="hljs-comment"># Establecer los valores de los secretos del Workspace al KeyVault</span>
                    $body = @{
                        <span class="hljs-string">"value"</span> = $response.id
                    } | ConvertTo-Json -Depth <span class="hljs-number">1</span>

                    <span class="hljs-keyword">try</span>{
                        <span class="hljs-comment">#Invoke-RestMethod -Headers $headerParams -Method Put -Uri "$($vaultUri)/secrets/fabric-workspace-id" -Body $body</span>
                        $secureStringValue = ConvertTo-SecureString -String $response.id -AsPlainText -Force
                        Set-AzKeyVaultSecret -VaultName $azureKeyVaultName -Name <span class="hljs-string">"$($projectName)-$($layer)-workspace-id"</span>.ToLower().Replace(<span class="hljs-string">"_"</span>, <span class="hljs-string">"-"</span>) -SecretValue $secureStringValue
                    }
                    catch {
                        Write-Host <span class="hljs-string">"Error al establecer el valor del secreto: $($_.Exception.Message)"</span> -ForegroundColor Red
                        exit <span class="hljs-number">1</span> 
                    }
                }
            } 
            catch {
                Write-Host <span class="hljs-string">"Error al crear el área de trabajo: $($_.Exception.Message)"</span> -ForegroundColor Red
                exit <span class="hljs-number">1</span> 
            }
        }


        <span class="hljs-comment">######################################################################################################################################</span>
        <span class="hljs-comment">## LAKEHOUSE</span>
        <span class="hljs-comment">##</span>
        <span class="hljs-comment">## Se crea el lakehouse correspondiente de la capa</span>
        <span class="hljs-comment">##</span>
        <span class="hljs-comment">######################################################################################################################################</span>

        Write-Host <span class="hljs-string">"Inicializando la creación del lakehouse..."</span>
        Write-Host <span class="hljs-string">""</span>

        $lakehousesUri = <span class="hljs-string">"{0}/v1/workspaces/{1}/lakehouses"</span> -f $baseFabricUrl, $workspaceId
        $lakehousesList = Invoke-RestMethod -Headers $headerParams -ContentType $contentType -Method GET -Uri $lakehousesUri
        $lakehousesExistentes = @()

        <span class="hljs-keyword">try</span> {
            $lakehousesList = Invoke-RestMethod -Headers $headerParams -ContentType $contentType -Method GET -Uri $lakehousesUri
            <span class="hljs-keyword">if</span> (-<span class="hljs-keyword">not</span> $lakehousesList) 
            {
                Write-Host <span class="hljs-string">"La API no devolvió ningún lakehouse."</span> -ForegroundColor Yellow
            } 
            <span class="hljs-keyword">else</span> 
            {
                foreach ($lakehouse <span class="hljs-keyword">in</span> $lakehousesList.value) {
                    $lakehousesExistentes += $lakehouse.displayName
                }
            }
        } 
        catch {
            Write-Host <span class="hljs-string">"Error al obtener los lakehouses: $($_.Exception.Message)"</span> -ForegroundColor Red
        }


        $lakehouseName = <span class="hljs-string">"$projectName`_$layer`_lh"</span>
        <span class="hljs-keyword">if</span>($lakehousesExistentes -notcontains $lakehouseName)
        {
            Write-Host <span class="hljs-string">"Creando lakehouse $($lakehouseName)..."</span>
            $body = @{
                <span class="hljs-string">"displayName"</span> = $lakehouseName
            } | ConvertTo-Json -Depth <span class="hljs-number">10</span>

            <span class="hljs-keyword">try</span> {
                $response = Invoke-RestMethod -Headers $headerParams -Method POST -Uri $lakehousesUri -Body $body -ContentType <span class="hljs-string">"application/json"</span>
                Write-Host <span class="hljs-string">"Lakehouse $($lakehouseName) creado con éxito:"</span> -ForegroundColor Green
                Write-Host <span class="hljs-string">"ID del lakehouse: $($response.id)"</span> -ForegroundColor Green

                <span class="hljs-keyword">if</span>($azureKeyVault)
                {
                    $body = @{
                        <span class="hljs-string">"value"</span> = $response.id
                    } | ConvertTo-Json -Depth <span class="hljs-number">1</span>

                    <span class="hljs-keyword">try</span>{
                        $secureStringValue = ConvertTo-SecureString -String $response.id -AsPlainText -Force
                        Set-AzKeyVaultSecret -VaultName $azureKeyVaultName -Name <span class="hljs-string">"$($projectName)-$($layer)-lh-id"</span>.ToLower().Replace(<span class="hljs-string">"_"</span>, <span class="hljs-string">"-"</span>) -SecretValue $secureStringValue
                    }
                    catch {
                        Write-Host <span class="hljs-string">"Error al establecer el valor del secreto: $($_.Exception.Message)"</span> -ForegroundColor Red
                        exit <span class="hljs-number">1</span> 
                    }
                }
            } 
            catch {
                Write-Host <span class="hljs-string">"Error al crear el lakehouse: $($_.Exception.Message)"</span> -ForegroundColor Red
            }
        }
        <span class="hljs-keyword">else</span>{
            Write-Host <span class="hljs-string">"El lakehouse $($lakehouseName) ya existe."</span> -ForegroundColor Yellow
        }
        Write-Host <span class="hljs-string">""</span>


    }
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Como obtener todas las configuraciones de la sesión de Spark + secretos de Azure Key Vault]]></title><description><![CDATA[Conocer como está configurada tu sesión de Spark es importante para debugging o para confirmar que los valores de los parámetros están bien configurados. Con el siguiente comando puedes obtener todas las configuraciones actuales de la sesión de Spark...]]></description><link>https://datagym.es/como-obtener-todas-las-configuraciones-de-la-sesion-de-spark-secretos-de-azure-key-vault</link><guid isPermaLink="true">https://datagym.es/como-obtener-todas-las-configuraciones-de-la-sesion-de-spark-secretos-de-azure-key-vault</guid><category><![CDATA[spark]]></category><category><![CDATA[PySpark]]></category><category><![CDATA[microsoftfabric]]></category><category><![CDATA[Azure Key Vault]]></category><dc:creator><![CDATA[Kilian Baccaro Salinas]]></dc:creator><pubDate>Thu, 16 Jan 2025 17:37:40 GMT</pubDate><content:encoded><![CDATA[<p>Conocer como está configurada tu sesión de Spark es importante para <em>debugging</em> o para confirmar que los valores de los parámetros están bien configurados. Con el siguiente comando puedes obtener todas las configuraciones actuales de la sesión de Spark</p>
<pre><code class="lang-python">spark.sparkContext.getConf().getAll()
</code></pre>
<ul>
<li><p><strong>spark.sparkContext</strong> accede al contexto de Spark de tu sesión</p>
</li>
<li><p><strong>getConf()</strong> devuelve las configuraciones de Spark</p>
</li>
<li><p><strong>getAll()</strong> devuelve una lista clave-valor con todas las configuraciones actuales de la sesión, incluyendo aquellas configuraciones por defecto y las que se hayan sobrescrito.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734469192221/c53efc97-0cb8-4414-8c26-ee7de79a3a89.png" alt class="image--center mx-auto" /></p>
<p>Para obtener un valor específico:</p>
<pre><code class="lang-python">spark.conf.get(<span class="hljs-string">"spark.driver.cores"</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737050701217/d2e36713-3159-44ad-a7e0-705930ab24cb.png" alt class="image--center mx-auto" /></p>
<p>Con este comando también podemos acceder configuraciones internas de Microsoft Fabric. Para ello, se utiliza el parámetro <strong>trident</strong>.</p>
<pre><code class="lang-python">spark.conf.get(<span class="hljs-string">"trident.tenant.id"</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737050739910/2492ec8f-d333-4325-adae-39441545075b.png" alt class="image--center mx-auto" /></p>
<p><code>trident</code>: Es un valor que representa una característica, servicio, o contexto específico en el que estás trabajando.<br />En entornos de <strong>Microsoft Fabric</strong>, "Trident" es un nombre en clave interno usado para referirse a ciertas funcionalidades de integración y configuración en Fabric, especialmente cuando se interactúa con servicios como OneLake, Key Vault, o Azure Active Directory.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> sempy.fabric <span class="hljs-keyword">as</span> fabric

default_lakehouse_id    = <span class="hljs-string">'No default lakehouse'</span> <span class="hljs-keyword">if</span> spark.conf.get(<span class="hljs-string">"trident.lakehouse.id"</span>) == <span class="hljs-string">''</span> <span class="hljs-keyword">else</span> spark.conf.get(<span class="hljs-string">"trident.lakehouse.id"</span>)
default_lakehouse_name  = <span class="hljs-string">'No default lakehouse'</span> <span class="hljs-keyword">if</span> spark.conf.get(<span class="hljs-string">"trident.lakehouse.name"</span>) == <span class="hljs-string">''</span> <span class="hljs-keyword">else</span> spark.conf.get(<span class="hljs-string">"trident.lakehouse.name"</span>)
notebook_item_id        = spark.conf.get(<span class="hljs-string">"trident.artifact.id"</span>)
notebook_item_name      = fabric.resolve_item_name(notebook_item_id)
pool_executor_cores     = spark.sparkContext.getConf().get(<span class="hljs-string">"spark.executor.cores"</span>)
pool_executor_memory    = spark.sparkContext.getConf().get(<span class="hljs-string">"spark.executor.memory"</span>)
pool_min_executors      = spark.sparkContext.getConf().get(<span class="hljs-string">"spark.dynamicAllocation.minExecutors"</span>)
pool_max_executors      = spark.sparkContext.getConf().get(<span class="hljs-string">"spark.dynamicAllocation.maxExecutors"</span>)
pool_number_of_nodes    = len(str(sc._jsc.sc().getExecutorMemoryStatus().keys()).replace(<span class="hljs-string">"Set("</span>,<span class="hljs-string">""</span>).replace(<span class="hljs-string">")"</span>,<span class="hljs-string">""</span>).split(<span class="hljs-string">", "</span>))
spark_app_name          = spark.conf.get(<span class="hljs-string">"spark.app.name"</span>)
workspace_id            = spark.conf.get(<span class="hljs-string">"trident.workspace.id"</span>)
workspace_name          = fabric.resolve_workspace_name(workspace_id)

print(<span class="hljs-string">f'default_lakehouse_id:   <span class="hljs-subst">{default_lakehouse_id}</span>'</span>)
print(<span class="hljs-string">f'default_lakehouse_name: <span class="hljs-subst">{default_lakehouse_name}</span>'</span>)
print(<span class="hljs-string">f'notebook_item_id:       <span class="hljs-subst">{notebook_item_id}</span>'</span>)
print(<span class="hljs-string">f'notebook_item_name:     <span class="hljs-subst">{notebook_item_name}</span>'</span>)
print(<span class="hljs-string">f'spark_app_name:         <span class="hljs-subst">{spark_app_name}</span>'</span>)
print(<span class="hljs-string">f'pool_executor_cores:    <span class="hljs-subst">{pool_executor_cores}</span>'</span>)
print(<span class="hljs-string">f'pool_executor_memory:   <span class="hljs-subst">{pool_executor_memory}</span>'</span>)
print(<span class="hljs-string">f'pool_min_executors:     <span class="hljs-subst">{pool_min_executors}</span>'</span>)
print(<span class="hljs-string">f'pool_max_executors:     <span class="hljs-subst">{pool_max_executors}</span>'</span>)
print(<span class="hljs-string">f'pool_number_of_nodes:   <span class="hljs-subst">{pool_number_of_nodes}</span>'</span>)
print(<span class="hljs-string">f'workspace_id:           <span class="hljs-subst">{workspace_id}</span>'</span>)
print(<span class="hljs-string">f'workspace_name:         <span class="hljs-subst">{workspace_name}</span>'</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735642835826/c5fde467-9102-40cb-8593-b601d3e0d4ae.png" alt class="image--center mx-auto" /></p>
<p>Puedes cambiar el valor de la configuración de la sesión de Spark utilizando el método <code>spark.conf.set</code>. Este método permite establecer configuraciones específicas para tu sesión actual de Spark</p>
<pre><code class="lang-python">spark.conf.set(<span class="hljs-string">"&lt;spark.conf.name&gt;"</span>, value)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737052036804/1dd43bef-4e3a-416b-80d6-f3a72a10c81d.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-azure-key-vault">Azure Key Vault</h1>
<p>Una de las buenas prácticas es almacenar los ids o datos sensibles en <strong>Azure Key Vault</strong> y acceder a ellos mediante notebook. A continuación os muestro un ejemplo:</p>
<p>En Azure tengo un Key Vault con la siguiente información</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735643829760/d48b81b0-42c7-46f6-8714-acab9840b418.png" alt class="image--center mx-auto" /></p>
<p>Para obtener el valor del secreto en un notebook utilizaremos NotebookUtils</p>
<pre><code class="lang-python">notebookutils.credentials.getSecret(<span class="hljs-string">'https://&lt;name&gt;.vault.azure.net/'</span>, <span class="hljs-string">'secret name'</span>)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735644834728/37432880-5a62-4b27-944c-fc462fc3b69d.png" alt class="image--center mx-auto" /></p>
<p>Como se puede ver, al acceder a un secreto aparece el valor [REDACTED]. Esto se debe a que, por razones de seguridad, <strong>Microsoft Fabric oculta automáticamente los valores de los secretos</strong> en las salidas de los notebooks para evitar exposiciones accidentales.</p>
<p>Aunque no se pueda ver el valor del secreto, si que podemos utilizarlo:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1735647178630/2186d9f4-44a6-49ee-9ca3-57bfa5f32eca.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item></channel></rss>