Используйте рекурсивное общее табличное выражение SQL Server, чтобы получить полный путь ко всем файлам в папке (с подпапками)

Существует недокументированная расширенная хранимая процедура SQL Server под названием xp_dirtree, которая может возвращать имена всех файлов и папок (включая подпапки) в формате таблицы. Чтобы попрактиковаться в понимании рекурсивного CTE, я решил использовать его для получения полного пути ко всем файлам в указанной папке (включая подпапки). Однако после часа головной боли я все еще не могу понять, как правильно это сделать. Следующий код - это то, что у меня есть в настоящее время. Можно ли реализовать эту цель с помощью рекурсивного CTE?

DECLARE @dir NVARCHAR(260) ;
SELECT  @dir = N'c:\temp' ;

IF RIGHT(@dir, 1) <> '\' 
    SELECT  @dir = @dir + '\' ;

IF OBJECT_ID('tempdb..#dirtree', 'U') IS NOT NULL 
    DROP TABLE #dirtree ;
CREATE TABLE #dirtree
(
 id INT PRIMARY KEY
        IDENTITY,
 subdirectory NVARCHAR(260),
 depth INT,
 is_file BIT
) ;

INSERT  INTO #dirtree
        EXEC xp_dirtree 
            @dir,
            0,
            1 ;

SELECT  *
FROM    #dirtree ;

WITH    files
          AS (
              SELECT    id,
                        subdirectory,
                        depth,
                        is_file, subdirectory AS path
              FROM      #dirtree
              WHERE     is_file = 1
                        AND depth <> 1
   UNION ALL
               -- ...
             )
    SELECT  *
    FROM    files ;

Предположим, что вывод xp_dirtree:

/*
id  subdirectory   depth   is_file
--- -------------- ------- -------
1   abc.mdf        1       1
2   a              1       0
3   a.txt          2       1
4   b.txt          2       1
5   a.rb           1       1
6   aaa.flv        1       1
*/

Я хочу:

/*
path
------------------
c:\temp\abc.mdf
c:\temp\a\a.txt
c:\temp\a\b.txt
c:\temp\a.rb
c:\temp\aaa.flv
*/

person Just a learner    schedule 24.04.2012    source источник


Ответы (3)


Если я правильно понимаю, вы хотите что-то вроде этого:

Тестовые данные:

CREATE TABLE #dirtree
(
    id INT,
    subdirectory NVARCHAR(260),
    depth INT ,
    is_file BIT,
    parentId INT
)

INSERT INTO #dirtree(id,subdirectory,depth,is_file)
VALUES
    (1,'abc.mdf',1,1),(2,'a',1,0),(3,'a.txt',2,1),
    (4,'b.txt',2,1),(5,'a.rb',1,1),(6,'aaa.flv',1,1)

Обновлен родительский идентификатор

UPDATE #dirtree
SET ParentId = (SELECT MAX(Id) FROM #dirtree
      WHERE Depth = T1.Depth - 1 AND Id < T1.Id)
FROM #dirtree T1

Запрос

