locked
Reemplazar cursores con ...???

    Question

  • Hola foro! Un favor. Les explico mi contexto: durante los últimos años me he dedicado a desarrollar sistemas en .NET, principalmente con C# y VB.NET. Tengo un paradigma completamente orientado a objetos. En este momento estoy revisando los procesos de un cliente que tienen toda su lógica de negocio dentro de stored procedures. Muchos de ellos son procesos por lotes, pero procesan los datos registro por registro con cursores. Quiero corregir esto, y antes de preguntar "¿cómo puedo cambiar este cursor por una consulta?" o algo parecido (aunque seguramente lo haré :P), quiero entender bien la lógica de esto para no tener que estar preguntando cada vez que me encuentre con algo así y además saber lo que estoy haciendo. Alguien sabe de alguna referencia, lectura o algo que me pueda servir? Se los agradeceré muchísimo.

    laloivol
    Wednesday, July 08, 2009 3:26 AM

Answers

  • Hola.

    No voy a transformar todo el batch, pero sí puedo darte algunas indicaciones. Seguro que apartir de ahí tú mismo consigues realizarlo. A ver, tienes como dos casos, el borrado y el update. Separa los registros que hay que eliminar de los que hay que actualizar. Para ello, a la select inicial, añádele los filtros en el where que luego controlas más abajo (para el borrado, sería que @montImp + @montInt = 0, que a su vez provienen ambos de cuentas previas). La tabla a actualizar o borrar la debes cruzar con vwConta por los campos NumCte = Fecha, con un join. Sería más o menos así:

    delete C 
    from Ctemont C inner join vsConta V on C.NumCte = V.numCte and C.Fecha = V.Fecha
    where ---> parte en la que filtras los registros originales para dejar sólo aquellos que tienes que borrar

    Con esto transformas tu cursor en dos sentencias, un borrado y un update. Inténtalo y nos cuentas. Otra cosa que ayuda es realizar consultas que muestren el resultado final de la tabla o los registros a borrar. Así tienes el conjunto de registros antes de lanzar las modificaciones en sí.


    Alberto López Grande.
    Thursday, July 09, 2009 11:03 AM
    Moderator

