简体   繁体   中英

SQL Server 2008 R2 Recursive Update Loop

I have a file system app that uses a SQL Server 2008 R2 to keep track of files. When I remove a file, I want to update parents in the path hierarchy to reflect their new size. I am using @fid for the current FILE_ID, @size as the FILE_SIZE, and @pid as the FILE_ID of the parent in the hierarchy.

Here is the loop I am using:

SELECT @pid=PARENT_ID FROM FILES WHERE FILE_ID=@fid;

WHILE @pid<>0
BEGIN

    UPDATE FILES
    SET 
        FILE_SIZE = 
        -- Avoid potential situation where new file size might incorrectly drop below 0
        CASE 
            WHEN FILE_SIZE-@size>=0 THEN FILE_SIZE-@size
            ELSE 0
        END
    WHERE FILE_ID=@pid;

    SET @fid=@pid;
    SELECT @pid=PARENT_ID FROM FILES WHERE FILE_ID=@fid;
END

When I run this, the sizes are not updating. If I replace the UPDATE with a SELECT, it looks like it should be working correctly. What is going on? Why are the sizes not getting updated? Is there a better way to do this?

To add some context, this snip-it is actually running inside another loop so multiple files can be deleted in a batch. Here is the code in this context:

-- Declarations
DECLARE @fid int, @size int, @pid int;
DECLARE c CURSOR FOR 
SELECT 
    FILE_ID, FILE_SIZE 
FROM
    FILES

OPEN c;

-- Initialize variables
FETCH NEXT FROM c 
INTO @fid, @size;

-- Main loop
WHILE @@FETCH_STATUS = 0
BEGIN

    -- Statements to delete the file --

    -- Loop to update sizes --
    SELECT @pid=PARENT_ID FROM FILES WHERE FILE_ID=@fid;
    WHILE @pid<>0
    BEGIN

        UPDATE FILES
        SET 
            FILE_SIZE = 
            CASE 
                WHEN FILE_SIZE-@size>=0 THEN FILE_SIZE-@size
                ELSE 0
            END
        WHERE FILE_ID=@pid;

        SET @fid=@pid;
        SELECT @pid=PARENT_ID FROM FILES WHERE FILE_ID=@fid;
    END

   FETCH NEXT FROM c 
   INTO @fid, @size;
END
CLOSE c;
DEALLOCATE c;

There is definately a better way to do this. The approach you outline is procedural and iterative and great for languages like C# and visual basic, however, for the most efficeint solution in SQL Cursors are the LAST trick you should reach for.

This should work

WITH    FileList
AS
(
    -- Select all the files that have no children
    SELECT   f.FILE_ID 
            ,f.FILE_SIZE
            ,f.PARENT_ID
            ,0 AS CHILD_ID
            ,0 AS Depth
    FROM    FILES f
            LEFT JOIN
            FILES f1 ON f.FILE_ID=f1.PARENT_ID
    WHERE   f1.PARENT_ID IS NULL
    UNION ALL
    -- Then recursively select thrir parents
    SELECT   f.FILE_ID
            ,fl.FILE_SIZE
            ,f.PARENT_ID
            ,fl.FILE_ID
            ,fl.Depth + 1
    FROM    FileList fl
            INNER JOIN
            FILES f ON f.FILE_ID=fl.PARENT_ID
)
-- With this data update the file size with the sum of all leaf nodes
UPDATE  FILES
SET     FILE_SIZE = (SELECT SUM(FILE_SIZE)
                    FROM    FileList fl
                    WHERE   fl.FILE_ID=FILES.FILE_ID
                    GROUP BY    FILE_ID)

One command, no itteration - should be orders of magnitude quicker and you are getting the accurate filesizes propagated all the way up from the leaves each time.

I wanted to post the result, in case it helps. I hope this is the right way to do it.

I needed to restrict the update to just the affected path, so I ended up using something like this, based on the suggestion from Dale M:

-- Since I already have @size and @pid, I use them here
WITH    FileList
AS
(
    -- Select the parent of the delete target
    SELECT   f.FILE_ID 
            ,f.PARENT_ID
            ,f.FILE_SIZE-@size AS FILE_SIZE
    FROM    FILES f 
    WHERE   f.FILE_ID=@pid
    UNION ALL
    -- Then recursively select its parents
    SELECT   f.FILE_ID
            ,f.PARENT_ID
            ,f.FILE_SIZE-@size AS FILE_SIZE
    FROM    FileList fl
            INNER JOIN 
            FILES f ON f.FILE_ID=fl.PARENT_ID
)
-- Update FILES with the size, already adjusted in the CTE above
UPDATE FILES
SET FILE_SIZE=fl.FILE_SIZE
FROM    FILES
        INNER JOIN
        FileList fl ON fl.FILE_ID=FILES.FILE_ID;

This is nice because I can start to break this script down into stored procedures. Thanks again to Dale M!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM