SQL Server

Buenas prácticas al definir procedimientos almacenados

El uso de procedimientos almacenados es una de las bases del desarrollo robusto en SQL Server. Aportan encapsulamiento, seguridad, rendimiento y reutilización del código. Pero como cualquier otro elemento persistente de base de datos, su eficacia no reside solo en lo que hacen, sino en cómo están definidos y estructurados. En este artículo repasaremos una serie de buenas prácticas que deberíamos aplicar de forma sistemática al crear procedimientos almacenados, tanto desde el punto de vista funcional como desde una perspectiva de mantenimiento y rendimiento.

Estructura básica de los procedimientos almacenados

Todo procedimiento almacenado debería comenzar por una cabecera clara, que incluya las opciones de sesión imprescindibles. Estas opciones afectan al comportamiento del objeto y quedan almacenadas junto con su definición, por lo que no deben dejarse al azar:

Las opciones ANSI_NULLS y QUOTED_IDENTIFIER son obligatorias para garantizar compatibilidad con vistas indexadas, funciones deterministas y otras funcionalidades. Y SET NOCOUNT ON evita el envío innecesario de mensajes como “(x filas afectadas)”, lo cual mejora el rendimiento en operaciones masivas o dentro de cursores.

Nombres coherentes y significativos para procedimientos almacenados

Otro de los mejores consejos que podemos seguir a la hora de crear procedimientos almacenados es que los nombres deben seguir una convención clara y consistente. Lo ideal es utilizar un prefijo común, como el esquema dbo o app, y un verbo que indique claramente la acción. Evitemos prefijos de sistema como sp_, ya que están reservados para procedimientos del sistema. Cuando un procedimiento almacenado empieza por sp_ SQL Server realiza una búsqueda prioritaria en la base de datos master, lo que afecta al rendimiento y puede provocar colisiones de nombres. Si quieres que el prefijo especifique claramente que es un procedimiento almacenado yo soy más partidario de prefijos como usp_ (User Stored Procedure) pero, esto ya va en gustos. Te dejo unos ejemplos de lo que para mi serían buenos nombres:

  • dbo.usp_ObtenerClientesPorCiudad
  • app.usp_ActualizarStockProducto
  • dbo.usp_InsertarPedidoCabecera

Declara los parámetros de los procedimientos almacenados de forma explícita y validada

Todo parámetro debe tener un tipo de dato bien definido, preferiblemente sin utilizar tipos innecesariamente amplios como NVARCHAR(MAX) o INT si con TINYINT es suficiente. Además, es una buena práctica validar parámetros críticos antes de ejecutar lógica compleja:

Esto permite fallos controlados y evita errores más adelante que sean difíciles de rastrear. Si no quieres usar RAISERROR también puedes dejar log en una tabla diseñada para este fin.

Evita la lógica excesiva en un solo procedimiento almacenado

Un error común es concentrar demasiada lógica en un solo procedimiento, lo que dificulta su mantenimiento, pruebas unitarias y reutilización. Si un procedimiento está realizando múltiples tareas (validación, inserción, actualización, notificación…), es mejor dividirlo en partes más pequeñas y orquestarlas desde uno principal.

Además, si reutilizamos lógica compartida entre procedimientos, podemos encapsularla en funciones o en procedimientos secundarios.

Utiliza transacciones de forma segura y explícita en tus procedimientos almacenados

Si un procedimiento realiza más de una operación que debe completarse como una unidad, debemos protegerla con una transacción. Pero cuidado, de nada sirve iniciar una transacción si no vamos a contar con que puede haber errores y tenemos planificado rollback cuando sea necesario.

Esto garantiza integridad de datos y un control efectivo de los errores. Nuevamente, también puedes usar una tabla de log en vez de RAISERROR.

Documenta todos tus procedimientos almacenados

Seamos claros, para mi este punto no es opcional. Una breve cabecera que explique qué hace el procedimiento, qué parámetros recibe y qué devuelve es una inversión de tiempo que ahorra horas de análisis futuro. Especialmente en equipos grandes o proyectos duraderos, esta práctica marca una gran diferencia.

Cuidado con el uso de SELECT * en procedimientos almacenados

El uso de SELECT * dentro de código que se va a persistir puede ser cómodo al principio, pero es una mala práctica y te dará problemas en cuanto el esquema cambie. Rompe la estabilidad del contrato de salida del procedimiento y complica la integración con aplicaciones externas. En su lugar, deberíamos listar explícitamente las columnas:

Añade a tus procedimientos almacenados un control de errores robusto

Todo procedimiento que modifique datos debe incluir manejo de errores. Además del bloque TRY…CATCH, conviene capturar el código y mensaje del error para diagnóstico:

También puede ser útil registrar errores en una tabla de log para su análisis posterior.

Cuida el rendimiento de los procedimientos almacenados desde el diseño

Cuantas veces hemos oido eso de “Es que en desarrollo funcionaba…”. Cada vez que definimos un procedimiento, estamos estableciendo un plan de ejecución potencialmente reutilizable. Además, como son objetos que se van a persistir tenemos que tener en cuenta que los datos van a crecer en algún momento y el rendimiento tiene que seguir siendo óptimo. Aunque con datos de desarrollo o en etapas tempranas del proyecto pueda parecer que no hace falta invertir tiempo en la optimización te prometo que tu yo del futuro te agradecerá que lo hayas pensado a tiempo. Algunas recomendaciones básicas podrían ser:

  • No uses OPTION (RECOMPILE) si no es estrictamente necesario (casi nunca lo es).
  • Declara los parámetros con tipos de datos que coincidan con las columnas.
  • Evita subconsultas complejas innecesarias.
  • Usa EXISTS en lugar de COUNT(*) si solo interesa saber si hay filas.
  • Indexa correctamente las tablas que usas como origen o destino.

Pruebas, idempotencia y versionado de los procedimientos almacenados

En general, es buena práctica que los procedimientos sean idempotentes si su función lo permite (es decir, que al ejecutarse más de una vez no cambien el resultado final).

Además, es recomendable mantener versiones numeradas y fechadas en sistemas donde los procedimientos evolucionan con el tiempo, y anotar los cambios relevantes en el propio código fuente.

Conclusión

Definir procedimientos almacenados no es solo cuestión de escribir lógica T-SQL funcional. Es una disciplina que requiere previsión, consistencia y enfoque estructurado. Aplicando estas buenas prácticas, conseguimos objetos más fiables, mantenibles y adaptables a largo plazo.

Tengo en mi lista de posibles futuros artículos aspectos complementarios a este artículo como procedimientos para auditoría, patrones para operaciones masivas o generación dinámica de SQL con seguridad. Mientras tanto, si aún no lo has hecho, te invito a revisar nuestro artículo sobre opciones SET imprescindibles al crear procedimientos, un complemento perfecto para consolidar estos conceptos.

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

Opciones SET: configuraciones que transforman SQL Server

En nuestro día a día trabajando con SQL Server, a menudo escribimos consultas y procedimientos almacenados sin detenernos a pensar en el contexto de ejecución que los rodea. Sin embargo, en segundo plano, hay una serie de configuraciones que condicionan profundamente cómo se interpretan, ejecutan y optimizan nuestras instrucciones T-SQL. Me refiero a las opciones que se establecen con la instrucción SET, y que van mucho más allá de unas simples “banderas”.

En este artículo repasamos las opciones SET más relevantes, agrupándolas por su impacto funcional y destacando aquellas que no deberían faltar en ningún entorno profesional. No todas las opciones son igual de conocidas, pero su uso adecuado marca la diferencia entre un código fiable y uno plagado de inconsistencias.

¿Qué hace realmente SET en T-SQL?

La instrucción SET en T-SQL permite establecer opciones de sesión que afectan al comportamiento del motor en aspectos como comparación de valores nulos, comportamiento de las transacciones, uso de cursores, sintaxis permitida o restringida, interpretación de identificadores o precisión y formato de resultados

Estas opciones afectan a la sesión actual y, en algunos casos, se heredan en la creación de objetos como procedimientos, vistas o funciones. Por ello, es fundamental entender qué estamos configurando, especialmente en entornos donde se combinan herramientas de desarrollo, procesos automatizados y ejecución mediante SQL Server Agent.

Opciones SET más críticas para el desarrollo y despliegue

Algunas opciones son especialmente relevantes porque afectan de forma directa a la definición y comportamiento de objetos persistentes. Estas deben estar activadas siempre antes de crear procedimientos almacenados, índices filtrados, vistas indexadas o columnas calculadas indexadas.

ANSI_NULLS

Ya lo analizamos en profundidad en este artículo sobre ANSI_NULLS, pero recordamos que debe estar activado para permitir comparaciones nulas según el estándar ANSI. Obligatorio para vistas indexadas, columnas calculadas e índices filtrados.

QUOTED_IDENTIFIER

Controla si las comillas dobles se interpretan como delimitadores de identificadores. Debe estar activado para usar funcionalidades modernas como índices XML, MERGE, vistas indexadas y más. Lo explicamos en detalle en este artículo dedicado.

ANSI_PADDING

Afecta a cómo se almacenan los valores en columnas CHAR y VARCHAR, así como BINARY y VARBINARY. Su valor queda grabado en la definición de columnas y también es imprescindible al crear índices en columnas de longitud fija.

Opciones SET que afectan a la lógica de ejecución

Algunas opciones SET influyen directamente en cómo se evalúan las instrucciones T-SQL, en especial las condiciones, errores y tipos de datos.

ARITHABORT

Si está activado, provoca que una consulta se detenga ante errores de desbordamiento aritmético (como división por cero). Es obligatorio tenerlo activado para usar vistas indexadas y estadísticas precisas durante la optimización de consultas.

CONCAT_NULL_YIELDS_NULL

Controla si concatenar NULL con una cadena devuelve NULL o no. Es recomendable mantenerlo activado para seguir el comportamiento ANSI:

NUMERIC_ROUNDABORT

Si está activado, cualquier redondeo de datos DECIMAL o NUMERIC provocará un error. Por defecto está desactivado, y debe permanecer así si queremos trabajar con vistas indexadas o funciones deterministas.

Opciones SET que afectan a transacciones y control de errores

Vamos a ver ahora esas opciones SET que definen cómo se van a comportar nuestras transacciones, en especial ante errores.

XACT_ABORT

Al activarlo, cualquier error en una transacción hace que se aborte automáticamente, lo que evita estados intermedios o inconsistentes. Es especialmente útil cuando se trabaja con transacciones distribuidas o procedimientos de mantenimiento.

IMPLICIT_TRANSACTIONS

Al activarse, cada instrucción que modifica datos inicia una transacción automáticamente, que debe cerrarse de forma explícita. Aumenta el control, pero puede provocar bloqueos si se olvida un COMMIT o ROLLBACK.

Opciones SET que afectan a cursores y resultados

Otro de los grupos de opciones SET son las que afectan a cursores y a los resultados de las consultas.

CURSOR_CLOSE_ON_COMMIT

Determina si los cursores abiertos se cierran automáticamente al hacer COMMIT. Por defecto está desactivado, permitiendo que el cursor siga abierto. En general, es buena práctica mantener el control explícito de los cursores pero implica llevar cierto cuidado y acordarnos de cerrarlos cuando terminemos de trabajar con ellos.

ANSI_WARNINGS

Activa advertencias para operaciones con datos truncados, errores de tipo de datos, división por cero, etc. Debe estar activado para crear vistas indexadas y columnas calculadas.

Opciones SET de formato

Por último, vamos a ver el grupo de opciones SET que nos permiten configurar los formatos y comportamientos de los datos.

DATEFIRST, LANGUAGE, DATEFORMAT

Estas opciones controlan la interpretación de fechas y días de la semana, especialmente en funciones como DATENAME, DATEPART o expresiones con formatos ambiguos. Son críticas en sistemas multi-región o migraciones.

TEXTSIZE

Controla el tamaño máximo (en bytes) de datos TEXT, NTEXT o IMAGE devueltos por una consulta. En algunos entornos, limitarlo evita retornos innecesarios de grandes volúmenes de datos binarios.

Buenas prácticas: estandarización en scripts y entornos

Para evitar inconsistencias entre entornos, sesiones o herramientas, es recomendable fijar siempre las opciones clave de forma explícita en la cabecera de scripts de despliegue, procedimientos y vistas. Un bloque típico sería este:

Además, en los jobs de SQL Server Agent o entornos que generan T-SQL de forma dinámica, estas configuraciones deben añadirse manualmente, ya que el valor por defecto suele ser distinto al del entorno interactivo de SSMS.

Conclusión

Las opciones SET en T-SQL no son meros ajustes cosméticos, son configuraciones que determinan cómo se comporta SQL Server en aspectos fundamentales como la lógica booleana, el almacenamiento físico de los datos, el control de errores y la compatibilidad ANSI. Conocerlas y aplicarlas correctamente es una responsabilidad crítica para cualquier desarrollador o DBA. Establecerlas de forma explícita no solo previene errores, sino que garantiza que nuestros objetos sean coherentes, portables y preparados para integrarse con las capacidades más avanzadas del motor de base de datos.

¿Quieres profundizar más en el impacto de estas opciones en vistas indexadas o columnas calculadas? Te recomendamos leer nuestros artículos sobre QUOTED_IDENTIFIER y ANSI_NULLS y ANSI_PADDING, donde exploramos con más detalle cómo afectan a la creación y uso de objetos persistentes en SQL Server.

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

ANSI_NULLS y ANSI_PADDING en SQL Server: configuración esencial para objetos duraderos

En el ecosistema de SQL Server, ciertas opciones de sesión pueden parecer simples banderas de configuración sin mayor trascendencia. Sin embargo, cuando hablamos de ANSI_NULLS y ANSI_PADDING, nos referimos a directivas que afectan de forma directa y permanente a la definición de objetos como procedimientos, índices, vistas o tablas. Ignorar su correcto uso puede derivar en errores sutiles, comportamientos inesperados y problemas de compatibilidad en entornos modernos. En este artículo profundizaremos en su función, implicaciones y mejores prácticas.

Introducción: más que opciones de sesión

Tanto ANSI_NULLS como ANSI_PADDING son opciones heredadas del estándar ANSI SQL y forman parte de las configuraciones que influyen en cómo SQL Server interpreta y almacena los datos. Lo relevante es que, al igual que QUOTED_IDENTIFIER, estas opciones no solo afectan a la sesión actual, sino que quedan asociadas al objeto creado o modificado en ese contexto.

Esto significa que si definimos una tabla, índice o procedimiento con una de estas opciones desactivadas, su comportamiento futuro dependerá de ese estado, aunque cambiemos la configuración más adelante. Veamos en detalle cómo afecta cada una.

ANSI_NULLS: el comportamiento de las comparaciones con NULL

Cuando ANSI_NULLS está activado, se sigue el estándar ANSI para el tratamiento de valores nulos. Esto implica que cualquier comparación de una columna con valor NULL mediante el operador = o <> devolverá FALSE, ya que los NULL no se consideran iguales ni distintos a ningún otro valor, incluido otro NULL.

Ejemplo con ANSI_NULLS ON:

En cambio, si desactivamos ANSI_NULLS, SQL Server permite que Departamento = NULL devuelva resultados, rompiendo la lógica ANSI y provocando código no portable y difícil de mantener.

Requisitos del motor

SQL Server exige que ANSI_NULLS esté activado para crear:

  • Vistas indexadas
  • Índices en columnas calculadas.
  • Índices en columnas filtradas
  • Funciones con determinismo
  • Replicación, CDC, tablas temporales o cualquier funcionalidad avanzada

Además, al igual que con QUOTED_IDENTIFIER, el estado de ANSI_NULLS se graba con los objetos persistentes como procedimientos almacenados. Y una vez definidos, no se puede cambiar este comportamiento salvo recreando el objeto.

ANSI_PADDING: control del almacenamiento de datos tipo CHAR y BINARY

Mientras que ANSI_NULLS afecta a la lógica de comparación, ANSI_PADDING determina cómo se almacenan los valores que se insertan en columnas CHAR, VARCHAR, BINARY o VARBINARY, especialmente cuando se trata de cadenas más cortas que la longitud definida en la columna.

Cuando ANSI_PADDING está activado los valores de tipo CHAR y BINARY se rellenan con espacios o ceros hasta la longitud declarada. Además los valores de tipo VARCHAR y VARBINARY se almacenan tal cual, sin relleno y los valores NULL se almacenan correctamente.

En cambio, cuando está desactivado, los valores de VARCHAR y VARBINARY se truncan al final si terminan con espacios o ceros. Las columnas CHAR y BINARY, por su parte, mantienen su comportamiento de relleno, pero los efectos secundarios en columnas NULL o con valores dinámicos pueden ser impredecibles.

Un ejemplo concreto:

Este comportamiento puede parecer inocuo, pero si más adelante se cambia a ANSI_PADDING ON, las futuras inserciones pueden almacenarse de forma distinta, provocando inconsistencias en datos, índices o comparaciones.

Impacto permanente en la definición de tablas e índices

Uno de los efectos menos conocidos de ANSI_PADDING es que su estado queda grabado en la definición de la columna. Si creamos una tabla con ANSI_PADDING OFF, incluso si lo activamos después, las columnas seguirán comportándose como si estuviera desactivado.

Podemos comprobarlo en cualquier tabla con la vista de sistema sys.columns:

Una vez definida, la única forma de modificar el comportamiento de una columna con ANSI_PADDING incorrecto es eliminarla y volver a crearla.

Prácticas recomendadas al trabajar con ANSI_NULLS y ANSI_PADDING

En proyectos de larga duración, donde la compatibilidad, la trazabilidad y el rendimiento son críticos, lo más recomendable es activar siempre ambas opciones antes de crear cualquier objeto persistente:

Esto garantiza que las definiciones serán compatibles con futuras versiones de SQL Server, con funcionalidades avanzadas como replicación o índices filtrados, y que se alinearán con el comportamiento ANSI estándar esperado por herramientas y desarrolladores.

Consideraciones adicionales en SQL Server Agent con ANSI_NULLS y ANSI_PADDING

Al igual que ocurre con QUOTED_IDENTIFIER, cuando se ejecutan scripts T-SQL desde un job de SQL Server Agent, las opciones ANSI_NULLS y ANSI_PADDING se encuentran desactivadas por defecto. Esto puede suponer un problema importante si el script crea objetos que dependen de esas configuraciones. Por tanto, es imprescindible establecerlas explícitamente al comienzo del script del paso del job:

De esta forma evitamos errores como: “Cannot create index on column because it does not allow NULL comparisons using IS NULL” o “Column cannot be part of index because ANSI_PADDING is OFF

Conclusión

Tanto ANSI_NULLS como ANSI_PADDING son opciones fundamentales en el desarrollo y mantenimiento de bases de datos en SQL Server. Aunque puedan parecer detalles menores, su impacto es profundo y permanente en los objetos que se crean bajo su contexto.

Activarlas de forma sistemática es una buena práctica que garantiza compatibilidad, coherencia de datos y aprovechamiento de las funcionalidades modernas del motor. Como en otros aspectos del diseño de bases de datos, los pequeños detalles marcan la diferencia entre un sistema robusto y uno plagado de sorpresas silenciosas.

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

QUOTED_IDENTIFIER en T-SQL: controlando el comportamiento de las comillas

Una de las opciones de configuración menos comprendidas, pero no por ello menos importantes, en SQL Server es QUOTED_IDENTIFIER. A menudo la encontramos en los scripts generados por Management Studio, en procedimientos almacenados o al realizar tareas de mantenimiento, pero, por experiencia, rara vez se le presta la atención que realmente merece. En este artículo vamos a analizar en profundidad qué es QUOTED_IDENTIFIER, cómo afecta al comportamiento de nuestras consultas T-SQL y por qué es esencial conocer su implicación en entornos complejos.

Introducción a QUOTED_IDENTIFIER: ¿Qué hace realmente?

La opción QUOTED_IDENTIFIER determina cómo SQL Server interpreta las comillas dobles («) en una consulta T-SQL. Cuando esta opción está activada, las comillas dobles delimitan identificadores (por ejemplo, nombres de columnas o tablas), permitiendo el uso de palabras reservadas o caracteres especiales. Por el contrario, si la opción está desactivada, las comillas dobles se interpretan como literales de cadena, lo que puede romper código que dependa de identificadores entrecomillados.

El comportamiento predeterminado de SQL Server desde versiones muy tempranas es tener QUOTED_IDENTIFIER activado. Sin embargo, todavía es posible desactivarla explícitamente, y ahí es donde empiezan las particularidades.

QUOTED_IDENTIFIER ON: el estándar ANSI al que estamos acostumbrados

Cuando QUOTED_IDENTIFIER está activado, se siguen las reglas ANSI SQL: las comillas dobles delimitan identificadores, y las cadenas de texto deben ir entre comillas simples (‘). Esto permite, por ejemplo, crear una columna llamada «select» o «Column With Spaces» sin que el motor se queje por el uso de palabras reservadas o caracteres especiales.

Podemos ver este comportamiento claramente:

Además, algunas funcionalidades de SQL Server, como los índices XML, las columnas calculadas indexadas o las consultas distribuidas, requieren explícitamente que QUOTED_IDENTIFIER esté activado. Es más, al crear procedimientos almacenados, vistas, funciones o triggers, esta opción queda «grabada» junto al objeto y no puede cambiarse dinámicamente durante su ejecución.

QUOTED_IDENTIFIER OFF: compatibilidad heredada… y peligros potenciales

Cuando QUOTED_IDENTIFIER está desactivado, las comillas dobles se tratan como delimitadores de cadenas. Esto puede llevar a comportamientos inesperados si asumimos que SQL Server sigue el estándar ANSI. Por ejemplo:

Este comportamiento puede parecer trivial, pero en entornos donde el código se genera dinámicamente o se espera interoperabilidad con otros motores, las diferencias sintácticas pueden provocar errores difíciles de depurar.

Además, muchas características modernas del motor de SQL Server no funcionarán si QUOTED_IDENTIFIER está desactivado en el momento de crear el objeto. Un ejemplo frecuente es el intento de indexar una columna calculada:

Esta consulta nos va a devolver un Error 1934 porque no es posible crear un índice en una columna calculada cuando QUOTED_IDENTIFIER está desactivado.

Objetos definidos: persistencia silenciosa

Uno de los aspectos más delicados de QUOTED_IDENTIFIER es que su valor queda almacenado con el objeto al momento de su creación. Es decir, si definimos una vista o procedimiento con QUOTED_IDENTIFIER OFF, ese valor persistirá en el objeto, aunque más tarde ejecutemos SET QUOTED_IDENTIFIER ON en sesiones posteriores.

Este hecho tiene implicaciones especialmente relevantes cuando trabajamos con funcionalidades como replicación, indexed views o columnas calculadas indexadas, ya que en muchas de estas características, SQL Server exige que QUOTED_IDENTIFIER esté activado.

