Teoría BBDD

Grupos de alta disponibilidad Always On Distribuidos

Hay momentos en la vida de un DBA en los que la alta disponibilidad ya no es suficiente. Hemos sobrevivido a clústeres fallidos, a discos compartidos corruptos y a réplicas que se desconectan con la gracia de una llamada de Teams a las 3 de la madrugada. En ese punto, cuando el entorno ya es exigente, el SLA nos aprieta y la palabra «disponibilidad» se queda corta, es cuando entramos en el terreno de los Availability Groups distribuidos.

Y no, no hablamos de tener un par de réplicas en distintas zonas de disponibilidad dentro del mismo centro de datos. Hablamos de desacoplar entornos completos en distintas regiones, con su propio clúster, su propia lógica de failover, y aún así, mantener la sincronización de nuestras queridas bases de datos. Bienvenidos al club.

¿Qué demonios son los AG distribuidos?

Un AG distribuido (Distributed Availability Group, para los amigos) es una especie de “supergrupo” de alta disponibilidad. No estamos montando un único AG con réplicas dispersas. Estamos conectando dos AG independientes como si fueran nodos de un mismo sistema. Y eso lo cambia todo.

Cada uno de los AG que participan en un DAG (sí, también lo llaman así) tiene su propia topología: su clúster de Windows, sus réplicas, su configuración de quórum, su failover automático. Se comportan como entornos autónomos, que además se sincronizan entre sí a través de una réplica intermedia que hace de “puente” entre ambos mundos. ¿El resultado? Podemos tener un AG en Madrid y otro en Dublín, y replicar entre ellos de forma controlada, tolerando fallos completos del entorno primario sin perder la cabeza.

No es magia. Es ingeniería. Y, como todo lo potente, requiere cabeza y experiencia.

Arquitectura de un DAG: dos mundos bien separados

La gracia del AG distribuido es que cada uno de los AG participantes se gestiona por separado. Esto nos da una ventaja brutal en términos de recuperación ante desastres: si uno se cae por completo, el otro puede activarse sin necesidad de que el primero esté en línea. No hay dependencia directa entre ellos, ni a nivel de clúster, ni de quorum, ni de discos compartidos. Cada AG es soberano en su dominio, y eso, en un mundo donde los CPDs pueden arder, es oro puro.

La configuración básica requiere lo siguiente:

  • Dos AG independientes, cada uno con su propio clúster de Windows.
  • Al menos una réplica en cada AG que actuará como “forwarder” (la que se conecta con el otro AG).
  • Comunicación de red directa entre las réplicas forwarders (y permisos adecuados, claro).
  • Certificados válidos en ambos entornos si usamos endpoints con autenticación por certificado (spoiler: casi siempre).
  • DNS y nombres de servidor que no se pisen entre sí. Si tienes dos réplicas llamadas SQLSRV01 en distintas regiones, vas a llorar.

¿Resultado? Una topología que no depende de un solo clúster de Windows, que permite failover regional controlado, y que puede integrarse perfectamente con estrategias de backup y DR serias.

La sincronización es asíncrona. Y eso está bien

Una pregunta habitual: ¿puedo tener un DAG con sincronización síncrona?

Respuesta corta: no.

Respuesta larga: la sincronización entre los dos AG de un DAG siempre es asíncrona. No es negociable. Y antes de que alguien lo vea como una limitación, recordemos que estamos hablando de replicar entre regiones completas, separadas por cientos o miles de kilómetros. Pedir sincronía aquí es como pedirle baja latencia a una conexión satelital. Lo importante es entender que esto no es para alta disponibilidad de datos en tiempo real, sino para continuidad del negocio ante desastres mayores.

Si necesitamos RPO cero, esto no es para nosotros. Si podemos tolerar unos segundos (o minutos) de pérdida en caso de caída total del entorno primario, entonces un DAG puede ser nuestra mejor baza.

¿Qué se puede hacer con Always On Distribuidos?

Lo primero y más importante: podemos tener una réplica totalmente operativa en otra región, lista para convertirse en nueva primaria si la original cae. Esta réplica puede formar parte de un AG secundario completo, con su propio listener, sus propias réplicas locales, y servir tráfico de lectura si así lo queremos.

Lo segundo: la recuperación ante desastres se convierte en un proceso autónomo. Si se va la luz en todo nuestro entorno primario (sí, ha pasado), podemos activar el AG secundario sin tener que reconstruir el entorno entero o esperar a que vuelva a estar online. Y esto no solo ahorra tiempo: salva el negocio.

