Teoría BBDD

Funciones inline en SQL Server.

Si vienes del artículo anterior sobre procedimientos almacenados vs funciones, ya sabes que no todas las funciones son iguales ni todas son igual de bienvenidas en producción. Hoy vamos a entrar en el terreno de las que sí merecen un sitio en entornos serios, las funciones inline con valores de tabla, o inline TVF.

Porque sí, en SQL Server también hay funciones que se comportan bien, no matan el rendimiento y hasta pueden ser aliadas del optimizador. Eso sí, como siempre en SQL Server, hay letra pequeña. Y no es precisamente corta.

¿Qué es exactamente una función inline?

Una función inline con valores de tabla es un objeto de base de datos que devuelve una tabla como resultado, definida a partir de una única instrucción SELECT. La sintaxis es clara: no hay BEGIN…END, no hay tablas variables intermedias ni lógica procedural. Solo una expresión SELECT que define el resultado.

Un ejemplo básico:

La clave aquí está en el hecho de que la función no tiene cuerpo procedural. El RETURN devuelve directamente el SELECT, sin necesidad de declarar una variable tipo tabla ni usar bloques de control. Y eso le encanta al optimizador de consultas.

¿Por qué son tan diferentes del resto?

La principal ventaja de las funciones inline es que se expanden dentro del plan de ejecución de la consulta que las llama. Es decir, se comportan como si hubieras escrito el SELECT directamente en la consulta. Esto permite al optimizador generar un único plan coherente, hacer estimaciones de cardinalidad más precisas y aplicar filtros, joins o incluso reescrituras sin perder información.

No hay saltos de contexto, no hay opacidad. Todo se ve. Todo se puede optimizar. Todo fluye.

Esto contrasta brutalmente con las funciones escalares o las funciones con valores de tabla multisentencia, donde el optimizador pierde visibilidad, rompe el plan y acaba tirando de valores fijos o estimaciones aleatorias dignas de una quiniela.

En resumen, si necesitas encapsular lógica reutilizable que devuelve datos en forma de tabla y quieres mantener el rendimiento, la inline TVF es tu opción.

Buenas prácticas: lo que conviene hacer

Usar funciones inline no solo es una cuestión de sintaxis correcta. También hay que saber cómo diseñarlas para que sean eficientes, reutilizables y mantenibles. Aquí van algunas prácticas que realmente marcan la diferencia:

Primero, mantén la lógica centrada en una única operación clara. Si necesitas múltiples pasos, joins complejos, condiciones opcionales o lógica condicional, es probable que estés forzando demasiado el diseño y que deberías estar en un procedimiento o en una vista indexada (si tienes valor para ello).

Segundo, evita usar funciones inline como simples extractores de columnas. Si solo encapsulas un SELECT de una tabla sin añadir valor, estás generando una capa innecesaria que complica los planes y aporta poco. Úsalas cuando encapsules lógica real: filtrados, joins relevantes, condiciones específicas.

Tercero, documenta los parámetros con claridad. SQL Server no impone muchas restricciones, pero el que venga detrás (que probablemente seas tú dentro de seis meses) agradecerá saber qué espera exactamente cada parámetro y cómo afecta al resultado.

Por último, cuidado con los TOP y los ORDER BY. En funciones inline, el ORDER BY solo es válido dentro de un TOP… pero eso no significa que respete el orden en la consulta final. El optimizador puede decidir hacer lo que le venga en gana si no hay ORDER BY explícito en la consulta externa. Que no te sorprenda.

Casos de uso donde brillan

Hay escenarios donde una función inline brilla con luz propia. Uno de los más frecuentes: filtros complejos reutilizables. Supón que tienes una lógica de negocio que define qué pedidos se consideran “activos”, basada en múltiples condiciones, fechas, flags y columnas calculadas. Puedes encapsular esa lógica en una función inline y usarla como si fuera una subconsulta parametrizada.

Otro caso clásico: reportes o dashboards parametrizados. Si usas Power BI o cualquier herramienta de reporting que genere consultas dinámicas, puedes exponer funciones inline como “vistas parametrizadas” que reducen la complejidad del modelo y mejoran el mantenimiento.

Y por supuesto, también son útiles en procesos ETL que necesitan aplicar filtros reutilizables en múltiples pasos sin replicar lógica por todas partes.

Lo que NO debes hacer (aunque la tentación sea fuerte)

El hecho de que las funciones inline se comporten tan bien no significa que sean inmunes al mal diseño. De hecho, hay errores recurrentes que siguen apareciendo incluso entre DBAs con experiencia.

Uno muy común es usar funciones inline con un JOIN interno a una tabla de millones de filas sin tener en cuenta filtros exteriores. Como la función se expande, esa tabla puede aparecer en cada ejecución de forma inesperada y generar planes desastrosos.

Otro clásico, parámetros mal definidos o con valores opcionales que no se gestionan bien. SQL Server no permite parámetros opcionales en funciones, así que muchos intentan simularlo con NULL y condiciones tipo (@Param IS NULL OR Columna = @Param). Esto degrada los planes de ejecución, impide la indexación eficaz y rompe la estimación de cardinalidad. Una joya.

Y uno más, pensar que por ser una función inline, siempre va a ir rápido. Si la lógica interna es compleja, hace joins mal indexados o usa filtros poco selectivos, el plan puede seguir siendo un desastre. El que se vea no significa que sea bueno.

Comparación real: subconsulta vs función inline

Para los escépticos, vamos con un ejemplo sencillo.

Subconsulta tradicional

Esto se puede convertir en una función inline así:

Y luego usarla así:

¿La diferencia? En un entorno donde se reutiliza la lógica o se combina con otras condiciones, encapsularla en una función inline puede mejorar la legibilidad y el mantenimiento. Y con OUTER APPLY, se comporta como una subconsulta correlacionada, pero con visibilidad total en el plan.

Eso sí, no uses CROSS APPLY si no estás 100% seguro de que todas las combinaciones producirán resultado. Te lo recordarás a las 3 de la mañana.

Conclusión

Las funciones inline son una de esas herramientas que, bien usadas, te hacen la vida más fácil y el código más limpio. Mal usadas, solo complican las cosas sin aportar rendimiento ni claridad. No son la solución a todo, pero en el conjunto correcto de circunstancias, pueden sustituir a vistas, subconsultas o incluso procedimientos ligeros con mucha más flexibilidad.

Y sobre todo, te permiten encapsular lógica sin pagar el peaje que sí tienen otras funciones. Como siempre en SQL Server, no es solo cuestión de saber que existen, sino de saber cuándo y cómo usarlas. Porque sí, hay funciones buenas. Solo hay que reconocerlas.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima! 

Publicado por Roberto Carrancio en Cloud, Rendimiento, SQL Server, 0 comentarios

Procedimientos almacenados y Funciones. SQL Server Basics