Un ejemplo habitual en mantenimiento de bases de datos es encontrar procedimientos que fallan al intentar recompilarse simplemente porque fueron creados originalmente con QUOTED_IDENTIFIER OFF.

Podemos comprobar la configuración de un procedimiento almacenado de la siguiente forma:

Si el valor devuelto es 0, el objeto fue creado con QUOTED_IDENTIFIER OFF.

QUOTED_IDENTIFIER y ANSI_NULLS: el tándem inseparable

Cuando tratamos con buenas prácticas en la definición de objetos de base de datos, QUOTED_IDENTIFIER suele mencionarse junto a ANSI_NULLS. Y no es casualidad: ambos son requeridos por muchas de las características avanzadas de SQL Server. Las herramientas de desarrollo como Management Studio o SSDT los activan por defecto en los scripts generados, precisamente para garantizar que los objetos resultantes no estén limitados en funcionalidades futuras. Un ejemplo típico sería:

Comportamiento en SQL Server Agent 

Aunque en el uso interactivo de SQL Server, por ejemplo desde SQL Server Management Studio, QUOTED_IDENTIFIER suele estar activado por defecto, no ocurre lo mismo en todos los contextos. Un caso especialmente importante es la ejecución de scripts T-SQL desde pasos de tipo «Transact-SQL script (T-SQL)» en los trabajos de SQL Server Agent.

En estos pasos, la configuración predeterminada de la sesión establece QUOTED_IDENTIFIER OFF. Esto puede pasar completamente desapercibido si no se especifica manualmente la opción al inicio del script, pero las consecuencias pueden ser críticas. Cualquier intento de crear índices en columnas calculadas, vistas indexadas, procedimientos almacenados que dependan de esa opción, o incluso utilizar ciertas instrucciones que exigen la semántica ANSI, fallará silenciosamente o con errores que no apuntan directamente al problema.

Para evitarlo, es recomendable incluir explícitamente la activación de QUOTED_IDENTIFIER al inicio de cualquier script ejecutado desde SQL Server Agent, especialmente si ese script crea o modifica objetos:

Esta simple precaución asegura que los comportamientos esperados de la sesión se mantendrán, alineados con el desarrollo realizado en otras herramientas como SSMS o Visual Studio.

QUOTED_IDENTIFIER en flujos de trabajo DevOps y control de versiones

En escenarios donde utilizamos herramientas de CI/CD para desplegar cambios en bases de datos, la configuración de QUOTED_IDENTIFIER cobra especial relevancia. Los generadores de scripts, como SQLPackage o herramientas de despliegue como DACPACs, se apoyan en estas configuraciones para determinar si deben aplicar ciertos cambios o recrear objetos desde cero.

No es raro encontrarse con diferencias de comportamiento entre entornos debido a inconsistencias en estas opciones. Por tanto, siempre es recomendable mantener estas configuraciones explícitas y estandarizadas a lo largo del ciclo de vida del desarrollo.

QUOTED_IDENTIFIER y el rendimiento: ¿hay impacto?

Aunque QUOTED_IDENTIFIER no afecta directamente al plan de ejecución o a la eficiencia de una consulta, sí tiene un impacto indirecto. Como hemos mencionado, su estado condiciona el uso de columnas calculadas indexadas, vistas indexadas y otras estructuras que sí mejoran el rendimiento. Por tanto, mantener esta opción activada es esencial para no limitar las posibilidades de optimización que ofrece SQL Server.

Conclusión

A estas alturas parece evidente: en la mayoría de los escenarios, debemos trabajar con QUOTED_IDENTIFIER ON. No solo garantiza compatibilidad con las funcionalidades modernas del motor de SQL Server, sino que evita ambigüedades y errores sutiles cuando se manipulan identificadores que contienen caracteres especiales o palabras reservadas.

Los únicos contextos en los que podríamos considerar desactivarlo son aquellos de compatibilidad con aplicaciones muy antiguas o migraciones específicas donde el código ya existente lo exige. Fuera de esos casos, es una práctica segura y recomendada mantener QUOTED_IDENTIFIER activado en todas nuestras sesiones y scripts.

Para cerrar, si queremos asegurarnos de que todos nuestros objetos han sido creados con QUOTED_IDENTIFIER ON, conviene hacer una auditoría periódica en nuestros entornos y revisar su uso, especialmente si trabajamos con desarrolladores múltiples o heredamos bases de datos legacy.

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, 2 comentarios
Funciones de Ventana

Funciones de Ventana

Cuando analizamos datos con SQL Server, a menudo nos encontramos con la necesidad de realizar cálculos complejos que involucren conjuntos de filas relacionados entre sí. Tradicionalmente, recurríamos a subconsultas o a la ya conocida cláusula «GROUP BY». Sin embargo, existe un conjunto de herramientas mucho más potente y elegante para abordar estas situaciones: las funciones de ventana. A lo largo de este artículo, exploraremos en profundidad qué son, cómo funcionan y cómo podemos sacarles el máximo partido en nuestras consultas en SQL Server.

¿Qué son realmente las Funciones de Ventana en SQL Server? 

Las funciones de ventana son un tipo especial de funciones que nos permiten realizar cálculos sobre un conjunto de filas que están relacionadas con la fila actual que estamos procesando. No es nada nuevo, las vimos por primera vez en 1998 en Oracle8i y fueron introducidas como parte del estándar SQL en su versión 3 en el año 2003. En SQL Server las tenemos desde la versión 2005 y posteriormente llegaron a otros sistemas de bases de datos como PostgreSQL (2009), MariaDB (2016) y MySQL (2018).

La clave de su potencia reside en que, a diferencia de las funciones agregadas tradicionales que colapsan múltiples filas en una única fila de salida (como sucede con «GROUP BY»), las funciones de ventana operan dentro de una «ventana» o «marco de ventana» definido por nosotros, devolviendo un valor para cada fila individual.

Imaginemos una tabla de ventas donde queremos calcular el total de ventas acumulado por vendedor a lo largo del tiempo. Sin una función de ventana, tendríamos que recurrir a subconsultas complejas o a cursores, lo que puede resultar ineficiente y difícil de mantener. Con una función de ventana, podemos definir una ventana que incluya todas las ventas del vendedor hasta la fecha actual, calculando el acumulado para cada venta sin perder la información de cada transacción individual.

La sintaxis fundamental para utilizar una función de ventana involucra la cláusula «OVER()». Esta cláusula es la que define la «ventana» sobre la cual la función operará. Dentro de «OVER()», podemos especificar cómo se particionarán los datos y cómo se ordenarán dentro de cada partición.

Sintaxis de las Funciones de Ventana en SQL Server

La estructura básica para emplear una función de ventana es la siguiente:

