Cloud

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

El libro de SQL Server que estabas esperando

Después de más de una década trabajando con SQL Server y ahora compartiendo contenido en SoyDBA, estoy escribiendo un libro pensado para profesionales que quieren ir más allá de la documentación oficial. Un libro técnico y muy didáctico.

Más de 40 capítulos en 7 partes, desde los fundamentos del modelo relacional hasta las herramientas de diagnóstico más avanzadas. Un recorrido completo, pensado para DBAs, analistas y desarrolladores que quieren entender cómo funciona SQL Server de verdad.

Portada Libro

El prólogo lo firma Fernando G. Guerrero, pionero y referente en nuestra comunidad. La contraportada, Juanjo Luna, MVP de Access y más apasionado de SQL de lo que quiere reconocer. Os dejo aquí el texto de la contraportada:

Si quieres conseguirlo corre a Amazón.

El libro está disponible en Amazon en formato papel y Kindle en todo el mundo.

Mantente al día de las novedades con mi newsletter gratuita

Logo SoyDBA

Únete a la newsletter de SoyDBA

Regístrate gratis para no perderte ninguna novedad. Te avisaré de noticias y eventos importantes

¡No hacemos spam! Lee nuestra política de privacidad para obtener más información.

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

MERGE en SQL Server: La eterna promesa llena de bugs

Durante años, MERGE ha sido esa promesa elegante que parecía resolver de un plumazo todo lo que nos complica la vida a los que escribimos consultas T-SQL: una única instrucción que unifica INSERT, UPDATE y DELETE. Menos código, más claridad, mejor rendimiento. El sueño húmedo de cualquier desarrollador… hasta que lo llevas a producción y te explota en la cara.

Sí, yo también quise creer. Pero tras ver más de un sistema roto por culpa de un MERGE traicionero, aprendí la lección. Y como este blog va de compartir experiencia real, no fantasías, vamos a desmontar con datos, técnica y un poco de sarcasmo, por qué MERGE es, en muchos entornos, un unicornio lleno de bugs.

El espejismo de la elegancia

La idea es sencilla y tentadora: haces una sola declaración, defines condiciones de coincidencia entre origen y destino, y luego eliges qué hacer si hay match o no. Todo en un solo bloque. Nada de IF EXISTS, nada de UPDATE por un lado e INSERT por otro. Un único statement, limpio y compacto.

El problema es que SQL Server no es un laboratorio teórico. Es un entorno donde las condiciones cambian, los bloqueos existen, los triggers hacen su trabajo (o no), y las consultas compiten por recursos. Y ahí, la belleza del MERGE se convierte en un infierno difícil de depurar.

El historial de errores no es opcional

Vamos con lo feo. Microsoft lleva más de una década acumulando errores abiertos relacionados con MERGE. Aquí no estamos hablando de edge cases oscuros, sino de fallos que afectan a la integridad de los datos.

Aaron Bertrand ya lo dejó claro hace más de siete años en su artículo «Use Caution with SQL Server’s MERGE Statement«, el operador MERGE acumulaba más de 20 errores críticos documentados en versiones de SQL Server que abarcan más de una década. Algunos tan graves como silenciosos, otros directamente absurdos. Y lo peor no es que existan, sino que muchos siguen sin resolverse a día de hoy después de años. Y cuando una funcionalidad lleva tanto tiempo con bugs abiertos y nadie los cierra, el mensaje es claro: no está pensada para producción. Punto.

Logo SoyDBA

Únete a la newsletter de SoyDBA

Regístrate gratis para no perderte ninguna novedad. Te avisaré de noticias y eventos importantes

¡No hacemos spam! Lee nuestra política de privacidad para obtener más información.

Estamos hablando de que MERGE no ejecuta triggers cuando debería, que dispara errores crípticos si hay más de una coincidencia por fila destino, que genera planes de ejecución incoherentes aunque las estadísticas estén actualizadas, o que, simplemente, no hace lo que le has dicho que haga.

Y cuando algo así puede romper datos sin avisar, no estamos ante una cuestión de estilo. Es un riesgo técnico que hay que conocer y evitar como parte del trabajo profesional.

Yo también lo he usado. Pero sabiendo dónde

Ahora bien, no todo es fuego y azufre. Yo mismo he usado MERGE con muy buenos resultados… pero en su contexto natural: procesos ETL para la carga de un Data Warehouse, sin concurrencia, sin triggers, sin usuarios esperando respuesta en tiempo real.

Ahí, MERGE brilla. En rendimiento, no tiene rival. Cuando puedes controlar el entorno y sabes que nadie va a meter la mano mientras el proceso está en marcha, la ganancia es real. Lo he usado para cargas masivas donde la lógica condicional era extensa y mantener la sincronización entre staging y destino requería algo más que un simple UPDATE. Y funcionó. Rápido, claro y sin sorpresas.

Y aun así, no es cómodo de usar