En SQL Server, hay ciertos conceptos que uno se encuentra tarde o temprano, tanto si empieza en el mundo de las bases de datos como si ya lleva un tiempo peleándose con entornos reales. Los procedimientos almacenados y las funciones son dos de ellos. Ambos permiten encapsular lógica en T-SQL, ambos pueden reutilizarse y ambos tienen su lugar. Pero no son lo mismo. Y tratarlos como si lo fueran es una fuente constante de confusión, errores y, en muchos casos, de mal rendimiento.

Este artículo no busca hacer una comparativa teórica más. Aquí vamos a ver en qué se diferencian realmente, cuándo conviene usar uno u otro, qué limitaciones tiene cada uno y qué errores son más comunes al trabajar con ellos. Si estás empezando con SQL Server, esto es una base fundamental. Y si ya llevas tiempo en el oficio, probablemente te encuentres con más de una práctica que conviene revisar.

Entendiendo la diferencia de base

Aunque en la teoría tanto procedimientos como funciones son objetos reutilizables de código T-SQL, en la práctica se comportan de forma muy distinta, sobre todo cuando hablamos de rendimiento, transaccionalidad, side effects y cómo se integran en una consulta.

Un procedimiento almacenado (Stored Procedure) es una secuencia de instrucciones T-SQL que se ejecuta como un bloque, puede devolver conjuntos de resultados, manejar errores, abrir y cerrar transacciones y, en general, actuar con bastante libertad. Como un EXEC.

Una función (Function), por otro lado, devuelve siempre un valor. Puede ser escalar (un solo valor) o con valores de tabla (inline o multisentencia). Se puede usar dentro de consultas como si fuera una expresión más. Y ahí empiezan los problemas.

Procedimientos almacenados: el «hombre para todo» de SQL Server

Los procedimientos almacenados llevan décadas siendo el núcleo de la lógica de negocio en la base de datos. Y aunque ahora todo el mundo quiere microservicios, orquestadores y que “la lógica viva fuera del SQL”, lo cierto es que los procedimientos siguen siendo una herramienta potentísima.

Pueden contener lógica compleja, trabajar con múltiples tablas, ejecutar DML (INSERT, UPDATE, DELETE), manejar cursores (sí, también hay gente que todavía los usa), y lo más importante: pueden tener efectos secundarios. De hecho, están pensados para eso.

Además, permiten declarar variables, manejar errores con TRY…CATCH, lanzar mensajes de error personalizados con RAISERROR o THROW, abrir transacciones, hacer ROLLBACK, hacer COMMIT, y todo esto mientras devuelven múltiples conjuntos de resultados si hace falta. No es elegante, pero es potente. Como un martillo neumático.

Funciones: elegantes, encapsuladas… y peligrosas

Las funciones escalar (Scalar-valued Functions, SVF) son atractivas. Parecen limpias, reusables, encapsulan lógica, se integran perfectamente en consultas… y luego te destruyen el rendimiento. Así, sin más.

El problema es que las SVF tradicionales se evalúan por cada fila de la consulta donde se usan. Y no, no se «inlinean» automáticamente (al menos no hasta SQL Server 2019 con INLINE y aún así, con limitaciones). Esto significa que si tienes una tabla con 10 millones de filas y usas una función escalar en el SELECT, estás ejecutando esa función… 10 millones de veces. Lo que se dice un plan brillante.

¿Y las funciones con valores de tabla? Aquí hay dos tipos: las inline TVF (Returns Table) y las multisentencia (Returns @Table con BEGIN…END). Las inline funcionan como una vista parametrizada, y eso está bien. Se comportan bien con el optimizador. Las multisentencia, en cambio, generan estimaciones de cardinalidad basura y planes horribles, porque el optimizador no tiene ni idea de lo que pasa dentro. Pero tú tampoco, así que estáis empatados.

Principales diferencias técnicas y funcionales

A nivel práctico, aquí van algunas diferencias que conviene no olvidar cuando te enfrentas al dilema función vs procedimiento:

  • Un procedimiento no se puede usar en un SELECT. No devuelve un valor directamente. Se ejecuta con EXEC.
  • Una función no puede tener side effects: no puede modificar datos (excepto si es una CLR, que eso es otra historia). Tampoco puede llamar a procedimientos, usar TRY…CATCH, ni realizar cambios en el estado de la base de datos.
  • Las funciones escalar tradicionales matan el paralelismo de las consultas y generan estimaciones de coste que parecen salidas de un generador aleatorio.
  • Los procedimientos pueden devolver múltiples conjuntos de resultados. Las funciones no.
  • El plan de ejecución de una función escalar no se ve dentro del plan de ejecución de la consulta (hasta SQL Server 2019 sin INLINE). Así que diagnosticar problemas es como hacer cirugía a ciegas.
  • Las funciones inline TVF sí se comportan bien en los planes de ejecución, porque se expanden como si fueran vistas. Y eso las hace usables en entornos serios.

Rendimiento: el gran elefante en la sala

Nadie crea funciones para que vayan lentas, pero casi todas acaban siéndolo. La causa es bien conocida: cada ejecución de una función escalar es un contexto separado. Si haces un SELECT que usa una función escalar sobre una tabla de millones de filas, estás pidiendo al motor que salte de contexto millones de veces. En vez de vectorizar el acceso, va fila a fila. Y sí, esto se nota.

En cambio, un procedimiento bien diseñado que trabaje con conjuntos puede procesar la misma lógica de forma mucho más eficiente. Y, sobre todo, puedes verlo venir en el plan de ejecución. No te lo oculta como si fuera un secreto de Estado.

Desde SQL Server 2019 se introdujo la inlining de funciones escalares (Scalar UDF Inlining) para funciones marcadas como SCHEMABINDING, sin lógica demasiado compleja y sin referencias a objetos no deterministas. Pero si la función tiene más de una variable o usa GETDATE(), ya estás fuera del juego.

Así que sí, puedes confiar en la mejora… pero más te vale leer la letra pequeña.

Casos de uso típicos (sin liarla)

Entonces, ¿cuándo usar uno y cuándo otro? Vamos al grano.

Usa procedimientos almacenados cuando necesitas lógica compleja, efectos secundarios, manejar errores, o quieres ejecutar bloques de operaciones que se comporten como transacciones. Ideal para llamadas desde aplicaciones, cargas ETL, tareas programadas, o cualquier situación donde el código no esté embebido en un SELECT.

Usa funciones inline con valores de tabla cuando necesites lógica reutilizable que se integre en consultas y no afecte negativamente al rendimiento. Si puedes expresarlo como una única SELECT y devolver una tabla sin BEGIN…END, adelante. Son tus amigas.

Evita funciones escalares tradicionales en cualquier sitio donde importe el rendimiento. O al menos, haz pruebas serias antes de meterlas en producción y que te explote todo el lunes por la mañana.

La trampa de la reutilización mal entendida

Una de las razones por las que se abusa de las funciones es el deseo de reutilizar lógica. Loable, pero mal ejecutado. Si una función contiene una consulta pesada que se va a ejecutar por cada fila, lo que tienes no es reutilización, es una trampa mortal.