Analicemos cada uno de los componentes esenciales:

  • funcion_ventana: Aquí especificamos la función de ventana que queremos aplicar. Puede ser una función de agregación (como «SUM()», «AVG()», «MIN()», «MAX()», «COUNT()»), una función de ranking («ROW_NUMBER()», «RANK()», «DENSE_RANK()», «NTILE()»), o una función de valor («LAG()», «LEAD()», «FIRST_VALUE()», «LAST_VALUE()»). El argumento dependerá de la función específica.
  • OVER(): Esta cláusula es obligatoria para indicar que estamos utilizando una función de ventana. Es dentro de sus paréntesis donde definimos el contexto de la ventana.
  • PARTITION BY lista_de_columnas (Opcional): La cláusula «PARTITION BY» divide el conjunto de resultados en particiones basadas en los valores de las columnas especificadas. La función de ventana se aplicará de forma independiente a cada una de estas particiones. Si omitimos «PARTITION BY», la función se aplicará a toda la tabla como una única partición.
  • ORDER BY lista_de_columnas [ASC | DESC] (Opcional): Dentro de cada partición (o en toda la tabla si no hay «PARTITION BY»), la cláusula «ORDER BY» define el orden lógico de las filas. Este orden es crucial para muchas funciones de ventana, especialmente las de ranking y las que trabajan con valores de filas precedentes o siguientes. Si se omite, el orden de las filas dentro de la partición será arbitrario.
  • ROWS o RANGE especificación_de_marco (Opcional): Esta cláusula nos permite definir aún más el marco de la ventana dentro de cada partición. Podemos especificar un conjunto de filas contiguas que se incluirán en el cálculo de la función para la fila actual. Las opciones más comunes incluyen:
    •  «ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW»: Incluye todas las filas desde el inicio de la partición hasta la fila actual.
    • «ROWS BETWEEN n PRECEDING AND CURRENT ROW»: Incluye las «n» filas anteriores a la fila actual y la fila actual.
    • “ROWS BETWEEN CURRENT ROW AND n FOLLOWING»: Incluye la fila actual y las «n» filas siguientes.
    • “ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING»: Incluye todas las filas de la partición.
    • «RANGE» funciona de manera similar a «ROWS», pero en lugar de un número fijo de filas, define el marco basándose en los valores de las columnas especificadas en «ORDER BY».

Tipos de Funciones de Ventana en SQL Server

Las funciones de ventana se pueden clasificar en varios tipos, cada uno diseñado para abordar necesidades específicas de análisis:

Funciones de Agregación

Podemos utilizar las funciones de agregación que ya conocemos (SUM(),AVG(), MIN(), MAX() o COUNT()) como funciones de ventana al incluirlas con la cláusula «OVER()». La diferencia fundamental con su uso tradicional con «GROUP BY» es que, al emplearlas como funciones de ventana, no perdemos la granularidad de las filas individuales.

Por ejemplo, para obtener el total de ventas por ciudad y a la vez visualizar el importe de cada pedido individual junto con el total de su ciudad, podríamos escribir algo como:

Aquí, «SUM(order_amount) OVER (PARTITION BY city)» calcula la suma de «order_amount» para todas las filas que comparten el mismo valor en la columna «city», y este total se muestra en cada fila correspondiente a esa ciudad.

Funciones de Ranking 

Las funciones de ranking nos permiten asignar una posición o rango a cada fila dentro de una partición según un criterio de ordenación específico. SQL Server nos ofrece las siguientes funciones de ranking:

  • ROW_NUMBER(): Asigna un número secuencial único a cada fila dentro de una partición, comenzando en 1. Si hay filas con los mismos valores en la columna de ordenación, se les asignarán números diferentes según el orden en que se procesen.
  • RANK(): Asigna un rango a cada fila dentro de una partición basado en el orden de las columnas especificadas en «ORDER BY». Si dos o más filas tienen el mismo valor, recibirán el mismo rango, y el siguiente rango se saltará. Por ejemplo: 1, 2, 2, 4…
  • DENSE_RANK(): Similar a «RANK()», asigna rangos basados en el orden, pero no se salta ningún rango en caso de empate. Por ejemplo: 1, 2, 2, 3, 4…
  • NTILE(n): Divide las filas dentro de una partición en «n» grupos (aproximadamente) iguales y asigna un número de grupo (desde 1 hasta «n») a cada fila. Es útil para identificar percentiles, cuartiles, etc..

Funciones de Valor 

Las funciones de valor nos permiten acceder a valores de otras filas dentro de la misma partición (o en toda la tabla) sin necesidad de realizar joins o subconsultas. Las más utilizadas son:

  • LAG(columna, n, valor_predeterminado): Accede al valor de la «columna» en la fila que está «n» filas antes de la fila actual dentro de la partición (ordenada por «ORDER BY»). Si no existe una fila anterior en la distancia especificada, devuelve el «valor_predeterminado» (si se proporciona, sino devuelve «NULL»).
  • LEAD(columna, n, valor_predeterminado): Accede al valor de la «columna» en la fila que está «n» filas después de la fila actual dentro de la partición (ordenada por «ORDER BY»). Similar a «LAG()», permite especificar un «valor_predeterminado» si no existe una fila posterior.
  • FIRST_VALUE(columna): Devuelve el valor de la «columna» de la primera fila dentro de la partición (ordenada por «ORDER BY»).
  • LAST_VALUE(columna): Devuelve el valor de la «columna» de la última fila dentro de la partición (ordenada por «ORDER BY»). Es importante tener en cuenta que, por defecto, el marco de la ventana para «LAST_VALUE()» va desde el inicio de la partición hasta la fila actual, por lo que a menudo se utiliza con una especificación de marco como «ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING» para obtener el verdadero último valor de la partición.

Cláusulas avanzadas en Funciones de Ventana

Como mencionaba anteriormente, las cláusulas «ROWS» y «RANGE» nos permiten refinar la definición del marco de la ventana. «ROWS» define el marco en términos de un número fijo de filas precedentes o siguientes a la fila actual. «RANGE», por otro lado, define el marco basado en los valores de la columna de ordenación.

Por ejemplo, para calcular una media móvil de ventas de los últimos tres meses (incluyendo el mes actual), podríamos utilizar:

Aquí, «ROWS BETWEEN 2 PRECEDING AND CURRENT ROW» define una ventana que incluye la fila actual y las dos filas anteriores según el orden de la columna «mes».

El poder de «PARTITION BY» y «ORDER BY» juntos en Funciones de Ventana 

La combinación de «PARTITION BY» y «ORDER BY» dentro de la cláusula «OVER()» es donde realmente brilla el potencial de las funciones de ventana. «PARTITION BY» nos permite dividir los datos en grupos lógicos, mientras que «ORDER BY» establece un orden significativo dentro de cada uno de estos grupos.

Consideremos el ejemplo de calcular el ranking de productos más vendidos dentro de cada categoría:

En este caso, los productos se particionan por «categoria», y dentro de cada categoría, se ordenan por «ventas» de forma descendente. La función «RANK()» asignará un ranking a cada producto dentro de su respectiva categoría.

Usando la Cláusula «WINDOW» para simplificar consultas complejas en SQL Server

A partir de SQL Server 2022 (con un nivel de compatibilidad de base de datos 160 o superior), se introduce la cláusula «WINDOW». Esta cláusula nos permite definir especificaciones de ventana con nombre que pueden ser referenciadas por múltiples funciones de ventana dentro de una misma consulta. Esto mejora significativamente la legibilidad y el mantenimiento de consultas complejas que utilizan las mismas definiciones de ventana varias veces. La sintaxis básica de la cláusula «WINDOW» es:

Una vez definida la ventana con nombre, podemos referenciarla en la cláusula «OVER()» de nuestras funciones de ventana:

Esto resulta especialmente útil cuando tenemos varias funciones de ventana que comparten la misma lógica de partición y ordenación.

Conclusión

Las funciones de ventana representan una herramienta fundamental en el arsenal de cualquier experto en SQL Server. Nos brindan la capacidad de realizar análisis sofisticados sobre conjuntos de datos relacionados sin sacrificar la información a nivel de fila, abriendo un abanico de posibilidades para calcular totales acumulados, medias móviles, rankings dinámicos, y comparar valores entre filas.

Dominar la sintaxis de la cláusula «OVER()», comprender los diferentes tipos de funciones de ventana (agregación, ranking, valor), y saber cómo utilizar las cláusulas «PARTITION BY», «ORDER BY», «ROWS», y «RANGE» nos permitirá escribir consultas más eficientes, legibles y potentes. La introducción de la cláusula «WINDOW» en versiones recientes de SQL Server simplifica aún más la gestión de consultas complejas con múltiples definiciones de ventana.

Os animamos a explorar y practicar con estas funciones en vuestros proyectos. El potencial analítico que desbloquean las funciones de ventana en SQL Server es enorme y, sin duda, os permitirá llevar vuestras habilidades de análisis de datos al siguiente nivel.

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

Tarea de Limpieza Fantasma en SQL Server

En nuestro día a día como profesionales de SQL Server, nos enfrentamos a una multitud de procesos internos que trabajan silenciosamente para mantener nuestras bases de datos saludables y con un rendimiento óptimo. Uno de estos procesos, a menudo menos visible pero de vital importancia, es la tarea de limpieza fantasma. En este artículo, vamos a sumergirnos en las profundidades de este mecanismo, explorando su funcionamiento interno, su necesidad y las consideraciones clave para su gestión.

¿Qué son los Registros Fantasma y por qué aparecen en SQL Server?

Cuando eliminamos registros de una tabla que tiene un índice (ya sea clúster o no clúster), SQL Server no los borra físicamente de inmediato de las páginas de datos. En su lugar, el registro se marca internamente como «para ser eliminado», un estado que conocemos como registro fantasma «ghosted». Imaginemos una biblioteca donde, en lugar de retirar un libro inmediatamente de la estantería, simplemente le ponemos una etiqueta que dice «para retirar». El libro sigue allí, ocupando espacio, pero marcado para una acción posterior.

Esta técnica de «eliminación lógica» ofrece varias ventajas significativas en términos de rendimiento durante las operaciones de borrado. Primero, la operación de eliminación en sí misma se vuelve mucho más rápida, ya que solo implica cambiar un bit en la cabecera del registro en lugar de una manipulación física de los datos. Segundo, las operaciones de reversión (rollback) también se optimizan, ya que simplemente necesitamos desmarcar los registros como «para eliminar» en lugar de tener que reinsertar los datos borrados.

Además de la optimización del rendimiento en las operaciones DML, los registros fantasma son esenciales para otras funcionalidades importantes de SQL Server. Son un componente clave para el bloqueo a nivel de fila, permitiendo que diferentes transacciones operen concurrentemente en distintas filas de la misma página sin interferir entre sí. También son fundamentales para el aislamiento de instantánea, donde necesitamos mantener versiones anteriores de las filas para garantizar una vista consistente de los datos para las transacciones en curso.

El Proceso de Limpieza Fantasma

Una vez que una eliminación se ha confirmado (commit), entra la tarea de limpieza fantasma. Este es un proceso en segundo plano, monohilo, que se encarga de eliminar físicamente los registros que han sido marcados como fantasmas. Podemos pensar en este proceso como el personal de la biblioteca que, periódicamente, revisa los libros con la etiqueta «para retirar» y los saca físicamente de las estanterías, liberando espacio.

La tarea de limpieza fantasma se ejecuta automáticamente en intervalos regulares: cada 5 segundos para SQL Server 2012 y versiones posteriores, y cada 10 segundos para SQL Server 2008 y 2008 R2. En cada activación, el proceso verifica si alguna base de datos ha sido marcada como que contiene registros fantasma. Si encuentra alguna, escanea las páginas PFS de esa base de datos en busca de páginas que contengan registros fantasma. Al encontrarlas, procede a eliminar físicamente los registros marcados, con un límite de 10 páginas procesadas por cada ejecución. Esta limitación asegura que el proceso de limpieza no consuma excesivos recursos del sistema en un solo ciclo.

Es importante destacar que la tarea de limpieza fantasma opera a nivel de base de datos. Cuando una página contiene registros fantasma, la base de datos a la que pertenece se marca internamente. El proceso de limpieza solo escaneará aquellas bases de datos que estén marcadas de esta manera. Una vez que todos los registros fantasma de una base de datos han sido eliminados, la base de datos se marca como «sin registros fantasma», y el proceso la omitirá en sus siguientes ejecuciones. Además, si la tarea de limpieza no puede obtener un bloqueo compartido en una base de datos (por ejemplo, si otra operación está utilizando la base de datos de forma exclusiva), la omitirá y volverá a intentarlo en su próxima ejecución.

¿Por qué son importantes los Registros Fantasma?

Como mencionaba anteriormente, la existencia de los registros fantasma no es arbitraria; responde a necesidades fundamentales del motor de base de datos. La optimización del rendimiento de las operaciones de eliminación es una de las principales razones. Al realizar una eliminación lógica inicial, SQL Server minimiza la sobrecarga inmediata, permitiendo que las transacciones se completen más rápidamente. Esto es crucial en sistemas con una alta tasa de operaciones DML.

La necesidad de bloqueo a nivel de fila también juega un papel crucial. Los registros fantasma permiten que el motor de base de datos mantenga la coherencia y el aislamiento entre transacciones concurrentes que podrían estar interactuando con la misma página de datos.

Finalmente, en entornos donde el aislamiento de instantánea está habilitado, los registros fantasma son esenciales para mantener las versiones anteriores de las filas. Esto garantiza que las transacciones que se iniciaron antes de la operación de eliminación puedan seguir viendo una imagen consistente de los datos en el momento en que se iniciaron.

Monitorizando la actividad de Limpieza Fantasma y la presencia de Registros Fantasma

Aunque la tarea de limpieza fantasma opera en segundo plano, es posible monitorizar su actividad y la presencia de registros fantasma en nuestras bases de datos. Podemos utilizar la DMV `sys.dm_exec_requests` para identificar si el proceso de limpieza fantasma está en ejecución, buscando peticiones con un comando similar a ‘%ghost%’.

Para determinar cuántos registros fantasma existen en una base de datos específica, podemos utilizar la DMV `sys.dm_db_index_physical_stats`. La siguiente consulta nos proporciona un resumen del número total de registros fantasma por base de datos:

Esta información puede ser valiosa para identificar bases de datos con una acumulación significativa de registros fantasma, lo que podría indicar una alta actividad de eliminación o posibles problemas con el proceso de limpieza.

Deshabilitar la Limpieza Fantasma: ¿Cuándo y cuáles son las consecuencias?

En sistemas con una carga de trabajo muy alta de eliminaciones, se puede deshabilitar la tarea de limpieza fantasma utilizando el trace flag 661. La justificación detrás de esto es que, en algunos escenarios extremos, el proceso de limpieza podría no ser capaz de mantenerse al día con el ritmo de las eliminaciones, lo que podría llevar a problemas de rendimiento al mantener páginas en el buffer pool y generar E/S.

Sin embargo, es crucial entender las graves implicaciones de deshabilitar la limpieza fantasma. Al hacerlo, los registros marcados para eliminación permanecerán en las páginas indefinidamente, impidiendo que SQL Server reutilice ese espacio. Esto puede llevar a un crecimiento innecesario del tamaño de la base de datos («bloated database files») y a problemas de rendimiento. La falta de reutilización de espacio obliga a SQL Server a agregar datos a páginas nuevas, lo que puede aumentar la frecuencia de divisiones de página («page splits»). Las divisiones de página son operaciones costosas que pueden afectar negativamente el rendimiento de las consultas y la creación de planes de ejecución.

Compensar la no limpieza automática

Si decidimos deshabilitar la tarea de limpieza fantasma (una acción generalmente no recomendada y que debe probarse exhaustivamente en un entorno controlado antes de implementarse en producción), necesitaremos tomar acciones alternativas para eliminar los registros fantasma y reclamar el espacio. Algunas opciones son:

  • Reconstrucción de índices: Esta operación mueve los datos alrededor de las páginas, eliminando los registros fantasma en el proceso.
  • Ejecución manual de `sp_clean_db_free_space`: Este procedimiento almacenado limpia los registros fantasma de todas las páginas de datos de una base de datos.
  • Ejecución manual de `sp_clean_db_file_free_space`: Similar al anterior, pero permite limpiar los registros fantasma de un archivo de datos específico.