All replies

  • Hola.

    La lógica es sencilla de decir y no tan sencilla de implementar: Hay que pensar en conjuntos y no en registros individuales. Eso significa pasar de:

    inser ... values 

    A: 

    un insert ... select

    O pasar de:

    update tabla set campo = 'nuevovalor' where id = 7500 

    A:

    update t set campo = 'nuevovalor'
    from Tabla t inner join...

    Es decir, agrupar registros semejantes y actualizar, insertar, etc en bloques. También te tocará rehacer código que ya tengas hecho, por ejemplo, un procedimiento almacenado que hace una acción concreta, pero para un registro, tendrás que transformarlo para que lo haga por conjuntos.

    Sustituir un cursor no siempre es fácil, pero prácticamente siempre es posible. Y para resolver cada caso, habría que verlo, conforme los vayas arreglando, irás cogiendo más práctiva hasta que los sepas resolver casi todos. Te animo a que nos pases un ejemplo y así te podemos decir cómo puedes ir haciéndolo.


    Alberto López Grande.
    Wednesday, July 08, 2009 11:13 AM
    Moderator
  • Gracias nuevamente Alberto, siempre ayudandome :D

    En realidad lo que estaba buscando era algún tutorial o algo parecido, pero en fin, te hare caso con lo de incluir aqui mi código, espero que no te confundas tanto como yo... primero se declara el cursor, varias líneas después se obtiene la variable @suma de dos valores en la misma vista que hace referencia el cursor... que por cierto, es una vista que contiene datos de Ctemont. Tomé este ejemplo porque es uno de los usos más pequeños de cursores en los procesos que estoy revisando, pero resulta que está anidado dentro de otro cursor, en el cursor externo a este se obtienen @MyMvtoImp, @MyMvtoInt y @myMvtoIntImp (ah! y si... se que hay que cambiarle los nombres a las variables también)

    DECLARE CUR CURSOR local fast_forward
        FOR SELECT
                vwNumCte
               ,vwFecha
               ,vwMontImp
               ,vwMontInt
            FROM
                vwConta
            ORDER BY
                atpAtrFecha ASC
    
    -- MÁS CÓDIGO
                
    SELECT
        @suma = SUM(vwMontImp + vwMontInt)
    FROM
        vwConta
        
    -- MÁS CÓDIGO: @MyMvtoImp, @MyMvtoInt y @myMvtoIntImp se asignan mediante otro cursor.
                
    IF @suma > 0
        AND @MyMvtoImp > 0 
        BEGIN  
            OPEN CUR  
            WHILE 1 = 1  
                BEGIN 
                    FETCH next FROM CUR INTO @numCte, @fecha, @montImp, @montInt  
    	                 
                    IF @@FETCH_STATUS <> 0 
                        BREAK  
                    IF @MyMvtoImp >= @montImp + @montInt 
                        BEGIN  
                            SELECT
                                @intTot = @intTot + @montImp
                               ,@impTot = @impTot + @montInt
                               ,@MyMvtoImp = @MyMvtoImp - ( @montImp + @montInt )
                               ,@montImp = 0
                               ,@montInt = 0  
                        END  
                    ELSE 
                        BEGIN  
                            SELECT
                                @myMvtoInt = ROUND(@MyMvtoImp / 1.05 , 4, 0)
                               ,@myMvtoIntImp = @MyMvtoImp - @myMvtoInt
                               ,@intTot = @intTot + @myMvtoInt
                               ,@impTot = @impTot + @myMvtoIntImp
                               ,@montImp = @montImp - @myMvtoInt
                               ,@montInt = @montInt - @myMvtoIntImp
                               ,@MyMvtoImp = 0  
                        END  
    
                    IF @montImp + @montInt = 0 
                        BEGIN  
                            DELETE FROM
                                Ctemont
                            WHERE
                                NumCte = @numCte
                                AND Fecha = @fecha  
    	                     
                            SELECT
                                @liError = @@ERROR  
                            IF ( ISNULL(CAST(@liError AS BIT), 0) ) = 1 
                                GOTO SALIR  
                        END  
                    ELSE 
                        BEGIN  
                            UPDATE
                                Ctemont
                            SET 
                                MontImp = @montImp
                               ,MontInt = @montInt
                            FROM
                                Ctemont
                            WHERE
                                NumCte = @numCte
                                AND Fecha = @fecha  
    	                     
                            SELECT
                                @liError = @@ERROR  
                            IF ( ISNULL(CAST(@liError AS BIT), 0) ) = 1 
                                GOTO SALIR  
                        END  
                    IF @MyMvtoImp = 0 
                        BREAK
                END
            CLOSE CUR  
        END   
    Gracias nuevamente!

    laloivol
    Thursday, July 09, 2009 3:00 AM
  • Hola.

    No voy a transformar todo el batch, pero sí puedo darte algunas indicaciones. Seguro que apartir de ahí tú mismo consigues realizarlo. A ver, tienes como dos casos, el borrado y el update. Separa los registros que hay que eliminar de los que hay que actualizar. Para ello, a la select inicial, añádele los filtros en el where que luego controlas más abajo (para el borrado, sería que @montImp + @montInt = 0, que a su vez provienen ambos de cuentas previas). La tabla a actualizar o borrar la debes cruzar con vwConta por los campos NumCte = Fecha, con un join. Sería más o menos así:

    delete C 
    from Ctemont C inner join vsConta V on C.NumCte = V.numCte and C.Fecha = V.Fecha
    where ---> parte en la que filtras los registros originales para dejar sólo aquellos que tienes que borrar

    Con esto transformas tu cursor en dos sentencias, un borrado y un update. Inténtalo y nos cuentas. Otra cosa que ayuda es realizar consultas que muestren el resultado final de la tabla o los registros a borrar. Así tienes el conjunto de registros antes de lanzar las modificaciones en sí.


    Alberto López Grande.
    Thursday, July 09, 2009 11:03 AM
    Moderator
  • El problema es determinar si el proceso debe ser iterativo o puede hacerse de conjunto.

    Si la unica solucion posible es iterativa, por ejemplo en el caso de de procesos que dependen del orden de registros y de dependencias entre registros consecutivos, mi experiencia es que los procedimientos almacenados escritos en CLR son la mejor opcion.
    En el otro caso hay que usar la imagiiancion para aplicar sentencias de conjunto.

    Si la cantidad de registros del cursor es baja, apriori no te esfuerzes en resolverlo de conjunto ya que la mejora es infima o  dependiendo de las situaciones de diseño de tablas hasta puede ser peor. Esto deberas evaluarlo en cada caso. Es lo mismo que ocurre con los ordenes de complejidad de los algoritmos de ordenamiento, a veces el de burbuja es mejor que el quicksort.

    Saludos

    Ing. Jose Mariano Alvarez http://blog.josemarianoalvarez.com/ Microsoft MVP SQLTotal Consulting Mi.Correo.es.j0se.marian0.alvarez@gmail.c0m.Corregirl0 Este mensaje se proporciona tal como es, SIN GARANTIAS de ninguna clase
    Thursday, July 09, 2009 3:37 PM