A veces es mejor duplicar una expresión compleja dentro de dos consultas que encapsularla en una función escalar. Lo segundo parece más limpio, sí. Pero cuando empiecen las quejas de rendimiento, lo primero será más fácil de optimizar.

La clave está en saber cuándo sacrificar limpieza por control. Y no siempre la opción «bonita» es la correcta.

Conclusión

La diferencia entre procedimientos almacenados y funciones no es solo cuestión de sintaxis, sino de mentalidad. Si quieres control, potencia y libertad, usa procedimientos. Si necesitas expresividad dentro de consultas y sabes lo que estás haciendo, usa funciones inline. Pero si tu objetivo es reutilizar lógica sin entender el coste, acabarás con un sistema lento, opaco y lleno de problemas difíciles de rastrear.

Y si aún dudas, recuerda esto: un procedimiento bien hecho es un aliado; una función escalar mal usada es una bomba de relojería con apariencia de buen código. Tú decides qué prefieres tener en tu entorno.

Publicado por Roberto Carrancio en Cloud, SQL Server, 0 comentarios

Sargabilidad: el arte de no cabrear al optimizador

En SQL Server hay conceptos que separan al junior entusiasta del profesional que ha pasado noches enteras mirando planes de ejecución con el ceño fruncido. La sargabilidad es uno de ellos. Es esa palabra fea que no encontrarás en la RAE, pero que puede hacer que tus consultas vuelen… o se arrastren como un SELECT * sin índice por una tabla de 500 millones de filas.

Si llevas tiempo escribiendo T-SQL y nunca te has parado a pensar en si tus consultas son sargables o no, probablemente estés dejando rendimiento encima de la mesa. Y si trabajas con datos en serio, sabes que eso es inaceptable.

Qué es la sargabilidad (de verdad)

Sargabilidad viene del acrónimo SARG: Search ARGument-able. Es decir, que el motor de base de datos puede usar un índice para resolver la consulta de forma eficiente, sin escanear toda la tabla como si no hubiera mañana.

En SQL Server, una consulta es sargable cuando el predicado (la condición del WHERE, JOIN, HAVING, etc.) puede aprovechar un índice para filtrar datos directamente, sin tener que procesar fila por fila. En otras palabras: si el optimizador ve que puede usar un índice de forma directa, lo hará. Si no, te prepara un bonito Index Scan o Table Scan con aroma a cuello de botella.

Un ejemplo básico:

  • Sargable
  • No sargable

La diferencia es simple pero letal. En el primer caso, SQL Server puede usar un índice sobre la columna Nombre. En el segundo, como le has puesto una función encima, el optimizador no puede buscar directamente. Tiene que aplicar la función a cada fila y luego comparar. Y claro, eso escala tan bien como un script que hace DELETE sin WHERE.

Las funciones son el enemigo (cuando no sabes usarlas)

El caso anterior ilustra la regla de oro de la sargabilidad: si transformas la columna del predicado, estás condenado a un escaneo. Las funciones como LEFT, SUBSTRING, CONVERT, DATEDIFF, COALESCE o incluso simples operadores matemáticos, rompen la posibilidad de usar índices si se aplican sobre la columna.

Veamos otro clásico:

Esto es tan común como ineficiente. Lo correcto sería:

Y sí, es más feo. Pero el plan de ejecución será infinitamente más feliz. Y tú también, cuando no tengas que explicar por qué la consulta tarda 30 segundos con un índice que, supuestamente, estaba para eso.

ISNULL y COALESCE: los rompeíndices silenciosos

Uno de esos consejos que se repite como mantra en el mundo SQL es: «no uses funciones en columnas si quieres mantener la sargabilidad». Y, como todo buen mantra, es útil… pero no siempre cierto.

Tomemos el caso de ISNULL() y COALESCE(). Ambos permiten sustituir valores nulos por un valor por defecto. A efectos prácticos:

Parecen lo mismo, ¿no? Pero el optimizador de SQL Server no los trata igual. En la demo real que estás viendo, con un índice sobre DisplayName, la diferencia es cristalina.

ISNULL(): sorprendentemente sargable

Cuando usas ISNULL(DisplayName, ») = ‘Juan’, SQL Server aplica un Index Seek sin despeinarse. No le molesta la función. ¿Por qué? Porque el motor puede predecir exactamente lo que hace ISNULL() en este contexto: evalúa la columna, y si es NULL, la reemplaza por ». Como el predicado sigue siendo evaluable con un valor constante, puede derivarlo internamente como un predicado OR:

Y esa es la clave. Porque si escribes ese predicado tú directamente, es igual de eficiente y, de paso, más claro:

Mismo Index Seek. Misma eficiencia. Pero sin necesidad de funciones.

COALESCE(): aquí sí se complica

Ahora bien, si usas COALESCE(DisplayName, ») = ‘Juan’, el plan cambia. SQL Server ya no puede garantizar que la expresión sea determinista y sencilla de evaluar. Más que nada porque podría haber más de dos argumentos o que el orden de evaluación tuviera implicaciones semánticas. El resultado: Index Scan.

Así que no, ISNULL() y COALESCE() no son intercambiables en el WHERE (importante esto) cuando te importa el rendimiento. En teoría devuelven lo mismo, en la práctica afectan al plan de ejecución de formas distintas.

El OR: ese sospechoso habitual

Otro rompe-sargabilidad por excelencia es el OR, que puede obligar a escanear incluso si una de las condiciones sí es sargable. Imagina esto:

A menos que tengas un índice compuesto que cubra ambas columnas, el optimizador suele rendirse y tirar de escaneo. 

Así que ya sabes, si usas con frecuencia este tipo de filtros con OR asegurate de tener un índice compuesto bien creado.

Las columnas calculadas como tabla de salvación

A veces no hay forma humana de evitar una función sobre la columna. En esos casos, una solución decente (aunque no milagrosa) puede ser usar columnas calculadas indexadas. Si sabes que todo el mundo va a seguir escribiendo:

Puedes añadir una columna calculada persistente:

Y ahora sí, el predicado puede ser sargable, porque la función ya se calculó y se indexó. No es magia negra, es simplemente darle al optimizador algo que pueda usar sin pensar.

El caso especial del LIKE

El LIKE también puede ser sargable o no, dependiendo del patrón. Esta es sargable:

Esta no:

Si el comodín está al principio, el índice no sirve. No puede buscar «desde» ningún valor. El patrón tiene que permitir una búsqueda por rango, como ‘Juan%’ o incluso ‘J%’. Todo lo demás se traduce en escaneo.

Aquí no hay mucha escapatoria salvo usar Full-Text Search si de verdad necesitas búsquedas internas. O pensar si realmente necesitas buscar así, que a veces el problema no es técnico sino de requisitos mal planteados.

Cuando CONVERT(DATE, datetime) sí es sargable (sí, lo has leído bien)

Aunque llevamos un buen rato diciendo, con razón, que aplicar funciones sobre columnas rompe la sargabilidad, hay una excepción curiosa (y bastante útil) que merece su propio rincón en este artículo: la conversión de un datetime a DATE usando CONVERT o CAST.