Es fundamental recordar que deshabilitar la tarea de limpieza fantasma sin implementar una estrategia de limpieza alternativa conducirá inevitablemente a problemas de rendimiento y un crecimiento descontrolado de la base de datos.

Registros Fantasma en Heaps

Es interesante notar que, durante el procesamiento normal, los registros fantasma no ocurren en las tablas HEAP. La razón principal es que estas tablas no tienen una estructura de índice que requiera el mantenimiento de eliminaciones lógicas para la coherencia de la estructura. Cuando se elimina un registro de una tabla HEAP, se elimina físicamente de la página.

Sin embargo, existe una excepción importante a esta regla: cuando el aislamiento de instantánea está habilitado, las eliminaciones de un HEAP sí generan registros fantasma como parte del proceso general de control de versiones. Esto puede tener efectos secundarios interesantes. Por ejemplo, un registro con control de versiones tiene una sobrecarga adicional de 14 bytes. Si un registro de un HEAP se convierte repentinamente en un registro con control de versiones debido a una eliminación bajo aislamiento de instantánea, su tamaño puede aumentar en 14 bytes, lo que podría hacer que ya no quepa en la página actual. Esto, en casos donde la página está llena, podría llevar a que el registro se mueva, resultando en un par de registros de reenvío/reenviado, solo por haber sido eliminado.

La Limpieza Fantasma y su impacto en el Log de transacciones

Las operaciones relacionadas con los registros fantasma también se reflejan en el registro de transacciones. Cuando se elimina un registro de una página de índice, la operación se registra con un contexto de «ghosting» (LCX_MARK_AS_GHOST). Además, la modificación de la página PFS para indicar la presencia de registros fantasma también se registra.

Cuando la tarea de limpieza fantasma se activa y elimina físicamente los registros fantasma, esta acción también se registra en el log de transacciones como una operación de LOP_EXPUNGE_ROWS. Podemos observar esta actividad en el log de transacciones utilizando la función fn_dblog().

Consideraciones de rendimiento

En sistemas con un volumen extremadamente alto de operaciones de eliminación, es posible que la tarea de limpieza fantasma no pueda mantenerse al día con el ritmo de generación de registros fantasma. Dado que es un proceso monohilo, en servidores con muchos núcleos y una alta concurrencia de eliminaciones, un solo hilo dedicado a la limpieza podría convertirse en un cuello de botella.

Cuando la tarea de limpieza fantasma se retrasa significativamente, puede generar problemas de rendimiento al mantener páginas con registros eliminados en el buffer pool, consumiendo memoria que podría ser utilizada por datos activos. También puede generar operaciones de E/S al acceder a estas páginas para realizar la limpieza. En algunos casos, esto puede llevar a un aumento en el uso de CPU y a una degradación general del rendimiento del sistema.

Alternativas en escenarios de alta carga

Ante escenarios donde la tarea de limpieza fantasma por defecto no es suficiente, existen algunas alternativas a considerar (siempre con precaución y una comprensión clara de sus implicaciones):

  • Deshabilitar temporalmente la limpieza fantasma y realizar mantenimiento de índices de forma más agresiva: Como decía antes, esto debe hacerse con extremo cuidado y requiere una estrategia clara para reclamar el espacio.
  • Utilizar DBCC FORCEGHOSTCLEANUP: Este comando no documentado fuerza la limpieza de todos los registros fantasma de una base de datos. Aunque puede ser útil en situaciones de emergencia para reclamar espacio rápidamente, su naturaleza no documentada implica que su comportamiento y posibles efectos secundarios no están garantizados y podrían cambiar en futuras versiones de SQL Server. Su uso debe ser evaluado y probado cuidadosamente en entornos no productivos. Yo personalmente no lo recomiendo.
  • Ajustar el diseño de la base de datos y los patrones de acceso: En algunos casos, repensar la forma en que se realizan las eliminaciones o la estructura de los índices podría ayudar a mitigar la acumulación de registros fantasma. Por ejemplo, en tablas con altas tasas de eliminación e inserción en las mismas áreas del índice, podría ser beneficioso considerar estrategias de particionamiento.
  • Forzar la activación de la limpieza fantasma: Se dice, aunque no es una solución oficial, que realizar un escaneo completo de un índice (por ejemplo, con un SELECT COUNT(*)) puede forzar a que las páginas con registros fantasma se añadan a la cola de la tarea de limpieza para su posterior procesamiento.

Conclusión

La tarea de limpieza fantasma es un componente esencial del motor de base de datos de SQL Server que trabaja discretamente para asegurar que el espacio ocupado por los registros eliminados se recupere y que el rendimiento general no se vea afectado negativamente. Aunque en situaciones excepcionales se puede considerar su deshabilitación, las implicaciones de hacerlo son considerables y hay que probarlo muy bien.

Entender cómo funcionan los registros fantasma y el proceso de limpieza nos permite diagnosticar mejor posibles problemas de rendimiento relacionados con una alta actividad de eliminación y tomar decisiones más informadas sobre la gestión de nuestras bases de datos. Como administradores de SQL Server, debemos ser conscientes de este proceso silencioso pero crucial, asegurando que nuestras bases de datos no se conviertan en un cementerio de «fantasmas» olvidados.

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

Crecimiento del Persistent Version Store (PVS) en SQL Server

En el día a día de la administración de bases de datos SQL Server, nos encontramos con diversas funcionalidades que buscan optimizar el rendimiento y la disponibilidad de nuestros sistemas. Una de ellas, la Recuperación Acelerada de Bases de Datos (ADR), introdujo un concepto fundamental para la gestión de transacciones y la recuperación ante fallos: el Persistent Version Store (PVS). Aunque el ADR nos brinda notables ventajas en cuanto a la rapidez de las reversiones de transacciones y la disponibilidad de la base de datos, su componente central, el PVS, puede convertirse en una fuente de preocupación si no comprendemos su funcionamiento y cómo controlar su tamaño. En este artículo, profundizaremos en qué consiste el PVS, cómo impacta en el espacio de almacenamiento de nuestras bases de datos y, lo más importante, qué estrategias podemos implementar para mantenerlo bajo control. 

¿Qué es el Persistent Version Store (PVS) de SQL Server y cómo funciona?

El Persistent Version Store (PVS) es un mecanismo introducido con la característica de Accelerated Database Recovery (ADR) en SQL Server. Su principal función es almacenar las versiones antiguas de las filas que han sido modificadas por transacciones aún activas o recientemente completadas. Esta estrategia difiere significativamente del comportamiento tradicional de SQL Server sin ADR, donde los valores antiguos se guardan en el registro de transacciones. Con ADR, al modificar una fila, SQL Server escribe una nueva versión de dicha fila en la misma tabla, manteniendo la versión anterior intacta en el PVS. Esta arquitectura permite que las reversiones de transacciones sean prácticamente instantáneas, ya que el motor no necesita recuperar y aplicar información desde el registro de transacciones.

Es crucial entender que el PVS reside dentro de la propia base de datos de usuario, específicamente en los mismos archivos de datos (.mdf). Esta ubicación contrasta con el almacén de versiones utilizado por el Read Committed Snapshot Isolation (RCSI) cuando ADR no está habilitado, el cual se crea y mantiene en la base de datos del sistema TempDB. Aunque ambos mecanismos se basan en el versionado de filas para ofrecer lecturas consistentes sin bloqueos, la persistencia del PVS dentro de la base de datos de usuario tiene implicaciones directas en su tamaño y gestión.

¿Cómo influye el PVS en el tamaño de nuestra base de datos?

