T-SQL

Cuando un LEFT JOIN se convierte en INNER. SQL Server Basics

Lo has visto. Lo has sufrido. Y si aún no te ha pasado, enhorabuena, pero no te relajes, tu día llegará. Me refiero al clásico caso del LEFT JOIN que acaba comportándose como un INNER JOIN porque alguien decidió, en un momento de inspiración, colocar un filtro donde no debía. Y claro, la consulta no devuelve lo que se espera, o peor: devuelve justo lo que se espera, pero está mal.

Hoy vamos a hablar de un problema que se cuela en muchos desarrollos y auditorías de código: el efecto de los filtros en un LEFT JOIN, y cómo una mala ubicación puede cambiar radicalmente el comportamiento de tu consulta. Así que prepara café, que vienen curvas.

El LEFT JOIN que no era

Un LEFT JOIN debería, en teoría, devolver todos los registros de la tabla izquierda, aunque no haya coincidencia en la tabla derecha. Hasta aquí, todo bien. Pero esto solo se cumple si no saboteas la lógica desde el WHERE.

Veamos un ejemplo simple pero revelador:

A simple vista parece correcto. Queremos todos los clientes y, si tienen facturas a partir de 2025, las mostramos. Pero no. Lo que acaba devolviendo esta consulta es únicamente aquellos clientes que tienen facturas desde 2025. ¿Y los que no tienen ninguna factura? Fuera del resultado. Despedidos.

¿Por qué? Porque estás aplicando un filtro sobre una columna de la tabla derecha (Facturas) después del JOIN, pero en el WHERE, que se evalúa después del JOIN, cuando los NULL ya están presentes. Y claro, NULL >= ‘2025-01-01’ es FALSE. El motor hace su trabajo, y tú te quedas sin clientes sin facturas.

¿Dónde va el filtro entonces?

En el ON. Sí, ese que muchos usan como si fuera una casilla para emparejar claves primarias con foráneas y poco más. Pero el ON tiene un papel más interesante aquí, si metes el filtro ahí, estarás condicionando el JOIN, no el resultado final.

Así que la versión correcta sería:

Ahora sí, todos los clientes aparecen. Los que tienen facturas desde 2025 las verán en su fila. Los que no, tendrán NULL en FacturaID. Tal y como un LEFT JOIN debe comportarse.

¿Y si necesito más filtros?

Buena pregunta. No es raro que quieras filtrar también por condiciones de la tabla izquierda. ¿Qué pasa entonces? Pues ahí sí puedes usar el WHERE, con criterio.

Por ejemplo:

Esto funciona como debe. El filtro en Clientes actúa después del JOIN, pero como no anula la lógica del LEFT, todo sigue su curso. Los clientes activos salen todos, tengan o no facturas desde 2025. Así sí.

¿Y si necesito filtrar por una columna de la derecha, pero sólo cuando no sea NULL?

Aquí empieza el juego sucio. Lo que quieres, probablemente, es quedarte con clientes que tengan facturas desde 2025, pero también mostrar los que no tienen ninguna factura. En ese caso, tienes que pensar en lógica condicional.

Una forma válida sería:

Esto ya es más decente. Estás diciendo: «dame los clientes que tienen facturas desde 2025, o los que no tienen ninguna factura». Y eso se respeta.

Pero seamos sinceros, si tienes que hacer esto a menudo, algo en tu modelo de consultas no huele bien. O tu reporting, o tu forma de pensar los datos. En esos casos, es mejor plantearte si necesitas un LEFT JOIN, unas vacaciones o modificar la lógica entera de la consulta.

No es un detalle menor

Aquí no estamos hablando de una microoptimización ni de una virguería SQL. Estamos hablando de lógica rota. El tipo de error que pasa todos los entornos, llega a producción, y luego nadie entiende por qué los informes no cuadran con la realidad.

Y lo peor, es un error silencioso. No da fallo. No lanza excepciones. Simplemente te da datos incorrectos con cara de datos correctos. Lo cual, en nuestro mundo, es el infierno.

En auditorías de rendimiento y revisiones de lógica de negocio, me encuentro este patrón con una frecuencia desesperante. Muchas veces me dicen: «Pero es que es un LEFT JOIN, ¿por qué no salen todos?». Y la respuesta es casi siempre la misma: porque tú mismo has convertido ese LEFT en un INNER, sin darte cuenta.

¿Qué pasa con los ORMs?

Sí, los ORMs. Esos grandes generadores de consultas elegantes como un campo minado. Hay ORMs que optimizan mal los JOIN, o que aplican los filtros fuera del ON por defecto. Y luego te toca a ti arreglar el entuerto porque el modelo “no da para más”.

Así que si estás usando Entity Framework, Hibernate o cualquier ORM moderno, revísalo tú a mano. No confíes en que el generador de consultas haya entendido tus intenciones como tú las entiendes.

Y ya que estamos, un apunte sobre MERGE

Sí, ya sé que esto va de LEFT JOIN, pero la confusión entre el WHERE y el ON en MERGE también merece un recordatorio. Porque ahí también es fácil liarla. En un MERGE, el ON es la condición de emparejamiento, no de filtrado. Si lo usas mal, puedes terminar haciendo UPDATE a registros que no deberías tocar, o insertando cosas que ni sabías que existían.

Lo dejo ahí, pero si te interesa, en el blog ya hablé en profundidad sobre los problemas de MERGE.

Conclusión

Si vas a usar un LEFT JOIN, asegúrate de entender cómo funciona y dónde colocar los filtros. No es un tema de estilo, es un tema de lógica. Un WHERE mal puesto no solo degrada el rendimiento: rompe la semántica de tu consulta.