Mira este caso donde CreationDate es un campo DATETIME:

Lo lógico sería pensar: “acabo de meterle una función a la columna del WHERE, esto va directo a un Index Scan, ¿verdad?”. Pues no. SQL Server es capaz de usar un Index Seek, incluso con esa conversión.

¿Por qué? Porque el motor es más listo de lo que a veces creemos. Internamente, evalúa que convertir un datetime a date reduce la precisión pero no cambia la semántica de ordenación, y por tanto puede generar un predicado de rango equivalente: desde 2010-06-05 00:00:00.000 hasta justo antes del 2010-06-06. Si hay un índice sobre la columna CreationDate, lo usa. Sin mirar atrás.

Ahora bien: esto no lo convierte en buena práctica universal.

Porque si haces esto:

El motor no te va a perdonar. Ahí sí hay Index Scan, porque ya estás forzando una conversión de tipo y de formato, y encima con ambigüedad regional. No hay forma de hacer un seek con eso.

Así que, como en todo, hay que ser precisos: CONVERT(DATE, datetime) es una excepción válida a la regla general, y SQL Server lo optimiza sin necesidad de escanear. Pero eso no quiere decir que cualquier CONVERT o CAST sea seguro.

JOINs y sargabilidad: el otro lado del desastre

La sargabilidad no solo importa en los WHERE. También afecta a los JOIN. Si haces cosas como:

Estás rompiendo toda posibilidad de usar un índice sobre B.Codigo. La función LEFT en el lado derecho del JOIN es como ponerle una venda en los ojos al optimizador. Y luego esperar que encuentre el camino solo. Mejor no.

Reescribe la lógica o introduce columnas auxiliares si hace falta, pero no pongas funciones en las condiciones de unión. A menos que tu hobby sea revisar planes de ejecución a las tres de la mañana.

Cómo detectar consultas no sargables

El mejor chivato es el plan de ejecución. Si ves Index Scan o Table Scan donde debería haber un Seek, es que tienes un problema de sargabilidad. También puedes usar las estadísticas de ejecución o SET STATISTICS IO ON para ver si se están leyendo más páginas de las que tocan.

Además, en SQL Server 2019+ puedes usar Intelligent Query Processing que, en algunos casos, mitiga errores de diseño como este… pero no es una excusa para escribir mal. Es un parche, no una solución.

Y si quieres una ayuda más visual, herramientas como SQL Sentry Plan Explorer o el propio SSMS con su plan gráfico te permiten ver rápidamente si estás tirando índices a la basura.

Scan no siempre es sinónimo de error (aunque duela admitirlo)

Otro punto clave: un Index Scan no siempre es el villano. Sí, lo hemos demonizado durante años, pero como todo DBA que ha peleado con planes de ejecución sabe, hay ocasiones en las que el escaneo es simplemente lo más eficiente.

Por ejemplo, si tu predicado devuelve un porcentaje alto de filas (pongamos, más del 25-30% de la tabla), al optimizador le puede salir más a cuenta ir a buscar los datos de una vez que intentar ser selectivo con un Seek y acabar con mil Key Lookup de regalo.

Lo mismo pasa cuando el índice cubre poco y el motor tendría que hacer lookups constantes para recoger el resto de columnas. En ese caso, el coste de los seeks individuales más los lookups supera con creces el coste de un buen escaneo. El optimizador no es tonto: elige lo que más conviene a nivel global de coste estimado.

Y aquí viene lo peligroso: si tu consulta está perfectamente escrita y sargable, pero el plan muestra un Scan, no asumas automáticamente que está mal. Revisa el plan, el número de filas estimadas, y el acceso a columnas. Lo que queremos evitar son los scans necesarios por diseño deficiente, no los que el motor elige porque son lo más razonable.

Conclusión

La sargabilidad no es un capricho del optimizador ni un tema menor. Es la base sobre la que se construye el rendimiento de cualquier sistema OLTP que merezca la pena. Ignorarla es como ir a una entrevista con el pantalón del pijama: técnicamente puedes, pero no deberías.

Escribir consultas sargables exige disciplina, conocimiento y cierta humildad. Hay que dejar de pensar en lo que «funciona» y empezar a pensar en lo que escala. Y cuando la diferencia entre una consulta sargable y otra que no lo es se mide en millones de lecturas lógicas… no hay excusa.

Así que la próxima vez que te enfrentes a un WHERE, piensa como el optimizador. No le pongas trabas, no le escondas la columna tras una función, y no le pidas milagros con predicados imposibles. Dale lo que necesita. Y si aún así se queja, entonces sí: culpemos al cardinality estimator.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima! 

Publicado por Roberto Carrancio en Cloud, Índices, Rendimiento, SQL Server, 0 comentarios

Canalizaciones por nombre (Named Pipes) en SQL Server

Durante años, las canalizaciones por nombre (Named Pipes) han sido uno de esos protocolos de red que viven en las sombras de TCP/IP. Están ahí, aparecen en las configuraciones del SQL Server Configuration Manager, y de vez en cuando algún alma valiente pregunta si debería activarlas “por si acaso”. Y no, por si acaso no es un argumento válido en administración de sistemas, y mucho menos en bases de datos. Vamos a analizar qué son exactamente, cómo funcionan y en qué escenarios podrían tener sentido… si es que alguno queda en 2025.

¿Qué demonios son las canalizaciones por nombre?

Antes de decidir si las activamos o las dejamos durmiendo el sueño de los justos, conviene entender qué son. Las canalizaciones con nombre son un mecanismo de comunicación interprocesos (IPC) heredado del glorioso mundo de Windows NT. Permiten que dos procesos, incluso en máquinas diferentes, se comuniquen a través de una ruta virtual. En SQL Server, representan uno de los protocolos disponibles para aceptar conexiones de cliente.

Mientras que TCP/IP utiliza direcciones IP y puertos, Named Pipes utiliza una sintaxis como \\.\pipe\sql\query, y en red sería algo tipo \\Servidor\pipe\sql\query. Esta vía de comunicación puede ser más rápida en entornos muy controlados y locales (léase: conexión directa entre un cliente y un servidor dentro de la misma LAN y sin cuellos de botella), pero en la práctica actual… hay que hilar muy fino para justificar su uso.

TCP/IP vs Canalizaciones por nombre: ¿por qué seguimos hablando de esto?

La realidad es que TCP/IP es el protocolo estándar y recomendado para conexiones SQL Server, tanto en producción como en desarrollo. Es más robusto, más flexible, y más preparado para escenarios reales con múltiples capas de red, firewalls, balanceadores, NATs y demás fauna moderna.

Entonces, ¿por qué SQL Server sigue ofreciendo Named Pipes como opción? Por compatibilidad. Porque aún hay sistemas legacy que las usan. Y porque a Microsoft le cuesta soltar lastre tanto como a cualquiera que aún mantiene SQL Server 2008 en alguna esquina de su datacenter “temporal”.

Hay entornos donde, por motivos históricos o arquitecturas muy específicas, se configuraron conexiones a SQL Server utilizando Named Pipes. En esos casos, sí, deshabilitarlas podría romper algo. Pero esa es la excepción, no la norma.

¿Cuándo tienen sentido las Named Pipes?

Vale, no todo es blanco o negro. Hay escenarios, pocos pero existentes, donde las canalizaciones por nombre pueden tener cierta ventaja:

  • Conexiones locales (cliente y servidor en la misma máquina): En algunos benchmarks internos de Microsoft (de hace más de una década nada menos), se observó que el rendimiento de Named Pipes en conexiones locales era ligeramente superior al de TCP/IP. Pero, francamente, si ese es tu cuello de botella, tienes problemas mayores.
  • Autenticación integrada en entornos Windows puros: Las canalizaciones por nombre pueden simplificar ciertos escenarios de autenticación integrada en entornos totalmente controlados por Active Directory. Pero otra vez: TCP/IP también lo hace sin problemas.
  • Entornos legacy que no quieres (o puedes) tocar: Si tienes una aplicación que explícitamente se conecta usando np: o configuraciones hardcoded de canalizaciones con nombre, y no puedes modificarla… entonces no queda otra que habilitarlas.
  • Solución de problemas puntuales: En algunos casos, cuando el acceso por TCP/IP falla misteriosamente (DNS, puertos bloqueados, fuegos en el CPD…), usar Named Pipes puede servir para diagnosticar si SQL Server sigue vivo y coleando.

Pero si tu escenario no cae en alguno de estos puntos, las Named Pipes sobran.

Cómo funcionan realmente las canalizaciones por nombre

Cuando habilitas Named Pipes en SQL Server, el motor escucha en una canalización concreta: \\.\pipe\sql\query por defecto. El cliente debe conectarse utilizando ese nombre. Lo que muchos no saben es que esto no solo requiere que el cliente conozca la sintaxis, sino también que la resolución de nombres esté bien configurada (en red), y que no haya firewalls bloqueando el tráfico correspondiente.

Además, en entornos remotos, el protocolo puede comportarse de forma bastante torpe: mayor latencia en la negociación, más complejidad en el tráfico de red, y más exposición a errores difíciles de diagnosticar. Si te suena a dolor de cabeza… es porque lo es.

¿Qué pasa si dejo las canalizaciones por nombre activadas “por si acaso”?

Esto es como dejar todas las ventanas de casa abiertas por si te olvidas las llaves. En teoría podría ayudarte, pero en la práctica te estás exponiendo innecesariamente. Habilitar las canalizaciones por nombre sin necesitarlas abre un vector de ataque innecesario (sí, también hablamos de superficie de ataque), complica el troubleshooting de conexiones, y puede provocar que clientes mal configurados intenten conectar usando este protocolo en lugar de TCP/IP.

Por si fuera poco, cuando están activadas, SQL Server puede priorizarlas en la cadena de protocolos, lo que lleva a situaciones surrealistas como que un cliente tarde más de la cuenta en conectarse porque está intentando usar Named Pipes antes que TCP/IP.

Lo peor de todo es que, en redes modernas, usar Named Pipes puede ralentizar la conexión en lugar de mejorarla. Así que eso de activarlas para “ganar velocidad” es un mito que ya deberíamos haber dejado atrás con el disquete.

¿Y si las necesito, cómo las activo (o desactivo)?

Si a pesar de todo necesitas activar Named Pipes, el proceso es sencillo, pero no inmediato. Desde SQL Server Configuration Manager, accede al protocolo correspondiente bajo SQL Server Network Configuration, y ahí puedes habilitarlas o deshabilitarlas. Necesitarás reiniciar el servicio SQL Server para que los cambios tengan efecto.

Además, si vas a usarlas, asegúrate de configurar también correctamente la cadena de conexión en el cliente, usando el prefijo np: para forzar que se utilicen Named Pipes.

Y, por supuesto, monitoriza. No asumas que todo va mejor solo porque te conectaste. Comprueba latencias, errores, y la experiencia real del usuario. Porque los milagros no vienen de las canalizaciones.

¿Qué protocolo prioriza SQL Server?

El orden de los protocolos es importante. SQL Server Native Client (o el driver OLE DB / ODBC que uses) sigue un orden al intentar conectar, salvo que lo fuerces. Ese orden se define también en SQL Server Configuration Manager, y puedes modificarlo.

Si dejas Named Pipes activado y en primer lugar, el cliente intentará primero por ahí antes de probar TCP/IP. Si Named Pipes no está disponible o hay problemas de red, el tiempo de espera puede incrementarse de forma absurda. ¿Te suenan esas conexiones que tardan 20 segundos solo en conectar? Pues eso.

Conclusión

Las canalizaciones por nombre son como el fax, aún existen, aún funcionan, y hay quien defiende que tienen utilidad. Pero en la mayoría de escenarios modernos con SQL Server, no hay ninguna razón técnica sólida para tenerlas activadas si no se están usando.

No aportan ventaja real frente a TCP/IP, complican el diagnóstico, abren superficie de ataque, y pueden ralentizar la conexión. Si tu entorno las requiere, adelante, pero hazlo con conocimiento de causa. Si no sabes para qué las necesitas, es que no las necesitas.

Apaga las Named Pipes, reinicia tu SQL Server y duerme tranquilo sabiendo que has reducido complejidad innecesaria.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de LinkedIn y un canal de YouTube a los que te puede unir. ¡Hasta la próxima!

Publicado por Roberto Carrancio en SQL Server, 0 comentarios

¿Cuál es el problema que quieres resolver?

Si hay una pregunta que debería resonar en la cabeza de todo DBA antes de tocar nada es esta: ¿cuál es el problema que quieres resolver?. Parece de sentido común, pero basta con ver la cantidad de “optimizaciones” fallidas que nos encontramos a diario para entender que muchos prefieren actuar primero y pensar después.

No es cuestión de señalar a los juniors recién llegados. También he visto seniors con experiencia aplicar consejos a ciegas porque lo leyeron en un blog (sí, incluso en este). Optimizar sin tener claro el objetivo es la mejor forma de acabar con más índices de los que puedes contar y un servidor que corre peor que antes.

Cuando dos índices son un problema

Hace un tiempo publiqué un artículo en el blog que mostraba un comportamiento extraño en SQL Server: si creas dos índices con los mismos campos pero en orden inverso, el optimizador puede entrar en bucle y empeorar el rendimiento.

No es que uno de los índices fuera inútil, es que la mera coexistencia de ambos confundía al motor. El resultado eran planes de ejecución incoherentes, lecturas disparadas y un servidor que parecía castigar la osadía de haber creado ese segundo índice.

Lo comprobé en mi propio laboratorio y sí, el problema existe en distintas versiones, desde 2014 hasta 2022. No hay explicación oficial, pero el efecto es evidente: un índice que no se usa puede penalizar tanto como uno mal diseñado.

