Procedimientos almacenados en Microsoft Fabric


Video explicativo

Un Lakehouse y un Warehouse hacen lo mismo en el fondo: guardan datos. Ya montamos los dos.

En el Lakehouse aterrizaron los datos —videos y video_metrics— traídos con un Flujo de datos Gen2 (Power Query). Esos datos son dinámicos: si programas el flujo, cada semana llegan solos al lakehouse

Después, para el reporte, escribimos una consulta SQL que apuntaba al lakehouse, unía las dos tablas y agrupaba por año, y la materializamos como tabla física dentro del Warehouse con un CREATE TABLE AS SELECT (CTAS). De ahí salió metricas_por_anio, el modelo semántico en Direct Lake y el gráfico en un reporte.

Y ahí está el problema. El CREATE TABLE AS SELECT (CTAS) es fijo. Corrió una vez, el día que lo escribiste.

El Lakehouse se refresca solo; pero metricas_por_anio está en el warehouse y no se entera que hay nuevos datos porque es una tabla que se creo una sola vez con un CREATE TABLE AS SELECT (CTAS).

Entonces, ¿qué hacemos?

Lo primero no es escribir código: es entender qué tipo de cosa es eso que hay que hacer. Reemplazar esa tabla cada semana no es "quiero consultar algo y ya". Es una acción que hay que repetir, siempre igual, indefinidamente, para tener datos frescos. Eso es un trabajo.

Y un trabajo se puede empaquetar. El envase que lo guarda se llama procedimiento almacenado.

Este post construye ese envase con la pieza más pequeña posible, y de paso desactiva las dos confusiones más comunes:

  1. Que un procedimiento sirve solo para meter CREATE TABLE adentro. No.
  2. Que un procedimiento es "una vista con otro nombre". Tampoco — y hay una prueba de una línea que lo demuestra.

La promesa: no vas a aprender ni una sola cosa nueva de SQL. Vas a aprender tres palabras y a distinguir un trabajo de una consulta.

1. El problema: una tabla que nace una sola vez

Así materializamos la tabla en el post anterior:

CREATE TABLE dbo.metricas_por_anio AS
WITH ultimo_snapshot AS (
    SELECT
        video_id,
        view_count,
        like_count,
        ROW_NUMBER() OVER (PARTITION BY video_id ORDER BY snapshot_date DESC) AS rn
    FROM lakehouse_datablog.dbo.video_metrics
)
SELECT
    YEAR(v.published_at) AS anio,
    COUNT(*)             AS videos,
    SUM(m.view_count)    AS vistas_totales,
    SUM(m.like_count)    AS likes_totales
FROM lakehouse_datablog.dbo.videos AS v
JOIN ultimo_snapshot AS m
     ON v.video_id = m.video_id AND m.rn = 1
GROUP BY YEAR(v.published_at);

Ha pasado una semana y el Lakehouse ya tiene datos nuevos. Lo intuitivo es volver a correr lo que funcionó. Ejecuta otra vez ese mismo bloque, tal cual, sin cambiarle nada:

El problema que te vas a encontrar cuando ejecutes nuevamente la query es el siguiente:

There is already an object named 'metricas_por_anio' in the database.

Ese error es todo el post en una línea. El CREATE TABLE AS SELECT (CTAS) sabe crear, no sabe rehacer. Sirve exactamente una vez: la primera. Y nosotros necesitamos lo contrario — algo ejecutable todas las semanas, para siempre.

Lo que sí sabe rehacer

Vaciar y volver a llenar. Dos sentencias que ya conoces:

TRUNCATE TABLE dbo.metricas_por_anio;

WITH ultimo_snapshot AS (
    SELECT
        video_id,
        view_count,
        like_count,
        ROW_NUMBER() OVER (PARTITION BY video_id ORDER BY snapshot_date DESC) AS rn
    FROM lakehouse_datablog.dbo.video_metrics
)
INSERT INTO dbo.metricas_por_anio (anio, videos, vistas_totales, likes_totales)
SELECT
    YEAR(v.published_at),
    COUNT(*),
    SUM(m.view_count),
    SUM(m.like_count)