Ahora bien, incluso cuando MERGE funciona bien, escribirlo es otra historia. La sintaxis es tan compleja y poco intuitiva que soy incapaz de redactar una sentencia completa sin tener que abrir la documentación. Y no soy el único. Todos los que conozco, incluyendo perfiles senior, necesitan repasar los detalles o buscar ejemplos antes de atreverse a escribir uno no trivial. Obviamente al que escriba MERGEs a diario esto no le va pasar pero, al común de los mortales, nos cuesta y mucho.

Y no hablo de un MERGE básico, ese entra bien. Pero en cuanto mezclas múltiples condiciones, WHEN MATCHED, WHEN NOT MATCHED BY SOURCE, columnas calculadas, filtros condicionales y OUTPUT, el código se convierte en un monstruo ilegible. 

¿Es un problema grave? No, para eso está la documentación. Pero sí es un obstáculo innecesario. Más aún en un lenguaje como T-SQL, donde solemos priorizar claridad sobre compactación. Y aunque hoy en día las herramientas de IA nos pueden ayudar a escribirlo, mantenerlo sigue siendo responsabilidad nuestra. Y mantener un MERGE denso, con lógica compleja y comportamiento ambiguo, es como desactivar una bomba con guantes de boxeo.

Cuando MERGE falla… ni siquiera sabes por qué

Uno de los problemas más serios de MERGE es que sus errores no siempre son explícitos. En algunas situaciones simplemente no hace nada, o peor: hace algo, pero no lo que esperabas. Y ni el plan de ejecución, ni el @@ROWCOUNT, ni el log de errores te dan pistas.

Por ejemplo, si en el ON de un MERGE hay ambigüedad o más de una fila coincidente por destino, puedes recibir un error como:

The MERGE statement attempted to UPDATE or DELETE the same row more than once.

¿Y sabes qué fila fue? No. ¿Sabes por qué coincidieron dos filas? Tampoco, a menos que te pongas a hacer debugging con CTEs, filtros y OUTPUT hasta encontrarlo. Una joya para los que trabajamos bajo presión.

Triggers, locking y otras trampas técnicas de MERGE

Otra razón para evitar MERGE es su relación tóxica con los triggers. Por ejemplo, si tienes un INSTEAD OF TRIGGER, debe cubrir todas las acciones (INSERT, UPDATE, DELETE) usadas en el MERGE. Si no, obtendrás un error.

Además, dentro de los triggers, @@ROWCOUNT refleja la suma de todas las operaciones ejecutadas por el MERGE, no por separado. Así que no sabes si te han hecho un INSERT, un DELETE o un UPDATE. Y como $action no es accesible en el contexto del trigger… buena suerte.

A esto súmale que, en entornos de concurrencia, MERGE no garantiza aislamiento ni orden de ejecución. Necesitas usar explícitamente HOLDLOCK en la tabla destino para evitar condiciones de carrera, lo cual puede afectar gravemente al rendimiento o incluso provocar deadlocks si no lo haces bien.

Si Microsoft no usa MERGE… tú tampoco deberías

Hay una máxima que sigo como DBA, si ni el propio motor de SQL Server usa una funcionalidad para sus operaciones internas, será por algo. ¿O tú irías a comer a un restaurante donde ni el propio chef se come la comida que cocina?  Y adivina qué: Microsoft no utiliza MERGE internamente en sus procesos de sistema. Ningún SP del sistema usa MERGE. Ninguno.

Tampoco verás MERGE en herramientas como Replication, Change Tracking, CDC o Sync Framework. ¿Casualidad? No. Evitan su uso por inestabilidad, falta de garantías y dificultad de mantenimiento.

¿Y entonces qué usamos? Alternativas a MERGE

Lo que propongo no es TRY HARD SQL. Es simple, robusto y ha demostrado funcionar durante décadas:

Sí, son más líneas. No, no es sexy. Pero sabes lo que hace, puedes depurarlo, y es seguro en entornos OLTP. También puedes encapsularlo en un SP, usar OUTPUT para auditar, y meterlo en transacciones controladas. Resultado: código mantenible, auditable y libre de sorpresas.

Otra opción válida en operaciones masivas o ETL es usar UPDATE por separado, seguido de INSERT con NOT EXISTS. Ejemplo clásico:

Más claro, más predecible y con control absoluto de cada paso.

¿Y qué pasa con el rendimiento?

No podemos cerrar el tema sin hablar de la razón por la que muchos defienden MERGE con uñas y dientes: el rendimiento. La posibilidad de hacer INSERT, UPDATE y DELETE en una única operación suena, y es, más eficiente que dividir el trabajo en tres pasos.

Y sí, cuando funciona bien, MERGE puede reducir el número total de lecturas. El optimizador puede acceder a las tablas implicadas una sola vez, generar un plan compacto, y ejecutar todo con menos I/O que si lanzas varias operaciones por separado. Eso, en entornos de grandes volúmenes, marca la diferencia.