Así que la próxima vez que revises una query con LEFT JOIN, pregúntate si realmente está haciendo lo que debería. Y si no, ya sabes dónde mirar primero: el WHERE. Porque sí, puedes hacer un LEFT JOIN que se comporte como un INNER, y también puedes correr con los cordones desatados. Ambas cosas acaban 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 Telegram y un canal de YouTube a los que te puede unir. ¡Hasta la próxima! 

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

SQL contra el Apocalipsis Mutante (Parte 5): Última defensa

Pensábamos que lo peor ya había pasado. Que después de clasificar refugios, contar infectados y trazar mapas de avistamientos podríamos respirar. Ilusos.

La tercera y última parte nos obligó a sacar toda la artillería. Ya no bastaba con consultar datos. Había que analizar patrones, construir rutas de evacuación dinámicas y generar informes multidimensionales. Y sí, todo eso con SQL.

Aquí tienes las soluciones explicadas de los últimos cinco retos. Si estás leyendo esto, es que todavía no te han comido.

Reto 3.1 – Ranking por armamento: quién manda aquí

La cosa está cada vez peor, necesitamos urgentemente asignar un número a cada refugio según la cantidad de armas que tiene. Así de simple. O así de esencial, si estás organizando una defensa coordinada y necesitas saber a quién se le puede confiar una ametralladora sin que se dispare en el pie.

Empezamos suavecito, hay que calentar. ROW_NUMBER() es una función de ventana que genera un contador dentro del conjunto de datos, según el orden que tú le digas. Aquí lo ordenamos por Weapons DESC, es decir, del más armado al menos. Cada refugio recibe un número único, sin importar si hay empates.

Esto no devuelve “quién tiene más armas”, sino quién va primero, segundo, tercero…. Una forma de tomar decisiones rápidas sin tener que pensar demasiado. Como debe ser en medio de un asalto mutante.

Reto 3.2 – Comparar infectados entre refugios vecinos

Se nos pide una labor fundamental para analizar la situación, ver si los números de infectados suben o bajan en los refugios contiguos. No para hacer turismo sanitario, sino para prever si un brote se está expandiendo.

Y ahora si, funciones de ventana de verdad, LAG(Infected, 1) devuelve el número de infectados del refugio anterior mientras que LEAD(Infected, 1) devuelve el número de infectados del siguiente. Ambos requieren un orden: en este caso, ORDER BY RefugeID.

Esta es una forma elegante de comparar filas sin tener que auto-unir la tabla consigo misma. Ideal para ver tendencias, anomalías… o refugios que están a punto de convertirse en un problema.

Reto 3.3 – Avistamientos y totales con ROLLUP

Otro imprescindible, crear un informe con los avistamientos de mutantes por día y ubicación, pero incluyendo totales parciales. ¿Por qué? Porque en el apocalipsis, igual que en el día a día en la oficina, alguien en la cadena de mando pidió “una vista agregada para facilitar la toma de decisiones” y no supimos decir que no.

En este caso ROLLUP nos permite agrupar en varios niveles:

  • Día + coordenadas → número de avistamientos
  • Día (sin coordenadas) → total de ese día
  • Total global

Esto genera filas con valores NULL en las columnas que se van agregando. Si no sabes leer esas NULL, no estás leyendo totales. Estás leyendo confusión. Lo bueno: es más limpio que hacer varias consultas. Lo malo: requiere que el que lo lea sepa lo que está viendo. No apto para jefes con prisas.

Reto 3.4 – Rutas de evacuación dinámicas con CTE recursiva

Entre los datos de infectados y los avistamientos nos empezamos a preocupar, ¿y si salir corriendo es la mejor opción? Por si acaso vamos a construir todas las rutas de evacuación posibles a partir del refugio 1, siguiendo las conexiones que tenemos en la tabla EvacuationRoutes.

Esto es una CTE recursiva de manual. Traducido: una tabla temporal que se llama a sí misma para recorrer un camino paso a paso. En la parte “ancla” seleccionamos las rutas que salen del refugio 1 mientras que en la parte recursiva vamos empalmando los destinos como si siguiéramos el hilo de Ariadna, construyendo la ruta completa en texto (Path).

CAST y CONVERT se usan aquí para concatenar el camino en una cadena legible: 1 -> 2 -> 3 -> 4 -> 5.

¿Tiene límites? Claro. Sin control de ciclos puede acabar en bucle infinito, como los correos entre departamentos. Pero para rutas simples, esto es perfecto.

Reto 3.5 – Informes multidimensionales con CUBE

Por si acaso antes de irnos vamos a sacar un último informe de situación. Nos han pedido sacar un informe de cuántos refugios hay por estado (CRITICAL u OK) y por nivel de armamento, incluyendo subtotales y totales. Porque si, a alguien le ha parecido bien hacer una tabla dinámica sin usar Excel.

Esto tiene miga, lo sé. Vamos por partes. Primero agrupamos por dos variables derivadas con CASE estado (CRITICAL o OK) y rango de armas (0–20, 21–50, >50). Después, con CUBE generamos todas las combinaciones posibles:

  • Cada grupo individual
  • Totales por estado
  • Totales por grupo de armas
  • Total general (cuando ambas columnas son NULL)

Si ROLLUP ya era potente, CUBE es una navaja suiza para informes complejos. Útil, pero peligroso si no sabes leer lo que devuelve.

Conclusión

Estas consultas no se escriben con prisas. Se escriben con estrategia. En esta última fase, SQL dejó de ser una herramienta de lectura para convertirse en un lenguaje de decisión.