FROM lakehouse_datablog.dbo.videos AS v
JOIN ultimo_snapshot AS m
     ON v.video_id = m.video_id AND m.rn = 1
GROUP BY YEAR(v.published_at);

Córrelo dos o tres veces seguidas. Nunca falla, y la tabla queda siempre igual.

Fíjate en el cambio de piezas: desapareció el CREATE TABLE. La tabla ya existe y no hay que volver a crearla nunca; lo que hay que hacer es cambiarle el contenido. Guarda ese detalle, porque en la sección 5 es el que desmonta el malentendido más común sobre los procedimientos.

Pero antes, mira dónde está viviendo este SQL: en una pestaña del editor. Si cierras el navegador, se fue. Si mañana alguien pregunta cómo se arma metricas_por_anio, la respuesta honesta es "estaba en un query que tenía abierto".

Eso es lo que resuelve un procedimiento almacenado: le da a este trabajo un nombre y una casa.

2. Un procedimiento son tres palabras nuevas

Cuando alguien aprende SQL no se empieza por funciones de ventana. Se empieza por cinco cláusulas —SELECT, FROM, WHERE, GROUP BY, ORDER BY— que en realidad son una sola frase con huecos. Con esa frase ya respondes preguntas reales el primer día.

Los procedimientos tienen su equivalente, y es todavía más corto:

Pieza Para qué sirve
CREATE OR ALTER PROCEDURE <nombre> Bautizarlo
AS BEGIN ... END El envase donde va el SQL
EXEC <nombre> Llamarlo

Tres palabras. Y aquí está la diferencia con aprender SQL: las cinco cláusulas eran cinco conceptos nuevos —filtrar, agrupar, ordenar—. Un procedimiento no trae ningún concepto de SQL nuevo. El contenido del envase es el TRUNCATE + INSERT que acabas de escribir, sin cambiarle una coma.

Lo único que cambia es la categoría mental. Hasta ahora, todo lo que guardabas dentro de la base de datos describía algo: una tabla describe una estructura, una vista describe una consulta. Un procedimiento es lo primero que guardas que hace algo.

3. El procedimiento mínimo

Ejecuta esto en el Warehouse:

CREATE OR ALTER PROCEDURE dbo.sp_refrescar_metricas_por_anio
AS
BEGIN
    TRUNCATE TABLE dbo.metricas_por_anio;

    WITH ultimo_snapshot AS (
        SELECT
            video_id,
            view_count,
            like_count,
            ROW_NUMBER() OVER (PARTITION BY video_id ORDER BY snapshot_date DESC) AS rn
        FROM lakehouse_datablog.dbo.video_metrics
    )
    INSERT INTO dbo.metricas_por_anio (anio, videos, vistas_totales, likes_totales)
    SELECT
        YEAR(v.published_at),
        COUNT(*),
        SUM(m.view_count),
        SUM(m.like_count)
    FROM lakehouse_datablog.dbo.videos AS v
    JOIN ultimo_snapshot AS m
         ON v.video_id = m.video_id AND m.rn = 1
    GROUP BY YEAR(v.published_at);
END

Compáralo con el bloque anterior. Es el mismo SQL, con dos líneas encima y una debajo.

Detalles que vale la pena notar:

  1. CREATE OR ALTER — si el procedimiento no existe lo crea, y si existe lo reemplaza. Sin él tendrías el mismo problema que con el CREATE TABLE AS SELECT (CTAS): el segundo CREATE PROCEDURE fallaría por nombre repetido.
  2. dbo. — el esquema. En un Warehouse de Fabric todo vive en un esquema; dbo es el de fábrica.
  3. sp_ — convención heredada de SQL Server para nombrar procedimientos. No es obligatoria, pero hace obvio en el explorador qué es cada cosa.
  4. El nombre de tres partes sigue funcionando desde adentro. lakehouse_datablog.dbo.video_metrics se lee igual dentro del procedimiento que en una consulta suelta.
  5. En INSERT INTO ... SELECT las columnas se emparejan por posición, no por nombre. El orden de la lista del INSERT manda; por eso el SELECT ya no lleva alias.