Y lo tercero: permite hacer actualizaciones o migraciones sin downtime completo. Podemos preparar todo en el AG secundario, sincronizar, hacer el failover distribuido, y luego ajustar el entorno primario con tranquilidad. Es una forma de reducir riesgos en cambios grandes sin jugar al funambulista con el entorno de producción.

El failover en los AG Distribuidos: aquí mandamos nosotros

Un punto clave en los AG distribuidos es que no hay failover automático entre los dos AG. Esto es deliberado. No estamos hablando de un nodo que se cae por una hora y queremos rebotarlo automáticamente. Estamos hablando de fallos graves, donde lo último que queremos es que el sistema tome decisiones por su cuenta sin entender el contexto.

Por eso, el failover entre AGs distribuidos es manual. Y eso está bien. Podemos automatizarlo con scripts, orquestarlo con herramientas de gestión, y tenerlo documentado al detalle. Pero el botón rojo lo apretamos nosotros, cuando hemos validado que el entorno primario está realmente muerto y no va a levantarse en los próximos 5 minutos.

Una vez hacemos el failover, el AG secundario se convierte en el nuevo primario del DAG. Desde ahí, podemos operar normalmente, restaurar backups, servir peticiones y mantener el negocio en marcha. Cuando el entorno primario vuelva, podremos reintegrarlo en el DAG, pero el proceso no es automático ni trivial. Hay que hacerlo bien, con scripts preparados, sin improvisar.

AG Distribuidos en la nube: promesas, realidades y facturas

Cuando nos movemos a la nube —sea Azure, AWS o cualquier otro proveedor que venda disponibilidad como si fuera pan caliente—, los DAG siguen siendo perfectamente viables, pero el terreno cambia. En Azure, por ejemplo, podemos montar un DAG entre dos regiones usando máquinas virtuales con clústeres de Windows tradicionales o bien utilizando SQL Server en instancias de Azure VM con ILB (Internal Load Balancer) para simular el listener. Lo mismo ocurre en AWS, donde los DAG pueden desplegarse entre zonas de disponibilidad o incluso regiones distintas, aunque allí la gestión de redes, rutas y permisos puede volverse un pequeño infierno si no se domina bien el entorno VPC.

La ventaja clara en la nube es la infraestructura: tenemos latencias razonables entre regiones, almacenamiento redundante, y posibilidad de automatizar el despliegue completo con plantillas (CloudFormation, Terraform u otro, según tus gustos). Pero también hay que tener en cuenta que el coste de mantener dos entornos completos sincronizados no es trivial. Especialmente si se usan discos premium, réplicas activas y tráfico constante entre regiones.

Además, muchos se olvidan de que en la nube no hay testigos compartidos para el quórum, lo que obliga a diseñar bien la lógica de los clústeres para evitar split-brain. Y ojo con los nombres DNS y los certificados: en la nube, los nombres internos de las máquinas cambian, los certificados caducan cuando nadie mira, y el tráfico entre regiones puede requerir ajustes de firewall que no siempre están bien documentados. En resumen: se puede, se debe, pero hay que saber lo que se está haciendo. Porque aquí, equivocarse cuesta dinero. Literalmente.

Otra opción sería tener un AG en local y el distribuido en la nube. De esta manera reducimos costes y tenemos lo mejor de ambos mundos, la infraestructura local controlada y con menor latencia desde nuestra red y la copia segura en la nube replicada casi en tiempo real. Lista para una actuación de emergencia, como ese seguro de viajes que pagas pero deseas no tener que usar nunca.

Cosas que pueden salir mal (y lo harán)

Como todo en SQL Server que involucra clústeres, redes y nombres, hay margen para el desastre elegante. Algunas perlas:

  • Si los certificados no están bien configurados en ambos lados, la réplica forwarder no podrá conectar. Y lo descubrirás justo cuando más prisa tengas.
  • Si los nombres de los nodos se repiten en ambas regiones, los endpoints fallarán y la sincronización no se iniciará.
  • Si los puertos necesarios están bloqueados por firewalls, los AG estarán técnicamente configurados pero no replicarán una coma.
  • Si no documentas el proceso de failover, nadie sabrá qué hacer cuando llegue el momento. Especialmente tú, bajo presión.