Desde rutas de evacuación recursivas hasta informes multidimensionales, estas técnicas separan a los que saben ejecutar un SELECT de los que pueden liderar una operación de supervivencia basada en datos.

¿Es el final? Por ahora. La amenaza mutante ha sido contenida. Pero si algo hemos aprendido de los datos… es que siempre vuelven.Y esta vez, estaremos listos.

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

LINQ y SQL Server: consulta tú, que ya lloro yo

Si eres developer y trabajas con .NET, lo más probable es que en algún momento hayas caído rendido ante LINQ. Lo entiendo, es tentador cual canto de sirena. Tiene lo suyo: expresividad, fluidez, integración total con C#… es cómodo. Pero si trabajas con SQL Server como motor de datos, más te vale saber qué está haciendo LINQ por detrás. Porque aquí, en las profundidades del motor, no nos tragamos la excusa de «yo sólo puse un .Where()».

Este artículo no es para demonizar LINQ. Es para que entiendas, desde el punto de vista del DBA que va a recibir tus consultas, por qué usar LINQ sin saber cómo funciona es como lanzar T-SQL con los ojos cerrados y esperar que salga rápido. Alerta spoiler: no lo hace.

LINQ: el ORM moderno que no siempre sabe lo que hace

A ti LINQ te permite escribir consultas con tipos, IntelliSense, lambdas y toda la fantasía moderna del desarrollo elegante. A nosotros nos llega una consulta SQL que ha generado un ORM sin ningún sentido del rendimiento, de los índices ni del plan de ejecución.

Tú ves:

Nosotros vemos: joins sin sentido, LEFT OUTER JOIN innecesarios, condiciones que no usan índices, y a veces hasta SELECT TOP 1000000 porque alguien no quería paginar como es debido. Lo que para ti es una consulta sencilla, para nosotros puede ser una fiesta de bloqueos, CPU y discos saturados.

LINQ no es malo. Pero no sabe optimizar. Y su traductor a SQL (ya sea EF, LINQ to SQL o el flavor de turno) sólo tiene una misión: funcionar. No rendir. Eso, amigos, es responsabilidad vuestra.

SQL Server no es una caja negra

Si has trabajado toda tu vida con Entity Framework sin mirar lo que genera por debajo, te estás perdiendo la mitad de la película. Y no la buena. SQL Server es un motor potente, flexible y bastante generoso… pero sólo cuando le das instrucciones claras.

Cuando llega una consulta LINQ traducida automáticamente, SQL Server hace lo que puede. Pero si la estructura de la consulta es compleja, el uso de joins es arbitrario y los filtros se aplican después del .ToList(), entonces no hay milagros. Hay “table scans”, “sort warnings”, “hash matches” no deseados y todo lo que un DBA no quiere ver en el plan de ejecución.

Lo que tú ves como un .Include() para cargar relaciones, nosotros lo vemos como una máquina de hacer JOINs sin control. Y cuando eso se hace en producción con datos reales, hay que estar muy seguro de que se entiende lo que se está mandando.

IEnumerable, IQueryable y el horror que no ves en LINQ

Hay un detalle que seguimos viendo incluso en equipos senior: no distinguir IEnumerable de IQueryable a la hora de consultar.

Cuando usas IQueryable, la consulta se traduce a SQL y se ejecuta en la base de datos. Bien. Pero cuando haces .ToList() antes de aplicar más filtros, estás trayendo todo a memoria y filtrando en .NET. Eso está bien si tienes diez registros. Si tienes diez millones, acabas de llenar el servidor de aplicaciones con datos que no necesitabas.

El ORM no sabe optimizar. Tú sí deberías.

Así que cuando alguien hace esto:

Y luego se pregunta por qué la aplicación va lenta, la respuesta es clara: no es la base de datos, es tu código. LINQ puede ser declarativo, pero no mágico.

Los casos en que LINQ hace llorar al optimizador

¿Sabes lo que pasa cuando encadenas tres .Include()s, haces un .SelectMany() y luego aplicas una condición que sólo puede evaluarse en cliente? Que el SQL resultante se convierte en un engendro. Y a nosotros nos llega una consulta de 150 líneas con subconsultas, columnas que no se usan y JOINs en cascada que anulan los índices.

Esto no es una exageración. Lo he visto con mis propios ojos. Y no una vez. Las herramientas modernas de desarrollo facilitan mucho la creación de consultas… que nadie ha revisado. Y luego cuando llegan las quejas de rendimiento, el culpable nunca es el ORM. Siempre es “la base de datos que no escala”.

No. La base de datos escala. Lo que no escala es traducir expresiones lambda como si fueran instrucciones optimizadas para un motor relacional.

Cómo hacer las paces: consejos para developers (sí, es por vuestro bien)

Lo primero: aprende a ver el SQL que estás generando. EF Core permite interceptar y registrar el SQL generado. Úsalo. Lee ese SQL. Míralo con ojos críticos. Si ves que tiene 10 joins, 30 columnas innecesarias y ninguna cláusula WHERE, no lo envíes a producción. Mándalo al infierno de staging, a ver cuánto tarda.

Segundo: si una consulta es crítica, escribe SQL tú mismo. Usa FromSqlRaw() o, mejor aún,  procedimientos almacenados dentro de SQL Server. No pasa nada. No es “menos elegante”, es más responsable.

Tercero: si tienes dudas, habla con el DBA. No somos ogros. Bueno, algunos sí. Pero en general, preferimos una conversación a tener que cazar queries con el SQL Profiler porque ha saltado una incidencia de rendimiento a las 3 de la mañana.