Cuando termine, abre el explorador del Warehouse y busca la carpeta Stored Procedures. Ahí está, con nombre propio, dentro de la base de datos. Ya no vive en una pestaña.

4. El momento que importa: ejecútalo dos veces

Antes de ejecutarlo, vamos a romper la tabla a propósito. Si no, el EXEC no se ve hacer nada y no queda claro qué acaba de pasar.

-- 1. Vaciamos la tabla a propósito
DELETE FROM dbo.metricas_por_anio;

-- 2. Confirmamos que no queda nada
SELECT COUNT(*) AS filas FROM dbo.metricas_por_anio;

Devuelve 0. La tabla del reporte está vacía. Ahora el trabajo:

-- 3. Ejecutamos el trabajo
EXEC dbo.sp_refrescar_metricas_por_anio;
-- 4. Volvemos a mirar la tabla
SELECT * FROM dbo.metricas_por_anio ORDER BY anio;

Las filas están de vuelta.

Fíjate en el paso 3: el EXEC no devuelve ni una fila. No te muestra nada, y no es un fallo — es exactamente lo que hace un procedimiento. No devuelve un resultado: cambia algo. Por eso hace falta el paso 4, una consulta aparte, para ver qué dejó hecho.

Esa es la diferencia de fondo con una consulta normal. Un SELECT te da filas. Un EXEC te deja el mundo distinto. Guárdalo, porque en la sección 5 es lo que explica por qué un procedimiento no se puede meter dentro de otra consulta.

Ojo: los datos no se recuperaron, se recalcularon

Es tentador leer el paso 4 como "el procedimiento restauró lo que borré". No es eso, y la diferencia importa.

Las filas que volvieron son nuevas: el INSERT las calculó otra vez desde videos y video_metrics en el Lakehouse. Salieron idénticas solo porque el origen no cambió entre medias. No hay ninguna copia de seguridad de por medio.

Eso es lo que hace seguro todo el patrón: metricas_por_anio es una tabla derivada. No contiene información propia — es el resultado de una fórmula aplicada a otros datos. Es desechable por diseño, y por eso puedes vaciarla y rehacerla mil veces sin miedo. Si el Lakehouse desapareciera, el EXEC no traería nada de vuelta.

⚠️ Vaciar una tabla a propósito solo es seguro cuando es derivada. Si esa tabla fuera el único sitio donde vive ese dato, el DELETE sería definitivo y ningún procedimiento la traería de vuelta.

Y un detalle que confirma lo que hay dentro del envase: si en vez de DELETE FROM hicieras DROP TABLE dbo.metricas_por_anio, el procedimiento fallaría con Invalid object name. Porque el TRUNCATE exige que la tabla exista, y el procedimiento no sabe crearla. Nunca hubo un CREATE TABLE ahí dentro.

Ahora sí: ejecútalo dos veces

Con el trabajo ya entendido, corre el EXEC otra vez. Y una tercera.

EXEC dbo.sp_refrescar_metricas_por_anio;

SELECT * FROM dbo.metricas_por_anio ORDER BY anio;

Mismo resultado, siempre. Sin errores, sin filas duplicadas, sin "ya existe un objeto con ese nombre".

Compáralo con el CREATE TABLE AS SELECT (CTAS) del principio, que a la segunda vez te arrojaba el error: "ya existe un objeto con ese nombre". Ese es el cambio: el trabajo que metimos dentro del envase se puede repetir, y el del CREATE TABLE AS SELECT (CTAS) no.

Quien hace esa diferencia es el TRUNCATE. No está ahí por gusto: es lo que hace que la segunda ejecución no se sume a la primera. Quítalo y el procedimiento seguiría siendo un procedimiento perfectamente válido... que te duplica los datos cada semana, sin dar ningún error.

