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:
1 2 3 |
SELECT * FROM AdventureWorks2012.Sales.SalesOrderDetail WHERE ModifiedDate BETWEEN '2008-07-15 00:00:00.000' AND '2008-07-31 00:00:00.000' |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
DECLARE @rowguidVar UNIQUEIDENTIFIER -- prepare unique ID variable to use in the WHERE statement below DECLARE test_cursor CURSOR FOR SELECT rowguid FROM AdventureWorks2012.Sales.SalesOrderDetail WHERE ModifiedDate BETWEEN '2008-07-15 00:00:00.000' AND '2008-07-31 00:00:00.000' --This is the same query as above except we SELECT only the ID for each row OPEN test_cursor FETCH NEXT FROM test_cursor INTO @rowguidVar --This is the start of the cursor loop. WHILE @@FETCH_STATUS = 0 BEGIN SELECT * FROM Sales.SalesOrderDetail WHERE rowguid = @rowguidVar -- Here we select on row and then move onto the next row ID and loop FETCH NEXT FROM test_cursor INTO @rowguidVar END CLOSE test_cursor DEALLOCATE test_cursor -- Don't forget these statements which flush the cursor from memory |
Resultados:
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
DECLARE @GUIDS TABLE (rowguid UNIQUEIDENTIFIER PRIMARY KEY) --Here we create an indexed table variable to store all the GUIDs INSERT INTO @GUIDS SELECT rowguid FROM AdventureWorks2012.Sales.SalesOrderDetail WHERE ModifiedDate BETWEEN '2008-07-15 00:00:00.000' AND '2008-07-31 00:00:00.000' --Here we insert all our GUIDs into the variable/temporary table WHILE EXISTS (SELECT rowguid FROM @GUIDS) --This is a basic WHILE loop that runs as long as there is data in the variable table BEGIN SELECT * FROM AdventureWorks2012.Sales.SalesOrderDetail WHERE rowguid IN (SELECT TOP 100 * FROM @GUIDS) --We SELECT the top 100 rows that are in our variable table DELETE TOP (100) FROM @GUIDS --This deletes the 100 rows that we have just selected\ END --If there is still data in the variable table we return to the BEGIN point and process the next 100 |
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
- Problemas de desempeño de cursores en SQL Server - October 29, 2016
- Tutorial de cursor de SQL Server - December 18, 2015
- Características de SQL Server Business Intelligence – Creando un Simple Cubo OLAP - December 18, 2015