Cuarto: entiende el modelo de ejecución diferida de LINQ. Y si tienes que filtrar, hazlo antes del .ToList(). Siempre.

Y por último: no abuses del azúcar sintáctico. LINQ puede ser cómodo, pero no sustituye al conocimiento. No saber lo que está pasando en SQL Server es como conducir un coche sin saber si vas en primera o en tercera.

¿Cuándo sí usar LINQ?

LINQ brilla en consultas simples. En operaciones sobre colecciones en memoria. En proyecciones pequeñas y bien definidas. Si tu consulta es trivial y los datos están bien indexados, no hay problema. Pero si estás construyendo un informe complejo, una API con cientos de miles de registros o una consulta crítica para el negocio, deja de lado la comodidad.

Y si usas LINQPad, mejor. Ahí sí puedes ver lo que pasa y ajustar con cabeza. Porque en el fondo, LINQ no es el problema. El problema es no saber cuándo dejar de usarlo.

Conclusión: hay vida más allá de LINQ

No hay escapatoria. Si trabajas en un proyecto con SQL Server, y usas un ORM o LINQ, el rendimiento de tus consultas depende de ti. No vale decir “eso es cosa del DBA” o “es que eso lo hace el ORM solo”. Porque cuando la aplicación se arrastra, da igual de quién sea la culpa: hay que arreglarlo y ni todo el hardware del mundo arregla depende que consultas.

Así que la próxima vez que escribas una consulta LINQ, pregúntate: ¿qué SQL va a salir de aquí? ¿Lo he mirado? ¿Lo he probado con datos reales? ¿Estoy filtrando bien? ¿Estoy trayendo sólo lo que necesito?

Y si no puedes responder a eso con seguridad, tal vez lo mejor sea bajarse al T-SQL y escribir como los mayores.

Porque aquí no se trata de gustos. Se trata de no matar al servidor con buenas intenciones y malas queries.

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

Tablas temporales vs. variables de tabla en SQL Server: diferencias que sí importan

Las tablas temporales y las variables de tabla en SQL Server son como los destornilladores y las llaves inglesas de nuestra caja de herramientas: parecidas en concepto, pero con propósitos, comportamientos y peculiaridades bien distintos. Ambos mecanismos nos permiten trabajar con conjuntos de datos intermedios sin necesidad de recurrir a tablas permanentes, pero quien haya intentado usarlos indistintamente en entornos reales sabe que las diferencias importan. Y mucho.

Vamos a destripar ambas opciones con calma, ver cómo se comportan, qué ventajas ofrecen y en qué situaciones conviene (o no) usarlas. Porque sí, aunque parezcan intercambiables, tratarlas como tal suele acabar en consultas lentas, planes de ejecución misteriosos y DBA rezando en voz baja.

Tablas temporales: las inquilinas del tempdb

