Procedimientos almacenados en Microsoft Fabric
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:
- Que un procedimiento sirve solo para meter
CREATE TABLEadentro. No. - 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:
CREATE OR ALTER— si el procedimiento no existe lo crea, y si existe lo reemplaza. Sin él tendrías el mismo problema que con elCREATE TABLE AS SELECT(CTAS): el segundoCREATE PROCEDUREfallaría por nombre repetido.dbo.— el esquema. En un Warehouse de Fabric todo vive en un esquema;dboes el de fábrica.sp_— convención heredada de SQL Server para nombrar procedimientos. No es obligatoria, pero hace obvio en el explorador qué es cada cosa.- El nombre de tres partes sigue funcionando desde adentro.
lakehouse_datablog.dbo.video_metricsse lee igual dentro del procedimiento que en una consulta suelta. - En
INSERT INTO ... SELECTlas columnas se emparejan por posición, no por nombre. El orden de la lista delINSERTmanda; por eso elSELECTya 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
DELETEserí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 PROCEDURE — hay 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:
- Efectos sobre los datos. El resultado principal y el que importa. El nuestro deja
metricas_por_anioreconstruida. No "devuelve" nada — cambia algo, que es distinto. - Uno o varios conjuntos de resultados. Si el procedimiento lleva un
SELECTadentro, 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. - 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ó. - 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:
- Refrescar una tabla —
TRUNCATE+INSERT. Lo que acabamos de hacer. - Carga incremental — un
MERGEque 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. - Depurar datos — un
UPDATEque normaliza valores mal escritos, unDELETEque quita registros de prueba. - 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.
- Encadenar pasos en orden — un procedimiento puede llamar a otros con
EXEC. Primero cargar dimensiones, después hechos, después las tablas agregadas. - 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:
- 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).
- 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:
- Un nombre.
sp_refrescar_metricas_por_anioes ahora un sustantivo del que puedes hablar en una reunión. Antes tenías que describir un query. - 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.
- Repetibilidad. Se ejecuta las veces que haga falta sin romperse.
- Se puede llamar desde afuera. Y esta es la que abre la siguiente puerta:
EXEC dbo.sp_refrescar_metricas_por_anioes 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:
- 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. - Manejo de errores (
TRY...CATCH) — para atrapar el fallo en vez de dejar que reviente. - 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 TABLEexige que la tabla ya exista. ElCREATE 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 conInvalid object namehasta que la vuelvas a crear. - Sin
OR ALTER, el segundoCREATE PROCEDUREfalla con el mismo error de nombre repetido que elCREATE TABLE AS SELECT(CTAS).CREATE OR ALTER PROCEDUREestá en la sintaxis documentada de Fabric y evita el cicloDROP+CREATE. - La CTE (
WITH) va antes delINSERT INTO, no después. El orden esWITH ... AS (...)y luegoINSERT INTO ... SELECT. Funciona dentro de un procedimiento en el Warehouse de Fabric. - En
INSERT INTO ... SELECTlas columnas se emparejan por posición, no por nombre. Si cambias el orden delSELECTsin cambiar la lista delINSERT, 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 SELECTeINSERT ... 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