Y ojo con la conclusión fácil: el envase no aporta esa propiedad. Un procedimiento que por dentro solo hiciera un INSERT se puede llamar igual con EXEC, y cada llamada añadiría filas nuevas. Que un trabajo se pueda repetir sin daño no viene del CREATE PROCEDUREhay que diseñarlo así.

5. Una vista empaqueta una consulta; un procedimiento empaqueta un trabajo

Aquí es donde mucha gente saca la conclusión equivocada. Nuestro ejemplo rehace una tabla, así que es tentador guardar la idea como "un procedimiento sirve para materializar tablas". No es eso.

De hecho, fíjate en un detalle: en todo este post no hay un solo CREATE TABLE dentro de un procedimiento. El CREATE TABLE fue el problema —lo que falló al ejecutarlo dos veces—, no la solución. Dentro del envase solo hay TRUNCATE e INSERT.

La otra confusión es más sutil, porque una vista también "empaqueta SQL con un nombre". La diferencia está en qué empaqueta cada una: una vista empaqueta una consulta; un procedimiento empaqueta un trabajo.

5.1 La pregunta que traza la línea

Ante cualquier SQL que escribas, pregúntate:

¿Esto cambia algo, o solo lo mira?

Esa pregunta separa dos mundos:

Lo que necesitas La herramienta
Mirar los datos, siempre igual Una vista. Se recalcula sola cada vez que la consultas, no ocupa espacio, no hay nada que programar.
Mirar los datos filtrando por un valor distinto cada vez Una función con valores de tabla, o un procedimiento con parámetros
Cambiar algo: escribir, borrar, actualizar, mover Un procedimiento. Aquí no hay alternativa: una vista es incapaz de modificar datos.

La regla es asimétrica y conviene decirla completa: si cambias algo, estás obligado a usar un procedimiento. Si solo lees, casi siempre una vista es la respuesta más simple — y elegir un procedimiento sería complicarte la vida.

5.2 La prueba de una línea: el FROM

Hay una forma de comprobar que una vista y un procedimiento son cosas de naturaleza distinta, y cabe en dos consultas. Pruébalas en tu Warehouse.

La primera va contra la tabla que mantiene nuestro procedimiento. Funciona, evidentemente:

-- Una tabla va en un FROM sin problema
SELECT anio, vistas_totales
FROM dbo.metricas_por_anio
WHERE vistas_totales > 10000;

La segunda va contra el procedimiento:

-- Esto NO funciona. No es sintaxis válida.
SELECT * FROM dbo.sp_refrescar_metricas_por_anio;

Y aquí está la clave: si en la primera consulta cambiaras la tabla por una vista, seguiría funcionando igual. Cambia el FROM por el nombre de una vista y el motor ni se inmuta.

La razón no es un capricho. Una vista es una tabla — una tabla lógica, calculada al vuelo, pero una tabla al fin: tiene columnas fijas, tipos fijos, y devuelve exactamente un conjunto de filas.

Por eso el motor la trata igual que a dbo.metricas_por_anio: la puede poner en un FROM, unirla con otra tabla, filtrarla y anidarla dentro de vistas más grandes.

Un procedimiento no es una tabla. Es una acción. Puede no devolver ninguna fila (el nuestro no devuelve ninguna: solo cambia datos), puede devolver un conjunto, puede devolver tres conjuntos distintos con columnas distintas, y puede hacer cosas diferentes según el parámetro que le pases. El motor no tiene forma de saber de antemano qué estructura tendría eso, así que no puede tratarlo como una fuente de datos.

De ahí sale la regla práctica: si necesitas que el resultado alimente otra consulta, necesitas una vista. Si necesitas cambiar datos, necesitas un procedimiento.

5.3 Entonces, ¿qué te devuelve un procedimiento?