Las tablas temporales (#TablasTemporales) son lo más parecido a una tabla normal que podemos crear en tiempo de ejecución sin dejar huella permanente. Se crean en la base de datos tempdb, y existen mientras dure la sesión (o el scope) que las creó. Podemos definir índices, claves primarias, restricciones, estadísticas… Vamos, que son tablas de verdad, aunque vivan en alquiler. Su sintaxis es familiar y directa:

O incluso más común aún:

El plan de ejecución que las acompaña suele ser robusto, especialmente si trabajamos con cantidades de datos considerables. SQL Server genera estadísticas automáticas sobre las columnas, lo cual permite un plan más ajustado al volumen real. Esto, que parece un detalle técnico sin importancia, marca la diferencia entre una consulta que vuela y otra que arrastra los pies como yo un lunes por la mañana antes del primer café.

¿Inconvenientes? Algunos. El uso de tempdb implica competencia con otros procesos que también están abusando del mismo recurso. Además, su ciclo de vida depende del contexto: si las creamos en un procedimiento almacenado y lo llamamos varias veces, conviene borrarlas explícitamente con DROP TABLE o usar IF OBJECT_ID(…) IS NOT NULL.

Y por supuesto, cuidado con el scope: una tabla temporal creada dentro de un procedimiento no es accesible desde fuera. Pero eso no debería sorprendernos. Tampoco esperamos que un DECLARE de una variable sobreviva al final del procedimiento.

Variables de tabla: pequeñas, rápidas… y caprichosas

Las variables de tabla (@VariablesDeTabla) se introdujeron como una forma rápida y elegante de manejar pequeños conjuntos de datos sin el overhead de una tabla temporal completa. Son ideales para almacenar unas cuantas filas, iterar lógicamente sobre ellas o devolver resultados simples.

La sintaxis es limpia:

Y su ciclo de vida es exactamente el del bloque donde se declaran. No hay que preocuparse por borrarlas ni por interferencias externas. Hasta aquí todo bien.

Ahora viene el problema: SQL Server no genera estadísticas sobre variables de tabla. Nunca. Ni en 2008 ni en 2022. Esto significa que el optimizador trabaja a ciegas. Literalmente: asume que una variable de tabla tiene una única fila. Da igual si tiene 1 o 10.000. El plan de ejecución será el de una tabla de una fila. Y eso, amigos, rara vez termina bien.

¿Hay excepciones? Desde SQL Server 2019, con OPTION (RECOMPILE), el optimizador puede estimar el número real de filas en algunos casos. Pero es una tirita en una fractura abierta. A veces ayuda, otras no. Y seguir usándolas a ciegas es una receta para la frustración.

¿Entonces son inútiles? No, ni mucho menos. Funcionan de maravilla cuando el número de filas es pequeño (menos de 100 suele ser seguro) y cuando las operaciones son simples. Pero si metemos un JOIN, un GROUP BY o empezamos a empujar lógica compleja… mejor sacar la artillería de verdad: tabla temporal.

Tablas temorales vs Variables de tabla: lo que no te dice la documentación

Hablemos claro. Las diferencias no están solo en la sintaxis o el ámbito. Lo importante es cómo se comportan bajo carga, cómo afectan al plan de ejecución y qué tipo de mantenimiento requieren. Para compararlas vamos a ver uno a uno los aspectos más interesantes.

Estadísticas

Las tablas temporales sí generan estadísticas; las variables de tabla no. Esto significa que las temporales permiten planes de ejecución más óptimos en escenarios con muchos datos. Las variables, no.

Soporte de índices

Ambas opciones permiten claves primarias y restricciones únicas. Desde SQL Server 2014 es posible definir índices secundarios en variables de tabla, pero solo dentro de la declaración y con sintaxis limitada. En tablas temporales podemos crear cualquier tipo de índice, incluidos los columnstore, sin restricciones adicionales.

Transacciones

Las variables de tabla no se ven afectadas por ROLLBACK. Si algo falla, su contenido sigue ahí, lo cual puede ser bueno… o un bug encubierto. Las tablas temporales, en cambio, participan en las transacciones como cualquier otra tabla.

Almacenamiento y persistencia

Ambas opciones viven en tempdb, aunque las variables lo hagan de forma menos visible. Pero a nivel físico, no hay magia: no están «en memoria», como algunos aún creen. Eso sí, las temporales suelen dejar más rastro en el sistema de archivos si no se gestionan bien.

Además, como ya hemos comentado las tablas temporales duran lo que dura la sesión, es decir, mientras no las borremos o cerremos esa sesión seguirán ahí. Las variables tipo tabla, por el contrario duran lo que dura la ejecución del lote (batch).

Si veis en la imagen, la segunda consulta no encuentra nada. Esto pasa porque después del “GO” ya se considera otro lote.

Paralelismo

Las tablas temporales pueden beneficiarse del paralelismo en las consultas; las variables de tabla, salvo casos contados y versiones muy recientes de SQL Server, no.

Lectura y escritura

En escenarios de alto volumen, las operaciones sobre variables de tabla pueden ser considerablemente más lentas que sobre tablas temporales. Aunque el coste del DECLARE parezca nulo, el impacto acumulado en los planes de ejecución mal optimizados se paga caro.

Casos de uso, ¿Cuándo elegir tablas temporales o variables de tabla?

Una variable de tabla nos viene de perlas cuando queremos devolver una pequeña tabla desde una función, cuando estamos en mitad de un script complejo que necesita guardar una docena de valores intermedios, o cuando buscamos claridad sin sacrificar rendimiento (porque sabemos que los datos son pocos y controlados). Por norma general, no deberíamos usarlas para más de 100 registros.

Una tabla temporal brilla en todo lo demás: cargas intermedias, transformaciones complejas, conjuntos de datos que van a vivir varias etapas, o cuando necesitamos analizar y refinar el rendimiento de una consulta. Incluso para esas subconsultas que usamos varias veces en una misma consulta y pueden llegar a ser pesadas.

También conviene recordar que hay un tercer actor en esta historia: las tablas temporales globales (##TablaGlobal) y las tablas de memoria (MEMORY_OPTIMIZED). Pero eso ya es otro capítulo. O varios.

Conclusión

Elegir entre una tabla temporal y una variable de tabla no debería depender del estado de ánimo, sino del uso que le vamos a dar. Si los datos son escasos, la lógica es sencilla y no necesitamos estadísticas ni índices complejos, la variable funciona. Pero si hay que unirse a otras tablas, mover volumen o exprimir rendimiento, la tabla temporal es la opción profesional.

No olvidemos que el optimizador de SQL Server toma decisiones basadas en lo que sabe. Y con una variable de tabla, lo que sabe es poco. Si le damos una tabla temporal bien definida, con índices y estadísticas, puede hacer su trabajo. Si le damos una caja cerrada con un «ya te apañarás», no esperemos milagros.

Así que, la próxima vez que tengamos que elegir entre DECLARE @Tabla y CREATE TABLE #Temp, pensemos dos veces. Porque sí, ambas pueden almacenar datos. Pero sólo una de ellas está preparada para aguantar una jornada completa sin pedir un café doble a mitad de camino. Y no, no es la variable.

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, 1 comentario

Firmar procedimientos almacenados con certificados

Una política de permisos mínimos para usuarios (y roles) es fundamental para la seguridad de nuestros datos. Sin embargo, a menudo nos enfrentamos al dilema de cómo hacerlo sin sacrificar funcionalidad ni exponer en exceso nuestros objetos. Como decía, la necesidad de implementar un principio de privilegios mínimos es esencial en entornos seguros y controlados, especialmente en bases de datos con múltiples aplicaciones o equipos interactuando. Una técnica muy potente y poco utilizada en SQL Server es la firma de procedimientos almacenados con certificados. Hoy vamos a explorar cómo funciona y por qué puede marcar una diferencia importante en nuestras estrategias de seguridad.

Delegación segura con certificados

Cuando se habla de conceder permisos a través de procedimientos almacenados, una de las primeras aproximaciones suele ser el uso de EXECUTE AS. Aunque funcional, esta técnica presenta inconvenientes, especialmente cuando entra en juego el “chaining” de ejecución o cuando queremos evitar tener que gestionar contextos de ejecución elevados.

Ahí es donde entran los certificados. Firmar procedimientos almacenados nos permite encapsular permisos de forma precisa y segura sin cambiar el contexto de ejecución, sin depender de impersonaciones y sin debilitar el modelo de seguridad de la base de datos.

¿Cómo funciona la firma con certificados?

El procedimiento se basa en tres pasos fundamentales: creación del certificado, firma del procedimiento y asignación del permiso requerido a un login asociado al certificado. Cada uno de estos pasos es determinante para conseguir una delegación limpia y controlada.

Partimos de una situación donde un procedimiento necesita acceder a un objeto (por ejemplo, una tabla o vista), pero el usuario que lo ejecuta no tiene permiso directo sobre ese objeto. Esto normalmente no sería un problema salvo que el procedimiento y el objeto sean de un propietario distinto o que la consulta desde el procedimiento se ejecute en otro contexto de seguridad diferente, por ejemplo por usar sp_executesql. En estos casos, si firmamos el procedimiento con un certificado y asociamos el certificado a un login o usuario que sí tenga ese permiso, el procedimiento se ejecutará con esos privilegios, pero sin necesidad de cambiar el contexto de ejecución del usuario.

Caso práctico detallado

Supongamos que queremos que ciertos usuarios puedan ejecutar un procedimiento llamado usp_ObtenerVentasPrivadas, que accede a una tabla sensible como VentasPrivadas, pero sin darles acceso directo a dicha tabla. El procedimiento sería como el siguiente:

Tal como está, cualquier usuario que tenga permiso de ejecución sobre usp_ObtenerVentasPrivadas podrá acceder a VentasPrivadas, pero sin tener acceso directo a ella ni posibilidad de usarla fuera de este procedimiento. Esto es porque el contexto de ejecución sigue siendo el del usuario original al no tener código dinámico y al pertenecer la tabla y el procedimiento al mismo esquema (con el mismo owner del esquema).

Cuando la cosa se complica

Pero, ¿qué pasa si cambiamos el contexto de ejecución o tenemos distinto owner? Aquí es donde entran en juego los certificados. 

Yo para la demo que os pongo lo he simplificado y ejecuto una consulta simple pero, pongamos que tenemos un procedimiento que genera SQL dinámico y lo ejecuta con sp_executesql. Este sería el ejemplo que podeis reproducir vosotros mismos.

Como veis, si probamos a ejecutar el procedimiento almacenado con el usuario limitado va a dar error porque no tiene permisos sobre la tabla. Sin embargo, cuando creamos un certificado, lo asociamos a un usuario que tiene permisos sobre la tabla y firmamos el procedimiento con ese usuario ya podemos ejecutar sin error el procedimiento con el usuario limitado.

Ventajas clave de usar certificados

Una de las principales ventajas frente a EXECUTE AS es que el certificado no interfiere con el contexto de ejecución. Esto significa que si dentro del procedimiento hay llamadas a otros objetos que usan permisos del usuario original, todo seguirá funcionando correctamente. Además, los certificados son inmunes a problemas comunes como la pérdida de contexto entre bases de datos, lo que resulta útil en entornos distribuidos.

Otra ventaja es la auditabilidad. Como los permisos no se conceden directamente a los usuarios finales, sino que se encapsulan dentro de procedimientos firmados, es más sencillo identificar los puntos de entrada permitidos y realizar auditorías.

También se evita el problema clásico de los permisos residuales. Si un usuario necesita ejecutar varios procedimientos que requieren distintos permisos, no es necesario concederle un permiso amplio ni crear roles intermedios complejos. Firmamos cada procedimiento con los permisos justos que requiere, y el acceso queda perfectamente delimitado.

Consideraciones de los certificados a tener en cuenta

La firma de procedimientos con certificados no está exenta de ciertas limitaciones. Para empezar, no podemos firmar procedimientos encriptados ni CLR. Además, si un procedimiento se modifica, se pierde la firma y hay que volver a aplicarla.

En cuanto a la gestión de certificados, conviene centralizar su creación y almacenamiento de forma segura. El uso de contraseñas fuertes y una política clara de mantenimiento y renovación de certificados es fundamental para evitar riesgos.

Por último, es importante evitar el uso excesivo de esta técnica, especialmente si se convierte en la única forma de delegación. En bases de datos muy grandes o con cientos de procedimientos, puede ser más conveniente crear roles bien definidos y controlar el acceso de forma más clásica. La clave está en encontrar el equilibrio adecuado.

Comportamiento de DENY

Otra consideración muy importante que debemos tener en cuenta es que el uso de DENY no puede ser sobrepasado por una firma de certificado. Si el usuario que ejecuta el procedimiento tiene un DENY explícito sobre el objeto que se consulta dentro del procedimiento, nada podrá permitirle el acceso, por mucho que el certificado tenga ese GRANT.

Esto tiene dos consecuencias prácticas. La primera es que si queremos controlar el acceso mediante firmas, no debemos usar DENY. Sencillamente basta con no conceder permisos. Por otro lado, si existe un DENY, servirá como veto absoluto, incluso para accesos indirectos a través de procedimientos firmados.

Conclusión

Firmar procedimientos almacenados con certificados es una herramienta extremadamente útil y robusta para aplicar el principio de privilegios mínimos de forma segura y mantenible. Nos permite conceder permisos de forma encapsulada, sin necesidad de impersonaciones ni contextos elevados, y facilita una delegación precisa del acceso.

Es una técnica que deberíamos tener siempre en nuestro arsenal cuando diseñamos la arquitectura de seguridad de una base de datos SQL Server. En combinación con otras prácticas como el uso de roles, vistas seguras y auditoría de permisos, puede contribuir a sistemas mucho más sólidos, trazables y mantenibles.

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, SQL Server, 0 comentarios

Contextos de seguridad en SQL dinámico: permisos, procedimientos y sp_executesql

En mi artículo anterior os hablé de cómo construir SQL dinámico de forma segura usando sp_executesql, y cómo evitar riesgos como el SQL Injection. Sin embargo, hay un aspecto igual de crítico que no se suele tener en cuenta: el contexto de seguridad desde el que se ejecuta el código dinámico y su impacto en los permisos.

Muchos desarrolladores se sorprenden cuando un procedimiento almacenado que funciona con SQL «normal» deja de funcionar al pasar a SQL dinámico, a pesar de tener los mismos permisos. El motivo está en cómo SQL Server maneja los permisos de ejecución implícitos y qué ocurre cuando usamos sp_executesql. En este artículo voy a tratar de explicarlo paso a paso.

Permisos implícitos al ejecutar un procedimiento almacenado

Cuando concedemos a un usuario permiso para ejecutar un procedimiento almacenado, por ejemplo:

Ese usuario puede ejecutar el procedimiento sin necesidad de tener permisos directos sobre las tablas internas que use dicho procedimiento. Es decir, aunque no tenga SELECT sobre Sales.SalesOrderHeader, si el procedimiento ejecuta esta consulta:

… el usuario podrá obtener los datos. Esto ocurre porque el contexto de ejecución del procedimiento es el del propietario, y si el procedimiento y las tablas tienen el mismo dueño, SQL Server permite ese acceso mediante el mecanismo de ownership chaining (encadenamiento de propiedad). Este comportamiento es clave para encapsular la lógica de negocio sin exponer directamente las tablas subyacentes.

¿Qué ocurre con los permisos cuando usamos SQL dinámico?

Aquí viene la trampa. Si dentro del procedimiento usamos SQL dinámico con sp_executesql como:

… entonces se rompe la cadena de propiedad, y SQL Server evalúa los permisos como si el usuario estuviera ejecutando directamente la consulta, no como si formara parte del procedimiento. En otras palabras: aunque el usuario tenga permiso de ejecución sobre el procedimiento, necesitará permisos explícitos de SELECT sobre la tabla referenciada en el SQL dinámico.

Esto puede llevar a errores difíciles de diagnosticar si no se comprende cómo funciona el contexto de seguridad.

Demostración del problema de permisos

Imaginemos este escenario:

  • Usuario app_user
  • Procedimiento dbo.usp_Informe
  • Tabla Sales.SalesOrderHeader
  • Escenario app_user tiene EXECUTE sobre usp_Informe, pero no SELECT sobre la tabla
  • Código del procedimiento:

El primer SELECT se ejecuta correctamente gracias al ownership chaining. El segundo da error:

Cuándo se rompe y cuándo no

Para que se mantenga el ownership chaining, se deben cumplir dos condiciones:

  • Los objetos deben pertenecer al mismo propietario (normalmente dbo)
  • La consulta no debe usar SQL dinámico
  • En cuanto usamos EXEC, sp_executesql o EXECUTE AS con un contexto diferente, se interrumpe esa cadena y SQL Server valida los permisos del usuario directamente.

¿Cómo se soluciona el problema de permisos?

Existen varias estrategias según el contexto, pero las más comunes son estas:

1. Firmar el procedimiento con un certificado

Es la solución más profesional. Se firma el procedimiento con un certificado que tenga los permisos necesarios sobre las tablas, y el usuario ejecuta el procedimiento sin tener permisos directos. Requiere más trabajo, pero es la solución más segura y escalable, especialmente en entornos regulados o críticos.

2. Conceder permisos explícitos sobre las tablas

Es la opción más directa pero rompe el aislamiento que buscamos al encapsular la lógica dentro de procedimientos. Puede ser válido en entornos internos o controlados.

Pero en ese caso ya no protegemos las tablas detrás de la lógica del procedimiento.

3. Evitar usar SQL dinámico innecesariamente

Si el acceso a los datos no requiere construir partes dinámicas (columnas, nombres de tabla, filtros condicionales avanzados), es mejor mantener el SQL como texto plano. Así se conserva el contexto de permisos del procedimiento.

En ocasiones, usamos SQL dinámico cuando lo que realmente necesitamos es ejecutar una u otra consulta en función de un parámetro de entrada del procedimiento. En estos casos es mejor crear una lógica con IF que códigos dinámicos. 

Por ejemplo, este procedimiento con código SQL dinámico:

… lo podríamos sustituir por este:

Conclusión

El uso de SQL dinámico no es solo una cuestión de sintaxis o seguridad frente a inyecciones. También tiene implicaciones directas en el modelo de permisos y seguridad de SQL Server. Es importante entender que al usar sp_executesql, el procedimiento pierde la protección que le daba la cadena de propiedad, y el motor evalúa los permisos como si se tratase de una ejecución independiente. Este comportamiento puede ser confuso si no se conoce, pero una vez lo interiorizamos, se convierte en una herramienta poderosa para diseñar arquitecturas seguras y mantenibles. Si estamos diseñando procedimientos que deben proteger las tablas subyacentes, debemos considerar firmar con certificados, controlar cuidadosamente los permisos, o bien evitar SQL dinámico cuando sea posible.  

En el artículo anterior explicamos cómo generar SQL dinámico de forma segura desde el punto de vista sintáctico y de rendimiento. Pero ahora, sabemos que también hay que hacerlo de forma segura desde el punto de vista del contexto de ejecución.

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, SQL Server, 0 comentarios

Código dinámico con seguridad en SQL Server

En muchas ocasiones nos enfrentamos a escenarios donde necesitamos construir sentencias SQL de forma dinámica. Ya sea para crear filtros condicionales, construir cláusulas ORDER BY en tiempo de ejecución o ejecutar consultas sobre distintos objetos, la generación de SQL dinámico parece una solución sencilla y flexible. Pero esta potencia viene acompañada de riesgos, especialmente desde el punto de vista de la seguridad.

A lo largo de los años, he visto cómo el uso descuidado del SQL dinámico ha sido uno de los vectores de ataques de inyección SQL (SQLi) más comunes, y aún sigue siéndolo. Por eso, en este artículo vamos a repasar cómo generar SQL dinámico en SQL Server de forma segura, analizando técnicas recomendadas y errores frecuentes, con ejemplos claros y aplicables.

¿Cuándo usamos SQL dinámico?

Los escenarios más frecuentes en los que aparece la necesidad de SQL dinámico suelen estar relacionados con filtros condicionales, búsquedas avanzadas, generación de informes personalizables, lógica multi-tenant o incluso mantenimiento automatizado.

Un ejemplo muy habitual es una búsqueda con varios filtros opcionales. Supongamos una aplicación que consulta una tabla de personas donde el usuario puede buscar por nombre, ciudad y país. Si tratamos de resolver esto con un procedimiento estándar, el número de combinaciones posibles puede crecer exponencialmente. El SQL dinámico permite construir la sentencia ajustada a los filtros que el usuario haya proporcionado.

El problema del SQL Injection

El gran riesgo del SQL dinámico mal implementado es el conocido SQL Injection, una técnica con la que un atacante puede alterar la consulta ejecutada para acceder o modificar datos sin autorización. Esto ocurre cuando concatenamos directamente valores dentro de la cadena SQL. Veamos un ejemplo inseguro:

Si @city proviene de un parámetro externo (una app, una web), el usuario podría inyectar algo como: “Madrid’; DROP TABLE Person.Person;” y provocar un desastre.

Este patrón, por desgracia, sigue viéndose demasiado a menudo en aplicaciones heredadas o mal diseñadas.

Uso seguro de SQL dinámico con sp_executesql

La solución más eficaz ante este problema es usar sp_executesql, que permite construir consultas dinámicas pero separando el código de los datos mediante parámetros tipados. Esto bloquea cualquier intento de inyección porque el valor del parámetro no se interpreta como código. Reescribamos el ejemplo anterior de forma segura:

Aquí, aunque el usuario intentase inyectar código en @city, no lo conseguiría. SQL Server lo tratará como un valor, no como una parte de la instrucción.

Además, esta técnica también permite la reutilización de planes de ejecución, lo que supone una ventaja adicional en términos de rendimiento.

Código dinámico con filtros condicionales

Un paso más allá es cuando necesitamos construir dinámicamente múltiples filtros. En estos casos, lo ideal es ir concatenando las condiciones SQL pero parametrizando todos los valores. Veamos un ejemplo más completo:

De este modo, solo se añaden los filtros que tengan valor, pero todos los valores siguen protegidos mediante parámetros.

Identificadores dinámicos: el caso más delicado

sp_executesql no permite parametrizar nombres de columnas o tablas. Esto es especialmente importante si necesitamos cambiar el objeto sobre el que se ejecuta la consulta. En estos casos debemos concatenar el identificador, pero asegurándonos de que el valor es válido. La función QUOTENAME es clave para evitar inyecciones sobre identificadores ya que introduce el nombre del objeto entre corchetes [].

Aquí estamos asumiendo que el nombre de la tabla ha sido validado previamente. Aún así, QUOTENAME evita que un valor como SalesOrderHeader; DROP TABLE x;– pueda hacer daño. 

En entornos multi-tenant esto es especialmente útil si cada cliente tiene su propia tabla (modelo database-per-tenant) y accedemos a ellas dinámicamente.

Consejos adicionales para SQL dinámico seguro

Cuando usamos SQL dinámico en entornos críticos o expuestos a usuarios externos, es fundamental aplicar otras prácticas complementarias como encapsular en procedimientos almacenados ya que así reducimos la exposición del motor y permitimos auditar más fácilmente. Otra buena práctica es registrar las consultas generadas, esto es especialmente útil para soporte, auditoría y detección de patrones de abuso. Otro paso obligatorio, para mi es evitar privilegios excesivos; el usuario que ejecuta el código dinámico no debe tener más permisos de los necesarios.

Por último, si queremos ir un paso más allá, podemos aplicar SET FMTONLY OFF y otras opciones de seguridad sobre todo si trabajamos con herramientas de terceros. De esta manera podremos asegurarnos de que el motor de base de datos ejecuta todo el bloque tal cual, sin modificar el flujo por culpa del modo de metadatos.

Conclusión

El SQL dinámico en SQL Server puede ser tan útil como peligroso. En nuestras manos está la diferencia entre construir una solución flexible y robusta o abrir una puerta trasera a posibles ataques.

La clave está en nunca concatenar valores directamente y utilizar sp_executesql con parámetros siempre que sea posible. Cuando se trabaja con nombres de objetos, debemos validar y proteger con QUOTENAME. Y si el contexto lo permite, encapsular toda la lógica dentro de procedimientos controlados.

Estas técnicas yo las aplico habitualmente en proyectos reales, y son parte esencial de una arquitectura segura, especialmente en entornos donde la escalabilidad o la “multi-tenencia” requieren cierta flexibilidad a nivel de metadatos. 

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, SQL Server, 1 comentario