La moraleja es clara: no todo lo que parece buena idea lo es. Crear un índice extra “por probar” puede desatar un problema que no estaba ahí antes.

El optimizador también se equivoca

A ese escenario extraño se suma otro que me he encontrado demasiadas veces: el optimizador sugiriendo crear un índice que ya existe.

¿Cómo puede ser? Muy simple: lo que realmente falla son las estadísticas. Si los datos de distribución están obsoletos, el optimizador cree que no tiene un índice adecuado y recomienda uno nuevo. Y como “buenos DBAs obedientes”, lo creamos. Resultado: en el mejor de los casos acabamos con dos índices redundantes, el motor igual de confundido y el verdadero problema (las estadísticas) intacto. En el peor, con el problema que hemos descrito en el apartado anterior y con las estadísticas aún desactualizadas.

La historia se repite: en vez de preguntarnos qué queremos resolver, nos dejamos llevar por la sugerencia de turno y complicamos aún más el entorno.

Optimizar sin objetivo: el problema más común

Ambos casos ponen de manifiesto lo mismo: aplicar soluciones sin entender el problema real es un deporte de riesgo.

Ya lo comentamos en este otro artículo, los entornos de bases de datos no se arreglan con recetas universales ni con “scripts definitivos” de Internet. Cada entorno es distinto, con sus cargas, sus decisiones históricas y sus miserias. Copiar y pegar soluciones genéricas es más postureo técnico que administración seria.

La optimización sin objetivo es como disparar con los ojos cerrados, con suerte no das en nada, pero la mayor parte del tiempo acabas dañando algo.

La pregunta de Brent Ozar

Aquí entra la insistencia de Brent Ozar en su mantra favorito y que da titulo a este artículo:
¿Cuál es el problema que quieres resolver?

Antes de crear índices, actualizar estadísticas o aplicar hints a lo loco o cambiar configuraciones en el servidor hay que responder a esa pregunta. ¿El problema es el tiempo de respuesta? ¿Consumo excesivo de CPU? ¿Bloqueos? ¿O solo que alguien quiere que una consulta de 3 segundos baje a 0,3 porque sí?

Uno de esos es un problema técnico. El otro es un problema de expectativas. Y mezclar ambos lleva a dedicar horas a optimizar lo que no importa mientras lo urgente sigue sin atenderse.

Si no mides, no sabes si mejoras o empeoras

Aquí está el punto que muchos olvidan, si no sabes que tienes un problema y no mides antes, no puedes saber si lo que hiciste fue una mejora o un desastre.

Un cambio sin línea base no es optimización, es puro azar. Y en producción, jugar al azar es abrir la puerta a que mañana alguien te pregunte por qué el sistema está peor y no tengas respuesta. Sin métricas antes y después, la optimización es indistinguible del postureo.

Y ojo, también tienes que medir cuando todo funciona bien, sin esa linea base es imposible saber si de verdad algo va mal o es solo la sensación de los usuarios. Ya sabemos que adoran decir: “esto ahora va más lento” sin pruebas.

Diagnóstico antes de cirugía

La única forma de trabajar en serio es diagnosticar antes de tocar. Eso implica medir, observar y recopilar evidencia. Query Store, planes de ejecución, Extended Events, DMVs… las herramientas están ahí, pero hay que usarlas con criterio.

El motor se equivoca, los consejos de Internet también, y a veces hasta nuestras “buenas prácticas” son contextuales y no universales. La diferencia entre un DBA que arregla problemas y otro que los multiplica está en hacerse siempre la pregunta incómoda antes de optimizar.

Conclusión

Cambiar configuraciones del servidor, crear índices o aplicar consejos sin diagnosticar el problema real no es optimización, es azar.

SQL Server no premia las soluciones automáticas. Premia el análisis. Y la diferencia entre un DBA que arregla cosas y otro que las rompe está en hacerse siempre esa pregunta incómoda antes de tocar nada: ¿cuál es el problema que quiero resolver?

Si no sabes que tienes un problema y no lo mides antes, no tienes forma de saber si tu cambio fue mejora o desastre. Y en bases de datos, la duda casi siempre juega en tu contra.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de LinkedIn y un canal de YouTube a los que te puede unir. ¡Hasta la próxima!

Publicado por Roberto Carrancio en Cloud, Rendimiento, SQL Server, 0 comentarios

Casos de uso comunes de Row-Level Security (RLS)

Ya sabemos qué es Row-Level Security (RLS) y cómo se configura. Sabemos que se basa en funciones, en contexto y en algo muy parecido al sentido común. Pero el verdadero reto no es activarlo: es aplicarlo bien según el escenario real que tienes delante.

Aquí no vamos a hablar de casos teóricos ni de pruebas de laboratorio. Vamos a entrar en los tres escenarios más comunes que nos encontramos en proyectos reales, de esos con datos de verdad, usuarios impacientes y arquitecturas heredadas.

RLS interno en la organización: seguridad basada en Active Directory

Este es el caso más limpio y agradecido: una empresa con usuarios autenticados en Active Directory, accediendo a la base de datos con sus propias credenciales de Windows (sí, esto existe y funciona cuando se hace bien). Aquí el RLS puede aprovechar directamente la identidad del usuario para filtrar datos.

El patrón habitual consiste en mapear usuarios o grupos de AD con permisos o ámbitos de acceso, normalmente usando el ORIGINAL_LOGIN() o SUSER_SNAME() para identificar al usuario, y compararlo contra una tabla de mapeo como:

La función RLS consulta esta tabla y determina qué datos puede ver el usuario actual. Algo así:

Este modelo funciona bien si los usuarios acceden directamente con sus identidades, y si hay una buena sincronización entre AD y los datos internos. Pero cuidado: este patrón no es válido si la aplicación usa un login compartido, porque entonces ORIGINAL_LOGIN() siempre devuelve lo mismo.

También requiere un mantenimiento cuidadoso de la tabla de mapeo. Si te olvidas de añadir un nuevo usuario, no verá nada. Y eso está bien. Lo preocupante sería que viera de más.

Este patrón es ideal para:

  • Reporting interno
  • Aplicaciones corporativas en intranets
  • Escenarios con SSRS o Power BI con Kerberos bien configurado (sí, existen)

RLS en aplicaciones con un único login SQL y múltiples usuarios

Este es, con diferencia, el escenario más habitual. Una aplicación que se conecta a la base de datos con un solo login SQL Server (por ejemplo, AppUser) y desde ahí da servicio a miles de usuarios distintos, cada uno con sus propios permisos y ámbito de acceso.

Desde el punto de vista de SQL Server, todos los accesos vienen del mismo login. Por tanto, el nombre del usuario no sirve para nada. Aquí RLS solo tiene sentido si usamos SESSION_CONTEXT() para establecer el contexto del usuario en cada sesión.

  1. El patrón correcto es:
  2. La aplicación identifica al usuario (login web, token, sesión, lo que sea).
  3. Determina su TenantId, su ámbito de acceso, si es admin, etc.
  4. Al abrir la conexión (o justo después), ejecuta
    EXEC sp_set_session_context N’TENANT_ID’, 17;
    EXEC sp_set_session_context N’ES_ADMINISTRADOR’, 0;
  5. La función RLS consulta ese contexto y decide si mostrar o no cada fila.

