Evan Barke

Problemas de desempeño de cursores en SQL Server

October 29, 2016 by

Introducción

En un artículo previo discutimos cómo configurar un cursor básico. Explicamos que un cursor es una operación basada en filas que toma una sentencia SELECT dada y descompone el proceso de los datos en ejecuciones en bucle. La gente que no tiene experiencia en Administración de Base de datos o que no disfruta las clases de base de datos cuando está estudiando, a menudo encontrará los cursores de SQL Server útiles y fáciles de escribir. Esto es porque ellos están libres de los grilletes de la lógica basada en conjuntos que es lo normal cuando se escriben la mayor parte de scripts T-SQL. Es por esta razón que encontramos frecuentemente cursores SQL Server escritos en la lógica del negocio de una aplicación y es una pena porque son problemas reales de desempeño. Sí, hay momento en que está bien usar cursores ya que ellos pueden ser ajustados ligeramente jugando con sus diferentes tipos, pero, como regla general, uno debería intentar evadirlos a toda costa.

Problemas de desempeño

Los cursores de SQL Server son notoriamente malos para el desempeño. En cualquier buen ambiente de desarrollo, las personas hablan acerca de los cursores como si fueran demonios a ser evadidos a toda costa. La razón para esto es plana y simple; los cursores son la mejor manera de ralentizar una aplicación. Esto es porque SQL Server, como cualquier sistema de administración de bases de datos relacionales (RDBMS), está optimizado para operaciones basadas en conjuntos. Tome esta simple sentencia SELECT como un ejemplo:

Cuando usted escribe una sentencia SELECT como esta (que retorna 1051 filas) para recuperar un puñado de datos desde la base de datos, el sistema recibe la sentencia y crea o usa un plan de consultas existente, luego usa índices para localizar los datos en el disco, recupera los datos de una sola vez y retorna los datos como un conjunto. Si sus índices son correctamente colocados, la consulta puede ser acelerada. En el caso anterior, si el cambio ModifiedDate fue incluido en un índice, correría más rápido.

Cuando se está corriendo esta consulta y se encienten las estadísticas de tiempo (SET STATISTICS TIME ON) uno puede ver que el proceso entero coma menos de un segundo:

SQL Server Execution Times:
    CPU time = 15 ms, elapsed time = 87 ms.

Ahora digamos que usted quería (por alguna razón), reemplazar su sentencia WHERE con una variable que puede ser usada para llamar a una sola fila cada vez, pero para 1051 veces usted puede usar un cursor:

Resultados: Results gained by using a SQL Server cursor

Este cursor recuperará exactamente el mismo conjunto de datos, pero lo hace fila por fila y toma muchísimo tiempo para hacerlo, de hecho 48 segundos, opuesto a los 87 milisegundos, ¡eso es 55172% más lento! Esto es porque la lógica basada en conjuntos para la cual los sistemas RDBMS como SQL Server están optimizados, es completamente rota y el proceso entero de consultas tiene que repetido para cada fila.

Así que, ¿por qué existen?

Los cursores de SQL Server y cualquier otro tipo de cursores se remontan a antes de que los lenguajes de programación procedimentales pudieran manejar conjuntos de datos y requerían dividirlos en filas (por ejemplo, COBOL, FORTRAN, C de estilo antiguo, etc.). Así que a ese respecto, son simplemente anticuados. De todas maneras, aparte de compatibilidad hacia atrás, aún pueden servirnos bien en las situaciones correctas. Un momento así sería cuando usted escribe un script para restaurar un puñado de bases de datos desde archivos de copias de seguridad en un disco. En este caso, usted puede escribir un cursor para recolectar los nombres de bases de datos y correr un comando RESTORE DATABASE por cada base de datos, una por una. Otro momento en que esto puede ser útil es cuando usted necesita actualizar una columna entera de una tabla grande que es contantemente consultada en un ambiente de producción. Hacer esto fila por fila evitaría bloqueos y esperas para otros usuarios y su consulta UPDATE mientras las operaciones concurrentes están sucediendo en las mismas páginas de datos. De todas maneras, incluso en este caso es preferible escribir un bucle WHILE para actualizar los datos (por ejemplo, de 1000 en 1000 filas). Esto también evitaría muchos bloqueos y haría el trabajo más rápido.

Para ilustrar cómo un bucle while funciona, he masajeado la anterior consulta de ejemplo para retornar los mismos datos de nuevo, pero esta vez, en lugar de ir fila por fila o el conjunto completo, hace algo entre medio y retorna 100 filas al mismo tiempo.

Incluso este torpe bucle WHILE es vertiginosamente rápido en comparación al enfoque de cursor SQL Server. Toma menos de un segundo, pero está más cerca de 800ms que 87ms como en el caso de la consulta basada en conjuntos.

Conclusión

Las personas tienen razón al rechazar los cursores. Si se vuelve normal para usted usar cursores en su T-SQL mientras construye la lógica del negocio de una aplicación, usted está dirigiéndose a un camino con desempeño desastroso. Imagine, por ejemplo, que escribió un procedimiento almacenado que retorna resultados basados en un cursor y luego usted escribe otro procedimiento almacenado usando un cursor que llama al primero. Esto se llama cursores anidados, y es la manera perfecta de estancar un equipo de servidor caro/de alto desempeño con código torpe y no optimizado.

Así que, evite los cursores más de lo que evitaría a suegra y sólo úselos cuando haya dominado T-SQL basado en conjuntos y sepa que un enfoque de fila por fila es necesario sólo para una operación de mantenimiento/parchado de scripts.

Recursos útiles


Evan Barke
168 Views