Por eso, cualquier DAG serio necesita una prueba completa de recuperación. Hay que simular la caída del entorno primario, validar el failover al secundario, probar el listener, comprobar la latencia, y luego revertir. Si no has hecho esto al menos una vez, tu DAG es una promesa. No una solución.

Conclusión

Un AG distribuido no es para todo el mundo. Tiene complejidad, tiene costes y tiene curva de aprendizaje. Pero si lo que queremos es resiliencia real frente a desastres a nivel de región, y si nuestro negocio no se puede permitir estar horas (o días) sin SQL Server, entonces es una de las mejores inversiones técnicas que podemos hacer.

Eso sí: que nadie lo monte “porque queda bien en el diagrama”. Un DAG sin pruebas, sin documentación y sin monitoreo adecuado es una trampa. Pero bien hecho, es una fortaleza. Una que sigue operativa cuando el resto del castillo se cae.

Y en tiempos de incertidumbre, eso vale más que cualquier SLA firmado con letra bonita.

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

Usar siempre dbo en SQL Server: ventajas, riesgos y buenas prácticas

Después de analizar cómo funciona la propiedad db_chainig y los riesgos de activar cross db ownership chaining a nivel de instancia, nos queda un tema que está en el centro de todo: el uso del usuario y esquema dbo de forma sistemática. Es una práctica habitual, cómoda y hasta cierto punto tradicional en SQL Server, pero que conviene revisar con detalle cuando hablamos de seguridad, despliegues controlados y gobernanza de datos.

¿Realmente es buena idea que todo pertenezca a dbo? ¿Qué consecuencias tiene a medio y largo plazo? ¿Dónde está el equilibrio entre comodidad y control?

Qué es dbo y por qué se usa tanto

El usuario dbo (database owner) es un usuario especial en cada base de datos de SQL Server. Está vinculado al propietario real de la base de datos y tiene todos los privilegios dentro de ella. Cuando creamos objetos sin especificar un esquema, y el usuario con el que trabajamos está asignado como dbo, los objetos se crean bajo ese esquema por defecto.

Esto significa que en la mayoría de los entornos, a menos que se especifique lo contrario, todos los procedimientos, tablas, vistas y funciones acaban siendo del esquema dbo y perteneciendo al usuario dbo.

Esta práctica tiene una ventaja clara: evita problemas de permisos y simplifica el acceso a los objetos, sobre todo en proyectos donde no se quiere dedicar tiempo a gestionar esquemas ni propietarios. En resume, es una forma de trabajar directa y suele «funcionar a la primera». Pero también tiene implicaciones importantes que no se deben ignorar.

Seguridad: encadenamiento de propietarios dbo y permisos no deseados

Cuando todos los objetos son propiedad de dbo, es muy fácil que se establezcan cadenas de confianza sin querer. Esto es justo lo que habilita la propiedad db_chainig cuando está activa, y lo que se multiplica peligrosamente si además se activa cross db ownership chaining a nivel de instancia.

Al compartir el mismo propietario, los objetos pueden acceder entre sí sin que se comprueben los permisos del usuario que ejecuta la operación. Esto puede ser deseable en ciertos diseños bien controlados, pero también abre la puerta a que procedimientos maliciosos o mal diseñados escapen a la lógica de seguridad prevista, sobre todo cuando hay múltiples bases de datos con el mismo esquema y propietario.

Además, si todos los objetos son de dbo, resulta más difícil aplicar políticas de control por esquema o restringir accesos según áreas funcionales, ya que todo está en el mismo «cajón».

Mantenimiento y legibilidad: lo que hoy parece fácil con dbo, mañana es un dolor

Usar siempre dbo dificulta la organización lógica de los objetos. Cuando una base de datos empieza a crecer, no hay forma rápida de agrupar o identificar objetos por módulo funcional, equipo responsable o ciclo de vida.

Tampoco ayuda en tareas como generar scripts de despliegue por módulos, aplicar permisos diferenciados según esquema, monitorizar la actividad por subsistemas o delegar administración de partes concretas de la base de datos.

Todo eso requiere una separación explícita por esquemas o, al menos, una política clara de nomenclatura y propiedad. Si todo está bajo dbo, estas tareas se convierten en búsquedas manuales y excepciones constantes.

Auditoría y trazabilidad: todos los caminos llevan a dbo