;WITH CTE
AS
(
    SELECT
        t.id,
        t.subdirectory,
        t.depth,
        t.is_file
    FROM
        #dirtree AS t
    WHERE
        is_file=0
    UNION ALL
    SELECT
        t.id,
        CAST(CTE.subdirectory+'\'+t.subdirectory AS NVARCHAR(260)),
        t.depth,
        t.is_file
    FROM
        #dirtree AS t
        JOIN CTE
            ON CTE.id=t.parentId
    )
SELECT
    'c:\temp\'+CTE.subdirectory AS [path]
FROM
    CTE
WHERE
    CTE.is_file=1
UNION ALL
SELECT
    'c:\temp\'+t.subdirectory
FROM
    #dirtree AS t
WHERE
    is_file=1
    AND NOT EXISTS
    (
        SELECT
            NULL
        FROM
            CTE
        WHERE
            CTE.id=t.id
    )

Результат

path
---------------
c:\temp\a\a.txt
c:\temp\a\b.txt
c:\temp\abc.mdf
c:\temp\a.rb
c:\temp\aaa.flv

ИЗМЕНИТЬ

Изменены таблицы, используемые в примере, чтобы они больше походили на те, что указаны в вашем вопросе.

person Arion    schedule 24.04.2012
comment
Привет, Арион, я не могу полностью понять твой код. Когда я тестировал его, я получил неправильный ответ. - person Just a learner; 24.04.2012
comment
Процедура xp_dirtree не возвращает идентификаторы, и поэтому depth не является ссылкой, это просто глубина (уровень вложенности). Так что вы не можете присоединиться к id = depth. - person Andriy M; 25.04.2012
comment
Кажется, теперь он дает дополнительные строки. Я думаю, вы могли бы избавиться от UNION в основном запросе и заменить условие привязки CTE на parentId IS NULL или depth = 1. - person Andriy M; 25.04.2012
comment
Оно делает? Я думал, что рекурсивная часть выберет те, у которых есть путь, а второй UNION даст файлы, у которых нет пути - person Arion; 25.04.2012
comment
Извините, моя вина, я думаю, что я что-то пропустил или перепутал, играя с вашим запросом. Фактический результат - это именно то, что вы показали. - person Andriy M; 25.04.2012

Почти девять лет спустя, и, к сожалению, я не знаю готового решения. Так что я все еще изучаю xp_dirtree и мне нужно решение этой проблемы.

Я попробовал ответ Ариона и обнаружил, что он дает результаты. Однако с большой файловой системой, содержащей более 11 тысяч объектов, он работал очень медленно. Я видел, что это было очень медленно даже с самого начала:

UPDATE #dirtree
SET ParentId = (SELECT MAX(Id) FROM #dirtree
      WHERE Depth = T1.Depth - 1 AND Id < T1.Id)
FROM #dirtree T1

Хотя это не проблема островков и пробелов, у нее есть некоторые сходства, и способ мышления в этих проблемах помог мне здесь. Код в конце — это моя хранимая процедура. В разделах есть некоторые комментарии относительно того, что делает код.

Вы бы использовали это так:

exec directory 
    @root = 'c:\somepath', 
    @depth = 3, 
    @outputTable = '##results';

select * from ##results;

Что приводит к выводу, например:

+---------------------------------+------------+------------+-----------+--------+-----------+----------+
| path                            | name       | nameNoExt  | extension | isFile | runtimeId | parentId |
+---------------------------------+------------+------------+-----------+--------+-----------+----------+
| c:\somePath\DataMovers          | DataMovers | DataMovers | NULL      | 0      | 4854      | NULL     |
| c:\somePath\DataMovers\main.ps1 | main.ps1   | main       | ps1       | 1      | 4859      | 4854     |
+---------------------------------+------------+------------+-----------+--------+-----------+----------+

Мне пришлось построить его таким образом, потому что внутри он берет вывод xp_dirtree и загружает его во временную таблицу. Это предотвращает возможность брать результаты процесса и загружать их в таблицу вне процесса из-за запрета на вложенные операторы insert-exec. Не раскрывайте @outputTable ненадежным пользователям, потому что он подвержен SQL-инъекции. Конечно, переработайте процесс, чтобы избежать этого, однако он соответствует вашим потребностям.

/*

    Summary:        Lists file directory contents.

    Remarks:        - stackoverflow.com/q/10298910
                    - This assumes that the tree is put in order where 
                      subfolders are listed right under their parent
                      folders.  If this changes in the future, a 
                      different logic will need to be implemented.

                
    Example:        exec directory 'c:\somepath', 3, '##results';
                    select * from ##results;

*/
create procedure directory
    @root nvarchar(255),
    @depth int,
    @outputTable sysname
as

-- initializations

    if @outputTable is null or not (left(@outputTable,2) = '##') or charindex(' ', @outputTable) > 0
        throw 50000, '@outputTable must be a global temp table with no spaces in the name.', 1;

    if exists (select 0 from tempdb.information_schema.tables where table_name = @outputTable) 
    begin
        declare @msg nvarchar(255) = '''tempdb.dbo.' + @outputTable + ''' already exists.'; 
        throw 50000, @msg, 1;
    end

-- fetch the tree (it doesn't have full path names)

    drop table if exists #dir;

    create table #dir (
        id int identity(1,1),
        parentId int null,
        path nvarchar(4000),
        depth int,
        isFile bit,
        isLeader int default(0),
        groupId int
    )

    insert      #dir (path, depth, isFile)
    exec        xp_dirtree @root, @depth, 1;

-- identify the group leaders (based on a change in depth)

    update  d
    set     isLeader = _isLeader
    from    (
                select  id, 
                        isLeader, 
                        _isLeader = iif(depth - lag(depth) over(order by id) = 0, 0, 1)
                from    #dir
            ) d;

-- find the parents for each leader (subsetting just for leaders improves efficiency)

    update      #dir
    set         parentId = (
                    select  max(sub.id) 
                    from    #dir sub
                    where   sub.depth = d.depth - 1 
                    and     sub.id < d.id
                    and     d.isLeader = 1
                )
    from        #dir d
    where       d.isLeader = 1;

-- assign an identifier to each group (groups being objects that are 'siblings' of the leader)

    update      d
    set         groupId = _groupId 
    from        (
                    select      *, _groupId = sum(isLeader) over(order by id)
                    from        #dir 
                ) d;

-- set the parent id for each item based on the leader's parent id

    update      d
    set         d.parentId = leads.parentId 
    from        #dir d
    join        #dir leads 
                    on d.groupId = leads.groupId 
                    and leads.parentId is not null;

-- convert the path names to full path names and calculate path parts

    drop table if exists #pathBuilderResults;

    with pathBuilder as (

        select      id, parentId, origId = id, path, pseudoDepth = depth 
        from        #dir 

        union all
        select      par.id, 
                    par.parentId,
                    pb.origId,
                    path = par.path + '\' + pb.path,
                    pseudoDepth = pb.pseudoDepth - 1
        from        pathBuilder pb
        join        #dir par on pb.parentId = par.id
        where       pb.pseudoDepth >= 2 

    )

    select      path = @root + '\' + pb.path,
                name = d.path,
                nameNoExt = iif(ext.value is null, d.path, left(d.path, len(d.path) - len(ext.value) - 1)),
                extension = ext.value,
                d.isFile,
                runtimeId = pb.origId,
                parentId = d.parentId
    into        #pathBuilderResults
    from        pathBuilder pb
    join        #dir d on pb.origId = d.id
    cross apply (select value = charindex('.', d.path)) dotPos
    cross apply (select value = right(d.path, len(d.path) - dotPos.value)) pseudoExt 
    cross apply (select value = iif(d.isFile = 1 and dotPos.value > 0, pseudoExt.value, null)) ext
    where       pb.pseudoDepth = 1
    order by    pb.origId;

-- terminate

    declare @sql nvarchar(max) = 'select * into ' + @outputTable + ' from #pathBuilderResults';
    exec (@sql);
person pwilcox    schedule 04.02.2021

Создайте и используйте sp_dirtree @Path = 'c:\', @FileOnly = 1

 create or alter proc sp_dirtree
       @Path      nvarchar(4000) 
     , @Depth     int = 0
     , @FileOnly  bit = 0
    as -- Dir tree with fullpath. sergkog 2018-11-14
      set nocount on
      declare @Sep nchar(1) = iif(patindex('%/%',@Path) > 0,'/','\') -- windows or posix
      set @Path += iif(right(@Path,1) <> @Sep, @Sep,'')

    declare @dirtree table(
        Id int identity(1,1) 
      , subdirectory nvarchar(4000) not null
      , depth    int  not null
      , is_file  bit  not null
      , parentId int  null
    )

    insert @dirtree(subdirectory, depth, is_file)
    exec xp_dirtree @Path, @Depth, 1

    update @dirtree
       set ParentId = (select max(id) from @dirtree where Depth = t1.Depth - 1 and Id < t1.Id)
      from @dirtree t1

    ;with cte as(
      select t.*
      from @dirtree t
      where is_file=0
     union all
      select t.id
           , convert(nvarchar(4000), cte.subdirectory+ @Sep + t.subdirectory)
           , t.depth
           , t.is_file
           , t.parentId
      from
        @dirtree t join cte on cte.id = t.parentId
     )
     select @Path + cte.subdirectory as FullPath
          , cte.is_file as IsFile
     from cte
     where cte.is_file = iif(@FileOnly = 1, 1,cte.is_file)
     union all
     select @Path + t.subdirectory
          , t.is_file
     from @dirtree t    
     where
        t.is_file = iif(@FileOnly = 1, 1,t.is_file)
        and not exists(select null from cte 
                       where cte.id=t.id
        )
    order by FullPath, IsFile
    go
person sergkog    schedule 14.11.2018
comment
Привет, пожалуйста, добавьте описание, а не только код. - person live2; 14.11.2018