La implementación del PVS tiene un impacto directo en el consumo de espacio de nuestras bases de datos. Al almacenar múltiples versiones de las filas modificadas, es inevitable que el tamaño de la base de datos en disco aumente para albergar estas versiones. De hecho, al habilitar ADR, incluso antes de realizar modificaciones, se observa un aumento inicial en el tamaño de la tabla en comparación con una tabla idéntica sin ADR. Esto se debe a que ADR necesita añadir una marca de tiempo a cada fila para rastrear sus versiones.

Además del espacio ocupado por las versiones de las filas en sí, cada fila de la tabla con ADR habilitado contiene un puntero de 14 bytes que apunta a la ubicación de su versión en el PVS, incluso si la fila no ha sido modificada recientemente. Este overhead por fila es un factor significativo que contribuye al aumento del tamaño de la base de datos. Es importante señalar que este mismo puntero existe incluso cuando solo RCSI está habilitado (sin ADR), aunque en ese caso apunte al almacén de versiones ubicado en TempDB. Por lo tanto, el crecimiento en la tabla de usuario debido a este overhead es similar tanto con ADR como con RCSI. Hablamos de esto aquí.

Los experimentos han demostrado que, tras la carga inicial de datos en tablas con ADR y/o RCSI activados, estas tienden a ser más grandes que las tablas sin estas funcionalidades. Este crecimiento se acelera considerablemente al realizar actividad de escritura, llegando incluso a duplicarse el tamaño de los objetos tras la primera actualización en bases de datos con ADR y/o RCSI habilitados. Esta tendencia se mantiene con rondas sucesivas de actualizaciones, donde las bases de datos con versionado de filas experimentan un crecimiento mucho más rápido que aquellas sin él.

El PVS y el mito de la reconstrucción de índices para ahorrar espacio

Ante este crecimiento acelerado de las tablas con ADR y RCSI, una reacción común podría ser recurrir a la reconstrucción de índices como una solución para recuperar el espacio aparentemente «perdido». Efectivamente, al reconstruir los índices en estas tablas, se observa una reducción drástica en su tamaño, igualando incluso el tamaño de tablas sin ADR. Esto podría generar la ilusión de haber «ahorrado» espacio en disco.

Sin embargo, esta ganancia de espacio es puramente ilusoria y temporal. Tan pronto como la carga de trabajo habitual se reanuda y se realizan nuevas actualizaciones, el tamaño de las tablas con ADR y RCSI vuelve a inflarse rápidamente. Nos encontramos, por lo tanto, en un ciclo continuo de crecimiento y reconstrucción sin abordar la causa fundamental del aumento de tamaño: el versionado de filas necesario para el funcionamiento de ADR y RCSI.

La reconstrucción de índices simplemente reorganiza los datos y elimina las versiones antiguas que ya no son necesarias en el momento de la reconstrucción, pero no impide la generación de nuevas versiones con futuras modificaciones. Por lo tanto, si nuestro principal objetivo al reconstruir índices en un entorno con ADR o RCSI es ganar espacio en disco, debemos comprender que este ahorro será efímero. El espacio «ahorrado» volverá a ser necesario a medida que se generen nuevas versiones de las filas. En lugar de centrarnos en la reconstrucción como una panacea para el espacio, debemos enfocarnos en dimensionar adecuadamente nuestro almacenamiento y comprender las implicaciones del versionado de filas en el crecimiento de nuestras bases de datos.

Estrategias efectivas para controlar el tamaño del PVS

Dado que la reconstrucción de índices no es una solución sostenible para controlar el tamaño del PVS, ¿qué alternativas tenemos a nuestra disposición? La clave reside en comprender la naturaleza del PVS y cómo interactúa con la actividad de nuestra base de datos.

En primer lugar, es fundamental realizar un dimensionamiento adecuado del almacenamiento. Si habilitamos ADR, debemos ser conscientes del potencial crecimiento adicional que experimentará nuestra base de datos debido al almacenamiento de las versiones de filas y al overhead por fila. Ignorar este aspecto puede llevarnos rápidamente a situaciones de falta de espacio en disco.

En segundo lugar, la monitorización activa del tamaño del PVS es esencial. SQL Server nos proporciona herramientas para observar el comportamiento del PVS y detectar patrones de crecimiento inusuales. Mediante el uso de Dynamic Management Views (DMVs), como sys.dm_tran_persistent_version_store_stats, podemos obtener información valiosa sobre el tamaño actual del PVS, el porcentaje que representa del tamaño total de la base de datos, el número de transacciones abortadas y la antigüedad de las transacciones activas.

Además de la monitorización del tamaño, es importante analizar la actividad de la base de datos. Cargas de trabajo con una alta intensidad de escritura generarán más versiones de filas y, por lo tanto, un mayor crecimiento del PVS. Asimismo, las transacciones de larga duración pueden impedir la limpieza de las versiones antiguas, contribuyendo a un aumento sostenido del tamaño del PVS. Identificar y optimizar estas transacciones puede tener un impacto significativo en la gestión del espacio del PVS.

Monitorizando el Persistent Version Store

Terminar u optimizar las transacciones de larga duración se convierte en una práctica crucial. Si una transacción permanece abierta durante un tiempo prolongado, las versiones de las filas modificadas tanto por esa transacción como por todas las transacciones siguientes no podrán ser limpiadas del PVS, lo que provocará su crecimiento descontrolado.

Como mencionaba anteriormente, las DMVs son nuestras principales aliadas para supervisar el PVS. La DMV sys.dm_tran_persistent_version_store_stats nos ofrece una visión detallada del estado del almacén de versiones persistente. Algunas de las columnas más relevantes que podemos consultar son:

  • persistent_version_store_size_kb: Indica el tamaño actual del PVS en kilobytes.
  • oldest_active_transaction_id: El ID de la transacción activa más antigua.
  • oldest_transaction_begin_time: La hora de inicio de la transacción activa más antigua.

Podemos utilizar la siguiente consulta (de la documentación oficial) para obtener una visión general del PVS en nuestras bases de datos habilitadas para ADR:

Esta consulta nos proporciona información crucial para entender el consumo de espacio del PVS y la antigüedad de las transacciones activas, lo que nos puede ayudar a identificar posibles cuellos de botella o transacciones problemáticas que estén impidiendo la limpieza del almacén de versiones.

También es útil esta otra consulta (misma fuente) para obtener las consultas que pueden ser un potencial problema. Está parametrizada por defecto para localizar las transacciones de más de 15 minutos de duración o 1 Gb de log de transacciones, lo que ya empieza a ser preocupante y puede tener un impacto en el tamaño del PVS.

Conclusión

La introducción del Persistent Version Store (PVS) con la Recuperación Acelerada de Bases de Datos (ADR) representa un avance significativo en la forma en que SQL Server gestiona las transacciones y la recuperación. Sin embargo, como hemos explorado, su funcionamiento basado en el versionado de filas tiene implicaciones directas en el tamaño de nuestras bases de datos. La idea de que la reconstrucción de índices sea una solución efectiva para controlar este tamaño ha demostrado ser una ilusión temporal.

En lugar de recurrir a prácticas de mantenimiento obsoletas, debemos adoptar un enfoque más informado y proactivo. Esto implica dimensionar adecuadamente nuestro almacenamiento, monitorizar activamente el tamaño del PVS mediante las DMVs proporcionadas por SQL Server y, fundamentalmente, comprender y optimizar la actividad de nuestras bases de datos, prestando especial atención a las transacciones de larga duración.

En definitiva, la gestión exitosa del PVS requiere que evolucionemos nuestras rutinas de mantenimiento y nos centremos en comprender el mecanismo subyacente del versionado de filas. Solo así podremos tomar decisiones informadas y evitar invertir tiempo y recursos en acciones que nos ofrecen solo una sensación temporal de mejora, asegurando un rendimiento óptimo y una gestión eficiente del espacio en nuestras bases de datos SQL Server.

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