Desde el punto de vista de la auditoría, tener todos los objetos bajo dbo borra cualquier pista sobre quién creó o modificó qué. El seguimiento de cambios se vuelve opaco, y las herramientas de trazado o revisión de permisos no pueden distinguir entre distintos propietarios lógicos. Esto se agrava si además usamos usuarios compartidos o impersonificación (EXECUTE AS), donde es muy difícil reconstruir el contexto original de ejecución.

Cuando una base de datos es pequeña y la usa un solo equipo, esto puede parecer un problema menor. Pero en sistemas colaborativos, auditados o con cumplimiento normativo, no tener separación de propietarios o esquemas puede ser un problema serio.

Despliegue controlado: lo que no se define, se rompe

Si trabajamos con plantillas de despliegue, versionado de bases de datos o pipelines CI/CD, usar siempre dbo limita la capacidad de definir entornos reproducibles y con permisos granulados. No se puede diferenciar entre objetos internos, públicos, temporales o propios de cada equipo.

Además, algunos errores de despliegue tienen que ver precisamente con suposiciones implícitas sobre el esquema o el propietario: si un script no especifica dbo y el usuario por defecto cambia, el objeto puede terminar en otro esquema, provocando errores sutiles y difíciles de detectar.

¿Cuándo sí tiene sentido usar dbo?

No todo es negativo. Hay escenarios donde usar exclusivamente dbo puede ser razonable. Por ejemplo en bases de datos monolíticas mantenidas por un solo equipo, en proyectos personales o experimentales donde no hay requisitos de seguridad ni mantenimiento a largo plazo o en aplicaciones donde toda la lógica de negocio está encapsulada y no se expone acceso directo a las tablas.

En estos casos, usar dbo reduce fricción y permite concentrarse en el desarrollo funcional. Pero, incluso en estos casos, conviene tener claro que estamos asumiendo una simplificación consciente, no una buena práctica general.

Buenas prácticas recomendadas: ¿dbo si o no?

Si queremos mantener un equilibrio entre simplicidad y control, podemos aplicar algunas de estas pautas.

La primera sería no usar dbo y usar esquemas distintos para separar áreas funcionales, como ventas, compras, auditoria, etc. Especialmente debemos evitar crear objetos como dbo si el usuario final no debe tener control total. En estos casos asignar esquemas a roles o usuarios concretos, y establecer políticas de permisos basadas en ellos es clave. Además, definir el esquema siempre de forma explícita en scripts y objetos programables es imprescindible para que, si un usuario tiene otro esquema por defecto, todo siga funcionando. Por último documenta siempre la política de propiedad y esquema como parte del diseño de cada base de datos.

Con estas simples recomendaciones podrás mantener la claridad y la organización incluso en proyectos con muchos objetos o múltiples desarrolladores.

Conclusión

Usar dbo para todo es cómodo, pero a menudo es una señal de diseño descuidado o sin política de seguridad clara. Aunque puede parecer una solución rápida, a largo plazo complica el mantenimiento, debilita el control de accesos y dificulta la trazabilidad.

En bases de datos pequeñas o entornos cerrados, puede no suponer un problema. Pero si trabajamos en entornos colaborativos, con ciclos de vida largos, requisitos de auditoría o despliegues automatizados, separar esquemas y propietarios deja de ser una opción y se convierte en una necesidad.

No se trata de demonizar dbo, sino de dejar de usarlo como valor por defecto para todo. Definir y respetar una estructura clara es una de las formas más efectivas de asegurar calidad y sostenibilidad en nuestras bases de datos.

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

Cross DB Ownership Chaining en SQL Server

En el artículo anterior vimos cómo la propiedad db_chainig permite que el encadenamiento de propietarios funcione entre objetos ubicados en bases de datos distintas, siempre que esta opción esté activada en ambas bases de datos y se cumplan ciertos requisitos. Pero hay un aspecto que no abordamos en profundidad: qué ocurre cuando esta configuración se aplica no solo a nivel de base de datos, sino también a nivel de instancia, utilizando la opción cross db ownership chaining. Este ajuste tiene un comportamiento más agresivo y, en la mayoría de los entornos, representa una mala práctica de seguridad que conviene evitar.

¿Qué hace exactamente cross db ownership chaining?

Mientras que la propiedad db_chainig se puede activar o desactivar de forma individual en cada base de datos, la opción cross db ownership chaining es una configuración de instancia que fuerza la activación del encadenamiento de propietarios entre todas las bases de datos de la instancia, sin posibilidad de excluir ninguna.