Si no se puede poner en un FROM, vale la pena saber qué produce realmente. Cuatro cosas:

  1. Efectos sobre los datos. El resultado principal y el que importa. El nuestro deja metricas_por_anio reconstruida. No "devuelve" nada — cambia algo, que es distinto.
  2. Uno o varios conjuntos de resultados. Si el procedimiento lleva un SELECT adentro, esas filas te llegan a la pantalla (o a la aplicación que lo llamó). Pero llegan como salida final: no las puedes encadenar en otra consulta.
  3. Parámetros de salida. La sintaxis documentada de Fabric admite parámetros marcados como OUTPUT, para devolver valores sueltos — cuántas filas se cargaron, a qué hora terminó.
  4. Errores. Si algo sale mal, el procedimiento lanza el error a quien lo llamó. Suena obvio, pero es la base de todo lo que hace confiable a un procedimiento, y es el tema de próximos post.

5.4 Trabajos que no crean ninguna tabla

Para terminar de desmontar la idea de "procedimiento = crear tablas", estos son casos reales, y en ninguno aparece un CREATE TABLE:

  1. Refrescar una tablaTRUNCATE + INSERT. Lo que acabamos de hacer.
  2. Carga incremental — un MERGE que actualiza las filas que cambiaron e inserta las nuevas, sin tocar el resto. Es lo que usarías cuando la tabla es grande y rehacerla entera sale caro.
  3. Depurar datos — un UPDATE que normaliza valores mal escritos, un DELETE que quita registros de prueba.
  4. Archivar histórico — mover las filas antiguas a una tabla de archivo y quitarlas de la tabla activa. Dos operaciones que tienen que ir juntas.
  5. Encadenar pasos en orden — un procedimiento puede llamar a otros con EXEC. Primero cargar dimensiones, después hechos, después las tablas agregadas.
  6. Validar antes de actuar — comprobar una condición y detenerse si no se cumple, en lugar de arrasar con la tabla.

Los dos últimos son los que rompen del todo la idea de "un procedimiento es una consulta con nombre": ahí ya no hay una consulta, hay varias operaciones con lógica entre ellas. Eso es exactamente lo que una vista jamás podría contener.

5.5 Entonces, ¿por qué nuestro caso sí necesita un procedimiento?

Porque la salida barata está cerrada. Lo natural sería no materializar nada y dejar metricas_por_anio como vista — se recalcularía sola y no habría trabajo que programar. En Fabric eso no se puede, por dos razones que se suman:

  1. Direct Lake (el método para lllevar los datos a un modelo semántico y aun reporte) solo lee tablas Delta físicas, nunca vistas. Si la conviertes en vista, el modelo semántico pierde Direct Lake y cae a Import (refresco manual) o DirectQuery (más lento).
  2. El Warehouse de Fabric no soporta vistas materializadas. No es que sean peor opción: no existen. Están en la lista oficial de comandos no soportados.

Sumadas dejan un solo camino: la tabla tiene que ser física, y alguien tiene que reconstruirla periódicamente. Reconstruirla cambia algo, así que por la regla de arriba es territorio de procedimiento — obligatoriamente.

6. Qué ganaste al empaquetarlo

El envase parece trivial —tres palabras— pero te dio cuatro cosas concretas:

  1. Un nombre. sp_refrescar_metricas_por_anio es ahora un sustantivo del que puedes hablar en una reunión. Antes tenías que describir un query.
  2. Una casa. El código vive dentro de la base de datos. Cualquiera con acceso al Warehouse puede abrirlo y leer cómo se arma esa tabla, sin pedirte el archivo.
  3. Repetibilidad. Se ejecuta las veces que haga falta sin romperse.
  4. Se puede llamar desde afuera. Y esta es la que abre la siguiente puerta: EXEC dbo.sp_refrescar_metricas_por_anio es una sola línea que un proceso programado puede ejecutar cada lunes a las 4 AM sin que tú estés delante.

Las tres primeras son comodidad. La cuarta es la que convierte una tabla congelada en una tabla viva.

7. Lo que este procedimiento todavía no hace

Sería deshonesto terminar sin decir qué le falta, porque le falta bastante.