Pero, y este «pero» es importante, todo ese rendimiento se evapora en cuanto las condiciones dejan de ser ideales. Si tienes triggers, estadísticas obsoletas, datos duplicados, falta de unicidad, o cualquier mínima sorpresa en los datos, el plan de ejecución puede degradarse. Y si encima falla en mitad del proceso, el coste de arreglarlo supera con creces cualquier ganancia de rendimiento.

La pregunta no es solo “¿es más rápido?”, sino “¿cuánto me cuesta cuando va mal?”. Porque, al menos para mí, un MERGE que tarda 2 segundos pero rompe una fila cada 10.000 es mucho peor que un UPDATE + INSERT de 15 segundos que funciona siempre.

Dicho eso, si lo usas en un proceso ETL bien contenido, con staging controlado, sin usuarios ni competencia por recursos, y puedes probar cada camino lógico… adelante. En esos casos, sí, el rendimiento justifica su uso y, como os decía antes, yo mismo lo he usado.

¿Se puede usar MERGE con garantías?

La respuesta corta es sí, pero sólo bajo ciertas condiciones. Y con cuidado quirúrgico.

  • No debe haber concurrencia. Ni un solo proceso paralelo tocando las mismas filas.
  • Debe usarse HOLDLOCK explícito.
  • Las condiciones del ON deben garantizar unicidad absoluta.
  • Debes probarlo exhaustivamente, incluyendo casos límite y datos duplicados.
  • Y por favor, nada de usarlo con triggers activos. Ahí directamente estás pidiendo un exorcismo.

¿Vale la pena todo ese esfuerzo para no escribir cinco líneas más? En procesos ETL donde los datos son temporales, y el entorno está bien controlado, puede tener sentido. En entornos críticos OLTP… ni loco.

Conclusión

MERGE es eficiente. Compacto. Potente. Y en los entornos correctos, puede ser una joya. Pero en SQL Server, también es frágil, lleno de trampas y muy poco transparente cuando falla. No es cuestión de gustos, es cuestión de riesgos.

Si lo vas a usar, hazlo con pleno conocimiento de sus limitaciones y con un entorno controlado. Y si no puedes controlar todo lo que MERGE necesita para no traicionarte… mejor escribe unas líneas más y duerme tranquilo.

Porque a veces, la mejor optimización es no tener que hacer un análisis de errores.

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

¿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

Novedades sobre el libro

Después de meses de trabajo, revisiones y mucha paciencia, puedo compartir tres novedades clave del libro SQL Server: La NO guía práctica de optimización:

  • El prólogo lo firma Fernando G. Guerrero, pionero en SQL Server y miembro histórico de la comunidad. Además de escribir unas palabras introductorias, ha contribuido en la revisión técnica.

  • Aquí está el índice completo, con los más de 40 capítulos organizados en 7 partes.

  • Y sí: la fecha de lanzamiento ya es oficial. El libro estará disponible a mediados de septiembre. Pronto os diré el día exacto

Índice completo

Parte 1 – Fundamentos del modelo relacional y la arquitectura
Desde la definición de base de datos relacional hasta la estructura física de páginas y archivos. El punto de partida sólido para entender qué hay detrás de cada consulta.

Parte 2 – T-SQL y construcción de consultas eficientes
SELECT, INSERT, UPDATE y DELETE como base, y a partir de ahí subconsultas, CTEs, funciones de ventana, vistas, procedimientos y ejecución de consultas. Todo lo que necesitas para escribir SQL que funcione en producción.

Parte 3 – Transacciones, concurrencia y aislamiento
Propiedades ACID, niveles de aislamiento, bloqueos, deadlocks y el nuevo modelo de bloqueos optimizados. Una sección clave para quien de verdad administre entornos críticos.

Parte 4 – Internals, configuración y mantenimiento
TempDB, almacenamiento, memoria, CPU, cardinalidad, parametrización, índices, estadísticas y particionado. La cocina interna del motor y cómo configurarlo sin hipotecar el rendimiento.

Parte 5 – Backup, recuperación y disponibilidad
Modelos de recuperación, estrategias de backup, restore, Log Shipping, Database Mirroring y Availability Groups. Todo lo que sostiene la continuidad de un entorno SQL Server serio.

Parte 6 – Seguridad y control de acceso
Principales, usuarios, roles, permisos y técnicas avanzadas de seguridad. Desde lo básico hasta RLS, cifrado y enmascaramiento de datos.

Parte 7 – Control, monitorización y diagnóstico
Herramientas internas y externas: Resource Governor, Activity Monitor, DMVs, Profiler, Extended Events, Query Store y PerfMon. Para que no solo administres, sino que entiendas qué ocurre bajo el capó.

Con este índice ya podéis ver que no es un manual de recetas rápidas, sino una guía estructurada de principio a fin para profesionales de SQL Server.

El lanzamiento será a mediados de septiembre y os avisaré en cuanto esté disponible en Amazon.

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