Activarla equivale, a efectos prácticos, a establecer db_chainig = ON para todas las bases de datos, aunque no lo hayamos hecho explícitamente. Esto significa que cualquier procedimiento, vista, función u otros objetos en una base de datos que comparta propietario con una tabla en otra podrá acceder a ella sin que el usuario tenga permisos explícitos. Y lo más preocupante: este comportamiento se extiende automáticamente a cualquier base de datos nueva que se cree en la instancia.

Cómo activar y desactivar cross db ownership chaining

Podemos consultar el estado actual de la opción con:

Si devuelve valor 1, la opción está activada. Para desactivarla (que es lo recomendable en la mayoría de casos):

Esta configuración requiere nivel de sysadmin, ya que afecta directamente a la seguridad global del motor.

Implicaciones de seguridad

El principal riesgo de mantener esta opción activa es que se rompen los límites de seguridad entre bases de datos. En lugar de tener compartimentos estancos, con cross db ownership chaining se permite el acceso entre objetos de distintas bases de datos con solo compartir el mismo propietario. Esto debilita el aislamiento lógico que SQL Server garantiza por defecto.

Imaginemos una instancia donde convivieran varias bases de datos de distintos departamentos, proyectos o incluso clientes. Si todas ellas utilizan el esquema y usuario dbo, y se activa cross db ownership chaining, cualquier procedimiento mal diseñado o malicioso podría acceder a tablas fuera de su alcance original sin necesidad de permisos explícitos. Este escenario rompe cualquier principio de defensa en profundidad.

Además, si se utilizan funciones como EXECUTE AS, se complica todavía más el análisis de los permisos efectivos, y pueden aparecer brechas difíciles de detectar hasta que ya es tarde.

¿Hay algún caso donde activarla tenga sentido?

La única situación razonable para activar cross db ownership chaining es en entornos completamente controlados, con una única aplicación propietaria desplegada en la instancia, donde todas las bases de datos forman parte de la misma solución y no hay usuarios externos ni terceros ejecutando código dinámico o creando objetos.

Incluso en ese caso, sigue siendo preferible activar db_chainig solo en las bases de datos que lo necesiten, en lugar de aplicar un cambio de instancia que afectará a todo lo que esté ejecutándose ahora y en el futuro.

También puede tener cierta justificación en entornos de desarrollo o pruebas rápidas, donde se valore más la agilidad que la seguridad. Pero ahí conviene recordar que muchas veces lo que se prueba en desarrollo acaba en producción… y con ello los errores de configuración.

Buenas prácticas y recomendaciones

Para mantener la seguridad y el control en nuestras instancias de SQL Server, las pautas más recomendables respecto a esta configuración son claras: “Mantener cross db ownership chaining desactivado por defecto”. Siempre es más recomendable activar db_chainig solo en las bases de datos que lo requieran y siempre de forma explícita y documentada. En cualquier caso, además te recomiendo evitar el uso masivo del propietario dbo en todas las bases de datos, especialmente en entornos con múltiples aplicaciones o proyectos y revisar regularmente qué bases de datos tienen esta propiedad activada y por qué. Tampoco está de más aplicar control de cambios y auditoría cuando se modifiquen opciones de instancia.

Además, en cualquier revisión de seguridad o auditoría formal, tener esta opción activada puede ser motivo de alerta o incluso de no conformidad con normativas como ISO 27001, PCI-DSS o el Esquema Nacional de Seguridad (ENS).

Conclusión

La opción cross db ownership chaining es uno de esos ajustes que, a simple vista, puede parecer útil para evitar errores de permisos durante el desarrollo, pero que en producción representa una puerta abierta a problemas de seguridad difíciles de rastrear. Es una opción de legado que hoy en día solo deberíamos tocar si entendemos perfectamente sus implicaciones.

Si en tu entorno necesitas acceso cruzado entre bases de datos, activa db_chainig solo donde sea estrictamente necesario. Y si lo que quieres es encapsular accesos sin exponer tablas directamente, plantéate usar certificados para firmar procedimientos y mantener un modelo de permisos más seguro y robusto.

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

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

Procedimientos almacenados para auditoría en SQL Server; Cuando el «sálvese quien pueda» no es estrategia