Fíjate en el orden de las operaciones: primero TRUNCATE, después INSERT. Ahora imagina que un lunes el Lakehouse llega vacío por un problema en la fuente, o que el INSERT falla a mitad de camino. El TRUNCATE ya se ejecutó. La tabla queda vacía, el reporte de Power BI queda en blanco, y no te enteras hasta que alguien te escribe.

Ese escenario tiene solución, y no es complicada, pero son conceptos aparte:

  1. Transacciones (BEGIN TRAN / COMMIT / ROLLBACK) — para que el borrado y la carga sean una sola operación indivisible: o se aplican los dos o no se aplica ninguno.
  2. Manejo de errores (TRY...CATCH) — para atrapar el fallo en vez de dejar que reviente.
  3. Bitácora de ejecuciones — una tabla donde el procedimiento anota cada corrida con su resultado, para que un fallo nunca sea silencioso.

Ninguna de las tres es parte de lo que hace que algo sea un procedimiento. Son lo que hace que un procedimiento sea confiable, que es otro eje distinto.

Y después queda la pieza que da sentido a todo: el proceso programado que ejecuta este EXEC cada lunes sin que nadie abra el navegador.

8. Mi conclusión

Cuando escuché "procedimiento almacenado" por primera vez, asumí que era territorio avanzado — cosa de DBA, de las que se aprenden después. Resultó ser lo contrario: es de las piezas más simples de SQL. Tres palabras alrededor de código que ya sabías escribir.

Lo que me llevo es la distinción que ordena todo el resto. Una vista empaqueta una consulta: la puedes meter en un FROM, encadenar, reutilizar como si fuera una tabla. Un procedimiento empaqueta un trabajo: no va en ningún FROM, porque no es una tabla — es una acción con nombre. Cuando dudes entre uno y otro, la pregunta es siempre la misma: ¿esto cambia algo, o solo lo mira?

Gotchas

  • El procedimiento no crea la tabla, la rellena. TRUNCATE TABLE exige que la tabla ya exista. El CREATE TABLE AS SELECT (CTAS) sigue siendo necesario una vez, para el nacimiento; el procedimiento se encarga del mantenimiento. Si borras la tabla, el procedimiento falla con Invalid object name hasta que la vuelvas a crear.
  • Sin OR ALTER, el segundo CREATE PROCEDURE falla con el mismo error de nombre repetido que el CREATE TABLE AS SELECT (CTAS). CREATE OR ALTER PROCEDURE está en la sintaxis documentada de Fabric y evita el ciclo DROP + CREATE.
  • La CTE (WITH) va antes del INSERT INTO, no después. El orden es WITH ... AS (...) y luego INSERT INTO ... SELECT. Funciona dentro de un procedimiento en el Warehouse de Fabric.
  • En INSERT INTO ... SELECT las columnas se emparejan por posición, no por nombre. Si cambias el orden del SELECT sin cambiar la lista del INSERT, no hay error — hay datos en la columna equivocada. Es el fallo más silencioso de este patrón.
  • El editor SQL aborta el lote completo tras un error. Si pones varias sentencias en la misma pestaña y una falla, las siguientes no se ejecutan, aunque no dependan de la que falló. Al depurar, ejecuta las verificaciones por separado.

Referencia oficial

  • Superficie T-SQL del Warehouse de Fabric (incluye la lista de comandos no soportados): https://learn.microsoft.com/en-us/fabric/data-warehouse/tsql-surface-area
  • CREATE PROCEDURE — sintaxis para Microsoft Fabric: https://learn.microsoft.com/en-us/sql/t-sql/statements/create-procedure-transact-sql?view=fabric
  • Ingesta de datos en el Warehouse con T-SQL (CREATE TABLE AS SELECT e INSERT ... SELECT): https://learn.microsoft.com/en-us/fabric/data-warehouse/ingest-data-tsql
  • Tablas temporales en el Warehouse de Fabric: https://learn.microsoft.com/en-us/fabric/data-warehouse/temp-tables