Funciona. Es limpio. Es seguro. Pero solo si te aseguras de que cada conexión establece su contexto correctamente. Y eso implica entender bien cómo funciona el pool de conexiones. Porque si alguien reutiliza una conexión con el contexto de otro usuario, acabas sirviendo datos del usuario A al usuario B. Y eso no es un bug: es un incidente de seguridad.

Este patrón requiere no solo código en la aplicación para establecer el contexto en cada conexión sino validación en la base de datos para asegurarse de que el contexto existe y funciones RLS bien diseñadas (inline, sin llamadas externas ni joins innecesarios). Si además añades una capa de control en la aplicación tendrás doble seguridad.

Este escenario, bien montado, es el pan de cada día en escenarios como aplicaciones web SaaS, portales internos de gestión multiusuario o APIs que consumen datos con identidad propia (pero login compartido)

RLS en aplicaciones multi-tenant con varios clientes

Aquí las cosas se complican. Estamos ante una única aplicación que sirve a múltiples clientes independientes, y todos los datos viven en las mismas tablas, separados lógicamente por una columna TenantId. Este patrón es muy eficiente… hasta que alguien mete la pata y un SELECT devuelve datos de otro tenant.

RLS aquí no es una opción. Es una necesidad.

El patrón es similar al del punto anterior: la aplicación se conecta con un login compartido, pero establece el TenantId como contexto al inicio de cada sesión. La diferencia es que ahora ese TenantId define los límites legales del acceso a datos.

Es decir, ya no es un usuario con menos o más permisos. Es otro cliente, con sus propios datos. Si algo se cuela, no solo es un error, es una violación de privacidad con consecuencias legales.

La función RLS, por tanto, debe ser clara, directa, sin adornos:

Y la política se aplica a todas las tablas sensibles. Además, conviene reforzar la integridad con defaults:

Y con predicados de bloqueo (de eso hablaremos pronto) para evitar updates o deletes entre tenants.

Este patrón funciona, pero solo si se impide a la aplicación cambiar el contexto a lo loco, el TenantId se valida contra los permisos del usuario web y todas las rutas de acceso pasan por código que inicializa el contexto.

Además, como ya he remarcado, este escenario es el más sensible, y por tanto donde más cuidado debemos tener. Si no haces RLS aquí, estás confiando en que la aplicación nunca se equivoque. Buena suerte con eso.

Predicados de bloqueo en RLS

Cuando implementamos RLS, todo el mundo se queda encantado con el FILTER PREDICATE: ese que impide que el usuario vea filas que no le tocan. Pero la mayoría se olvida, o no quiere saber, que eso no impide que puedan hacer INSERT, UPDATE o DELETE sobre datos que no deberían tocar. Así de simple. Y así de peligroso. Ahí es donde entran los BLOCK PREDICATES, el otro 50% de la seguridad que RLS nos ofrece.

Un BLOCK PREDICATE define si una operación de modificación de datos está permitida. Se puede aplicar a:

  • AFTER INSERT (cuando se insertan nuevas filas)
  • AFTER UPDATE (cuando se actualiza una fila)
  • BEFORE UPDATE (para impedir que se cambie el valor de clave de filtrado, por ejemplo TenantId)
  • BEFORE DELETE (para validar que el usuario puede borrar esa fila)

Y aquí viene la parte buena, estos bloqueos se aplican antes de que la operación se realice. Si no se cumple la condición, la instrucción falla con error. Sin necesidad de triggers, sin lógica adicional. De forma declarativa y centralizada. Como debe ser.

Vamos con un ejemplo.

Supongamos que tenemos una tabla Facturas y ya tenemos aplicado un FILTER PREDICATE que impide ver facturas de otros tenants. Muy bien. Pero si no hacemos nada más, el usuario podría hacer esto:

INSERT INTO Facturas (FacturaId, TenantId, Importe) VALUES (9999, 42, 1500);

Y colar datos con el TenantId de otro cliente. Luego, como no puede verlas, ni se entera. Pero ya están en tu base. ¿Te hace ilusión explicárselo a legal? A mí tampoco.

Para evitarlo, añadimos un BLOCK PREDICATE:

Y luego lo aplicamos así:

Esto bloquea los INSERT y DELETE por defecto. Si queremos bloquear los updates también (y deberíamos), lo hacemos explícitamente:

Y si no queremos que nadie cambie el TenantId de una fila, lo añadimos como BEFORE UPDATE:

Esto impide que alguien coja una factura legítima y le cambie el TenantId para que desaparezca mágicamente de la vista de su propietario original. Muy creativo. Muy ilegal.

Consideraciones importantes con los bloqueos de RLS

Al usar BLOCK PREDICATE, las operaciones que no cumplan la condición fallan con un error genérico del tipo:

Esto es intencionado. No dice qué falló ni por qué, porque revelar detalles de seguridad no es buena idea. Pero esto también significa que debes controlar bien los errores en la aplicación, o los usuarios se encontrarán mensajes confusos.

Y como siempre, las funciones deben ser INLINE para no perder rendimiento. Y deben ser simples, sin joins ni condiciones complejas. Esto se ejecuta en cada fila afectada. Si metes un SELECT TOP 1 dentro, estás haciendo cosas malas y mereces tus bloqueos (los tuyos, no los de RLS).

Conclusión

RLS es una herramienta poderosa, pero no mágica. Su utilidad real depende de cómo te conectas, de quién eres en la base de datos y de cómo estableces el contexto. Un mismo mecanismo puede servir tanto para proteger acceso interno como para blindar un modelo multi-tenant.

Pero si no eliges el patrón correcto para tu escenario, estás poniendo una alarma en la puerta mientras dejas la ventana abierta. Y eso no es seguridad: es postureo.

Ya hemos cubierto lo esencial de RLS: qué es, cómo se implementa, cómo se adapta a escenarios reales y cómo no confiar ciegamente en que el FILTER PREDICATE lo hace todo. Porque si no añades bloqueos, estás dejando que el usuario escriba donde no puede leer. Y eso es como cerrar la puerta pero dejar las ventanas abiertas: solo te protege si el atacante es educado.

Usar RLS bien no es complicado. Pero requiere tomárselo en serio. Establecer contexto. Validar accesos. Aplicar filtros y bloqueos. Y sobre todo: asumir que si no lo defines tú, alguien lo hará por ti. Probablemente mal.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de LinkedIn y un canal de YouTube a los que te puede unir. ¡Hasta la próxima!

Publicado por Roberto Carrancio en Cloud, SQL Server, 0 comentarios

Seguridad a nivel de fila (RLS) en SQL Server