Llevamos años insistiendo en la importancia de tener auditoría en los sistemas. Y no, no hablamos de perseguir al becario por haber hecho un DELETE sin WHERE. Hablamos de una estrategia seria, sostenible y bien diseñada para dejar constancia de qué ocurre dentro de nuestras bases de datos. Y aquí es donde los procedimientos almacenados (sí, esos viejos conocidos de mil batallas) pueden jugar un papel muy digno si sabemos usarlos con cabeza.

Porque seamos honestos: muchos sistemas de auditoría empiezan con buenas intenciones y terminan convertidos en un Frankenstein lleno de triggers, jobs ocultos y tablas que nadie se atreve a truncar. Pero no tiene por qué ser así. Vamos a hablar de cómo diseñar procedimientos almacenados que nos permitan registrar operaciones relevantes, mantener la trazabilidad sin comprometer el rendimiento, y, sobre todo, que no nos obliguen a pedir perdón cada vez que ejecutamos un SELECT contra la tabla de auditoría.

¿Por qué procedimientos almacenados y no triggers o Extended Events?

Antes de lanzarnos a picar código como si no hubiera mañana, pongamos un poco de contexto. Sí, podríamos usar triggers para capturar operaciones DML. Sí, Extended Events son una maravilla para ciertas trazas. Pero aquí venimos a hablar de soluciones controladas, personalizables y que se integren de forma limpia con nuestras operaciones diarias.

Los procedimientos almacenados nos permiten centralizar la lógica de inserción, modificación y eliminación, y al mismo tiempo registrar en una tabla de auditoría todo lo que nos interesa: quién, cuándo, qué y desde dónde. Si el acceso a los datos está canalizado a través de SPs, tenemos el terreno perfecto para auditar sin traumas. ¿Que no todo pasa por SPs? Entonces el problema no es la auditoría, es el diseño general de la aplicación. Y eso es otra guerra.

Diseño de la tabla de auditoría: no empecemos la casa por el tejado

Antes de ponernos a escribir procedimientos, necesitamos tener clara nuestra tabla de auditoría. No, no vale con un INSERTED tirado en una tabla con columnas tipo Campo1, Campo2, y una nota que diga “aquí va lo que sea”. Una buena tabla de auditoría tiene estructura, contexto y propósito. Una estructura mínima (y funcional) podría ser algo así:

Y aquí hay que hacer una pausa. Porque sí, el campo Datos contiene JSON, pero no es de tipo JSON. ¿Por qué? Porque SQL Server, hasta la versión 2022, no soporta el tipo JSON nativo en entornos on-premises. En Azure SQL Database ya existe, y todo apunta a que SQL Server 2025 lo incorporará. Pero mientras tanto, usamos NVARCHAR(MAX).

Y sí, uso JSON. No, no me he pasado al NoSQL. Pero si queremos registrar los datos afectados sin necesidad de 50 columnas por cada tabla, el JSON nos da flexibilidad. 

No porque nos guste —aunque a muchos sí—, sino porque es lo que hay. Y además, no nos engañemos, el JSON nos da lo que necesitamos: almacenar estructuras flexibles y trabajar con ellas fácilmente gracias a funciones como JSON_VALUE, OPENJSON o ISJSON(). En resumen: guardamos JSON como texto, pero con cabeza.

Procedimientos almacenados con lógica de auditoría: así sí

Ahora que tenemos clara la tabla, vamos a por la lógica. El patrón es simple: cada procedimiento almacenado que realice operaciones sobre los datos deberá incluir una llamada al procedimiento de auditoría, justo después de la operación. Imaginemos un SP de actualización de empleados. Este es un ejemplo limpio y funcional, luego vosotros lo complicáis lo que queráis.

Y el procedimiento InsertarAuditoria, que sigue siendo igual de simple:

Nada de magia negra, solo lógica clara, controlada y auditable en sí misma.

Cuándo montar auditoría (y cuándo no): porque registrar cada pestañeo no es auditar

No todo merece una entrada en la auditoría. Si registramos cada acceso, cada SELECT o cada ejecución de SP, acabaremos con una tabla de 2 TB que nadie se atreve a consultar y que ralentiza cada INSERT. Hay que auditar cambios de estado relevantes: inserciones, modificaciones, eliminaciones y, dependiendo del negocio, consultas que tengan implicaciones legales o de privacidad. Auditar todo “por si acaso” es tan útil como llevar paraguas en el desierto. Y casi igual de cómodo.

Versionado de datos vs auditoría: primos lejanos

Una confusión habitual es pensar que la auditoría y el versionado son lo mismo. Spoiler: no lo son. Auditar es dejar constancia de qué ocurrió, quién lo hizo y cómo. Versionar es guardar el estado completo anterior de un registro, por si hay que volver atrás o comparar. Podemos combinar ambos enfoques, pero no deberíamos mezclarlos como si fueran equivalentes.

Para el versionado tenemos técnicas distintas como el libro de contabilidad, o incluso estructuras propias si el estándar se nos queda corto. Si lo que queremos es saber quién cambió qué, la auditoría nos basta. Si queremos saber qué valores tenía antes, entonces toca sacar la artillería del versionado.

Auditoría y rendimiento: ese delicado equilibrio

Registrar operaciones añade carga. No importa lo minimalista que sea el SP de auditoría: es una operación más por cada modificación. Si nuestra base de datos gestiona 10.000 cambios por minuto, quizás tengamos que plantear estrategias adicionales, como colas (Service Broker, por ejemplo) o particionado de la tabla de auditoría.

Y no, no vale con poner la tabla en otra base de datos para “que no moleste”. Separar el almacenamiento está bien, pero si la transacción espera a que el log de la otra base escriba… seguimos teniendo cuello de botella.

Diseñar una auditoría eficiente es como diseñar un buen índice: no basta con saber que existe, hay que entender cómo afecta a todo lo demás.

Conclusión

Los procedimientos almacenados siguen siendo una herramienta poderosa para implementar auditorías en SQL Server, especialmente cuando tenemos control sobre cómo accede la aplicación a los datos. Nos permiten registrar cambios con contexto, mantener el rendimiento bajo control y evitar la opacidad de soluciones como triggers masivos o rastreos indiscriminados.

Y sí, usamos JSON. Aunque hoy lo almacenamos como NVARCHAR(MAX), es una decisión consciente, no un apaño. Cuando SQL Server soporte el tipo JSON nativamente, y lo hará porque ya lo hace en Azure, estaremos listos para aprovecharlo sin rehacer nada.

Así que no, no necesitas un monstruo de triggers ni una solución de terceros para saber qué está pasando en tu sistema. A veces, basta con un poco de disciplina, procedimientos bien pensados… y la firme convicción de que auditar no es opcional. Es supervivencia.

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

Orden de los registros en SQL Server cuando no usamos ORDER BY

Cuando trabajamos con SQL Server, ya seamos DBAs, analistas o desarrolladores, a menudo nos topamos con una situación que puede parecer trivial, pero que esconde una trampa para quienes no conocen el funcionamiento interno del motor de SQL Server: ¿cuál es el orden de los resultados cuando no se especifica explícitamente un ORDER BY? El otro día me lo preguntó un compañero y quería también compartirlo con vosotros. La respuesta, aunque sencilla, tiene implicaciones importantes tanto para la lógica de negocio como para el rendimiento y la consistencia de nuestras aplicaciones.

El mito del orden “natural” en SQL Server

Todos hemos oído alguna vez frases como “esta tabla siempre me devuelve los datos en orden de inserción” o “los resultados salen ordenados por la clave primaria aunque no lo indique”. Y aunque en muchas ocasiones estas afirmaciones parecen cumplirse, lo cierto es que confiar en un orden implícito es una práctica peligrosa que puede romperse en cualquier momento. SQL Server, por diseño, no garantiza ningún tipo de orden si no se especifica expresamente una cláusula ORDER BY.

El optimizador y la aleatoriedad controlada

Esto pasa porque el motor de SQL Server tiene como objetivo principal devolver el resultado correcto de la consulta en el menor tiempo posible. Esto implica que, cuando no hay una indicación explícita de orden, el optimizador tiene plena libertad para usar el plan de ejecución que considere más eficiente, incluso si eso implica devolver los datos en un orden distinto cada vez que se ejecuta la misma consulta.

La forma en que SQL Server accede a los datos —ya sea mediante un table scan, index scan, index seek, lookup o incluso los hash match— influye directamente en el orden en el que los registros son devueltos. Y como estos planes pueden variar en función de las estadísticas, la carga del sistema o incluso la edición de SQL Server, el orden final de los resultados es impredecible.

El rol de los índices: ¿orden oculto?

Es cierto que muchas veces el orden “parece” coincidir con el de un índice, en especial cuando se usa un index scan. Por ejemplo, si tenemos una tabla con una clave primaria basada en un índice clustered, es habitual que un SELECT * FROM Tabla sin ORDER BY devuelva los datos según ese índice clustered. Pero esto no es una garantía, ni una promesa del motor.