Cuando hablamos de seguridad en SQL Server, la mayoría piensa primero en roles, permisos, y si somos afortunados, en principios de mínimo privilegio. Sin embargo, en muchas organizaciones se sigue dejando la lógica de acceso a los datos en manos de la capa de aplicación. Y eso, como todos sabemos, es pedirle a un desarrollador frontend que entienda el negocio y además proteja los datos. Un sinsentido. Ahí entra en juego Row-Level Security (RLS), una funcionalidad introducida en SQL Server 2016 (sí, hace casi una década) que permite filtrar los datos directamente en el motor, sin tener que montar castillos de naipes con vistas o triggers que solo traen dolores de cabeza.

¿Qué es RLS y por qué debería importarnos?

La seguridad a nivel de fila permite restringir el acceso a las filas de una tabla en función del contexto del usuario que realiza la consulta. Y esto se hace de forma transparente, sin modificar el código de las consultas. No hablamos de un WHERE opcional añadido por el ORM de turno. Hablamos de reglas que se ejecutan en el servidor, en cada acceso, sin excepciones.

RLS nos permite definir una política de seguridad que determina si una fila puede o no ser accedida. Esa política se implementa mediante una función escalar, que actúa como el portero de discoteca: decide quién entra y quién no. Y lo mejor: el usuario no ve que se le han vetado filas. Simplemente, no están.

A diferencia de los trucos sucios con vistas filtradas y lógica duplicada, RLS está diseñado para que el control sea centralizado, mantenible y eficiente. Y, lo más importante, que no se pueda esquivar con una consulta directa.

Cómo funciona RLS bajo el capó

Para habilitar RLS necesitamos tres ingredientes:

  • Una función de filtrado, generalmente inline para evitar penalizaciones absurdas de rendimiento.
  • Una política de seguridad que asocia la función con una tabla concreta.
  • Usuarios (o roles) sobre los que queramos aplicar el filtrado.

La función recibe como parámetros las columnas de la tabla que queremos usar como condición de acceso, y devuelve un 1 (permitido) o un 0 (bloqueado). Así de sencillo. Y como todo buen sistema de seguridad, lo sencillo no quita lo peligroso: una mala implementación puede abrir más puertas de las que cierra.

Veámoslo con un ejemplo rápido:

Aquí estamos usando SESSION_CONTEXT() para almacenar el contexto del usuario (en este caso, su departamento), y comparar si la fila que se intenta leer pertenece al mismo departamento. Este patrón es de lo más habitual: guarda el valor relevante al iniciar sesión, y luego úsalo como filtro en la función RLS.

Después, aplicamos la política:

Y listo. A partir de ese momento, cualquier consulta contra la tabla Empleados estará sujeta al filtro. Sin excepciones. Sin necesidad de modificar el SELECT. Sin posibilidad de que el usuario borde el control.

Tampas comunes de RLS

Sí, puedes meter lógica compleja en la función RLS. No, no deberías. Las funciones de RLS se ejecutan por cada fila accedida, y si te vienes arriba y le metes llamadas a otras tablas, condicionales complejos o conversiones mágicas, lo que consigues es una performance digna de una migración mal hecha a Access.

Además, cuidado con usar funciones escalares no inline. No solo penalizan el rendimiento, sino que en versiones anteriores a SQL Server 2019 no se pueden usar directamente en RLS. Y aun en 2019 o superior, si puedes escribir una función inline, hazlo. Por tu salud mental y la de quien venga después.

También hay quien se pregunta: “¿Y si quiero permitir el acceso completo a ciertos roles o usuarios?” Pues puedes gestionar excepciones dentro de la propia función, comprobando por ejemplo el nombre del login o algún valor del SESSION_CONTEXT. Pero hazlo con moderación. Si acabas con una función que parece una novela de Agatha Christie para decidir quién puede leer qué, igual el problema no es el RLS.

RLS en escritura: INSERT, UPDATE y DELETE

Por defecto, RLS aplica el filtro también a operaciones de escritura. Eso significa que un usuario solo podrá insertar o modificar filas que cumplan la condición del filtro. No basta con tener permiso para escribir: si el valor que se inserta no pasa el control, la operación falla.

Este comportamiento se puede personalizar con predicados de bloqueo (BLOCK PREDICATE), pero por ahora no vamos a entrar en eso. Esto es una introducción, no una tesis. Lo importante es entender que RLS no es solo para SELECT. También puede protegernos de inserts maliciosos o updates “por accidente”.

¿Dónde guarda el contexto el RLS?

Una de las claves para que RLS funcione bien es cómo pasamos el contexto del usuario al motor. SQL Server no tiene una variable mágica llamada UsuarioActual.Departamento, así que somos nosotros los que tenemos que alimentar ese contexto. La opción más limpia y controlada es usar SESSION_CONTEXT.

Esto nos permite establecer pares clave-valor que persisten durante toda la sesión, y que podemos consultar desde la función RLS. Por ejemplo, en el momento de la conexión:

Esto lo puede hacer la capa de aplicación al conectarse, o mejor aún, a través de un procedimiento almacenado bien controlado. ¿Que podríamos usar ORIGINAL_LOGIN() o SUSER_SNAME() para extraer el usuario? Sí, pero volvemos al infierno de tener que mapear usuarios, mantener tablas auxiliares, y perder la trazabilidad del acceso real.

Limitaciones que no debes ignorar (aunque quieras)

RLS no cifra los datos. No evita que un administrador con permisos vea todo. No protege frente a accesos directos desde backups o desde otros medios. Es una herramienta de seguridad a nivel lógico, no físico.

Tampoco funciona automáticamente con todo. Por ejemplo, si creas una vista que usa WITH CHECK OPTION, puedes encontrarte con comportamientos inesperados. Y si haces SELECT INTO o usas funciones que acceden a múltiples tablas, el rendimiento puede sorprenderte… pero no en el buen sentido.

Y por supuesto, si alguien tiene permisos para desactivar la política o modificar la función RLS, todo se va al garete. Esto no sustituye una buena gestión de permisos. La complementa.

Conclusión: si no usas RLS, que sea porque no lo necesitas, no porque no lo conoces

RLS es una funcionalidad potente, elegante y bastante ignorada. Nos permite aplicar reglas de acceso directamente en el motor de SQL Server, con garantías y sin depender de que la aplicación “haga lo correcto”.

Eso sí: como cualquier herramienta de seguridad, hay que usarla bien. Si no tienes un modelo claro de control de acceso, implementar RLS sin criterio solo añade complejidad. Pero si sabes lo que haces y quieres evitar que un SELECT de más enseñe lo que no debe… entonces RLS es tu amigo.

En siguientes artículos profundizaremos en patrones de uso, ejemplos reales, y cómo integrar RLS con Active Directory o Azure AD. Pero por hoy, lo dejamos aquí. Que no se diga que no avisamos.

Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de LinkedIn y un canal de YouTube a los que te puede unir. ¡Hasta la próxima!

Publicado por Roberto Carrancio en Cloud, SQL Server, 1 comentario