Un cambio en el plan de ejecución, una actualización estadística, o una simple alteración en el número de registros puede hacer que SQL Server decida usar otro índice, o incluso hacer un table scan, y romper ese orden «natural».

Casos prácticos: cuándo cambia el orden y por qué

Supongamos una tabla de pedidos con una clave primaria sobre OrderID. Si hacemos:

En la mayoría de las ejecuciones obtendremos los datos ordenados por OrderID. Pero si añadimos un WHERE, un JOIN, un TOP, un GROUP BY, o incluso una clausula INCLUDE en un índice, el plan de ejecución puede variar, y con ello el orden.

En una prueba con AdventureWorks, podréis observar cómo una simple adición de una condición WHERE puede provocar un cambio de plan de ejecución de un Clustered Index Scan a un NonClustered Index Seek, seguido de un Key Lookup, y con ello se alterará el orden de los resultados.

El problema de confiar en el azar

Imaginemos que una aplicación espera que los resultados vengan ordenados por fecha. La visualización puede estar funcionando correctamente durante meses, pero tras una reorganización de índices o una actualización del motor, el plan de ejecución cambia. De pronto, los datos aparecen en orden aparentemente aleatorio. El fallo no está en SQL Server, sino en haber confiado en algo que nunca fue una garantía.

A nivel de lógica de negocio, esta suposición puede tener consecuencias nefastas, especialmente en procesos de paginación, importaciones, exportaciones de datos o cálculos acumulativos.

El caso especial de las funciones TOP sin ORDER BY

Un caso particularmente peligroso es el uso de TOP(n) sin un ORDER BY. Por ejemplo:

¿El primer registro? ¿Según qué criterio? Puede que hoy obtengamos un empleado llamado «Tolomeo», mañana «Herminia» y pasado «Anacleto». El motor devolverá el primero que encuentre según el plan actual, que puede cambiar sin previo aviso. Este es uno de los errores más comunes en desarrollo, y conviene tenerlo siempre presente.

Orden en columnstore, tablas temporales y paralelismo

Cuando trabajamos con índices columnstore, la aleatoriedad del orden aún se amplifica más. Este tipo de almacenamiento columnar está optimizado para escaneos masivos, y el orden de los registros no es algo relevante desde el punto de vista del motor. Además, el uso de paralelismo, buffers intermedios y reordenamientos internos hacen que cada ejecución pueda devolver los datos en un orden distinto. 

En consultas complejas con operaciones UNION, CTE, tablas temporales o incluso operadores spool intermedios, la combinación de resultados puede alterar el orden. Una operación Merge Join, por ejemplo, puede forzar un orden intermedio, pero si se sustituye por un Hash Match debido a un cambio en las estadísticas, ese orden desaparece.

¿Y en Azure SQL?

En el caso de bases de datos en Azure, ya sea en modo SQL Database o en SQL Managed Instance, el comportamiento es exactamente el mismo. La arquitectura del servicio no cambia esta característica. De hecho, dado que el motor puede escalar dinámicamente o balancear cargas, es aún más crítico no confiar en ningún tipo de orden implícito.

Conclusión

Como práctica profesional, debemos asumir que ninguna consulta en SQL Server garantiza un orden si no usamos expresamente la cláusula ORDER BY. Esta regla es especialmente importante cuando diseñamos procedimientos almacenados, informes o integraciones que dependen del orden de los datos.

Aunque el comportamiento “parezca” consistente, no debemos basarnos en lo que ocurre hoy, sino en lo que el motor puede decidir hacer mañana. En nuestras pruebas y validaciones, es conveniente forzar el uso de ORDER BY siempre que sea necesario incluso cuando el conjunto de datos es pequeño, para asegurar que nuestras aplicaciones sean consistentes, mantenibles y previsibles. 

Pero cuidado, como suelo comentar en mis formaciones, el ORDER BY es una operación especialmente costosa, no debemos abusar de ella cuando no sea necesario tener ordenados los datos. Sin embargo, esto no quiere decir que sea solo un adorno estético para los datos, es una parte fundamental de la lógica de las consultas. Sin él, estamos a merced del optimizador, y eso nunca es buena idea.

¿Te gustaría que prepare un vídeo para youtube con pruebas para demostrar visualmente estos comportamientos? Pídemelo y lo montamos.

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