Crie hierarquia de vários níveis em que cada nó tem um número aleatório de filhos

16

Preciso criar alguns dados de teste que envolvam uma hierarquia. Eu poderia facilitar e executar alguns CROSS JOINs, mas isso me daria uma estrutura completamente uniforme / sem nenhuma variação. Isso não apenas parece chato, mas a falta de variação nos dados de teste às vezes oculta problemas que, de outra forma, seriam encontrados. Então, eu estou querendo gerar uma hierarquia não uniforme que siga estas regras:

  • 3 níveis de profundidade
    • O nível 1 é aleatoriamente de 5 a 20 nós
    • O nível 2 é de 1 a 10 nós, aleatório por cada nó do nível 1
    • O nível 3 é de 1 a 5 nós, aleatório por cada nó do nível 2
  • Todos os ramos terão 3 níveis de profundidade. A uniformidade em profundidade está boa neste momento.
  • Pode haver sobreposição nos nomes dos nós filhos em um determinado nível (ou seja, os nomes dos nós filhos não precisam ser exclusivos em todos os nós no mesmo nível).
  • O termo "aleatório" é definido aqui como sendo pseudo-aleatório, não exclusivamente aleatório. Isso precisa ser mencionado, pois o termo "aleatório" costuma ser usado para significar "ordenação aleatória de um determinado conjunto que não produz duplicatas". Eu aceito que random = random e se o número de filhos por cada nó do nível 1 for de apenas 4, 7 e 8, mesmo em 20 nós no nível 1, com uma possível expansão de 1 a 10 filhos por cada um desses nós, então tudo bem, porque é isso que é aleatório.
  • Mesmo que isso possa ser feito facilmente com WHILEloops aninhados , a preferência é encontrar uma abordagem baseada em conjunto. De um modo geral, a geração de dados de teste não possui os requisitos de eficiência que o código de produção teria, mas a busca por uma abordagem baseada em conjuntos provavelmente será mais educativa e ajudará no futuro a encontrar abordagens baseadas em conjuntos para problemas. Portanto, os WHILEloops não são descartados, mas só podem ser usados ​​se nenhuma abordagem baseada em conjuntos for possível.
  • Baseado em conjunto = idealmente uma única consulta, independentemente de CTEs, APPLYs, etc. Portanto, o uso de uma tabela de números existentes ou embutidos é bom. Usar uma abordagem WHILE / CURSOR / procedural não funcionará. Suponho que a preparação de partes dos dados em tabelas temporárias ou variáveis ​​de tabela seja adequada, desde que todas as operações sejam baseadas em conjuntos, sem loops. No entanto, dito isso, uma abordagem de consulta única provavelmente será favorecida em relação a várias consultas, a menos que seja possível mostrar que a abordagem de consulta múltipla é realmente melhor. Lembre-se também de que o que constitui "melhor" é geralmente subjetivo ;-). Lembre-se também de que o uso de "normalmente" na frase anterior também é subjetivo.
  • Qualquer versão e edição do SQL Server (2005 e mais recente, suponho) servirá.
  • Apenas T-SQL puro: nada daquelas coisas bobas do SQLCLR !! Pelo menos em termos de geração de dados. A criação dos diretórios e arquivos será feita usando SQLCLR. Mas aqui estou focando apenas na geração dos valores do que criar.
  • O TVF de múltiplas instruções T-SQL é considerado processual, não baseado em conjunto, embora, por fora, oculte a abordagem processual em um conjunto. Há momentos em que isso é absolutamente apropriado. Este não é um daqueles momentos. Na mesma linha, as funções escalares do T-SQL também não são permitidas, não apenas porque também são processuais, mas o Query Optimizer às vezes armazena em cache seu valor e o repete, de modo que a saída não seja a esperada.
  • Os TVFs embutidos T-SQL (também conhecidos como iTVFs) são okey-dokey, pois são baseados em conjuntos e efetivamente o mesmo que o uso [ CROSS | OUTER ] APPLY, que foi declarado acima como ok.
  • Execuções repetidas da (s) consulta (s) devem produzir resultados principalmente diferentes da execução anterior.
  • Atualização de esclarecimentos 1: O conjunto de resultados finais deve ser expresso como tendo uma linha para cada nó distinto do Nível3, tendo o caminho completo começando no Nível1. Isso significa que os valores de Nível1 e Nível2 se repetirão necessariamente em uma ou mais linhas, exceto nos casos em que haja apenas um nó de Nível2 contendo apenas um nó de Nível3.
  • Atualização de esclarecimento 2: Há uma preferência muito forte para cada nó que tenha um nome ou rótulo, e não apenas um número. Isso permitirá que os dados de teste resultantes sejam mais significativos e realistas.

Não tenho certeza se essas informações adicionais são importantes, mas, caso ajude a ter algum contexto, os dados do teste estão relacionados à minha resposta a esta pergunta:

Importar arquivos XML para o SQL Server 2012

Embora não seja relevante neste momento, o objetivo final de gerar essa hierarquia é criar uma estrutura de diretórios para testar os métodos recursivos do sistema de arquivos. Os níveis 1 e 2 serão diretórios e o Nível 3 será o nome do arquivo. Eu pesquisei (aqui e no Google) e encontrei apenas uma referência para gerar uma hierarquia aleatória:

Linux: criar hierarquia aleatória de diretório / arquivo

Essa pergunta (no StackOverflow) é realmente bastante próxima em termos de resultado desejado, uma vez que também busca criar uma estrutura de diretórios para teste. Mas essa pergunta (e as respostas) estão focadas no script de shell do Linux / Unix e não tanto no mundo baseado em conjuntos em que vivemos.

Agora, eu sei como gerar dados aleatórios, e já estou fazendo isso para criar o conteúdo dos arquivos, para que eles também possam mostrar variações. A parte complicada aqui é que o número de elementos dentro de cada conjunto é aleatório, não um campo específico. E , o número de elementos em cada nó precisa ser aleatório em relação a outros nós nos mesmos níveis.

Hierarquia de exemplo

     Level 1
              Level 3
|---- A
|     |-- 1
|     |   |--- I
|     |
|     |-- 2
|         |--- III
|         |--- VI
|         |--- VII
|         |--- IX
|
|---- B
|     |-- 87
|         |--- AAA
|         |--- DDD
|
|---- C
      |-- ASDF
      |   |--- 11
      |   |--- 22
      |   |--- 33
      |
      |-- QWERTY
      |   |--- beft
      |
      |-- ROYGBP
          |--- Poi
          |--- Moi
          |--- Soy
          |--- Joy
          |--- Roy

Exemplo de conjunto de resultados que descreve a hierarquia acima

Level 1    Level 2    Level 3
A          1          I
A          2          III
A          2          VI
A          2          VII
A          2          IX
B          87         AAA
B          87         DDD
C          ASDF       11
C          ASDF       22
C          ASDF       33
C          QWERTY     beft
C          ROYGBP     Poi
C          ROYGBP     Moi
C          ROYGBP     Soy
C          ROYGBP     Joy
C          ROYGBP     Roy
Solomon Rutzky
fonte

Respostas:

9

( Nota do OP: a solução preferida é o quarto / último bloco de código)

XML me parece ser a escolha óbvia da estrutura de dados a ser usada aqui.

with N as
(
  select T.N
  from (values(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),
              (12),(13),(14),(15),(16),(17),(18),(19),(20)) as T(N)
)

select top(5 + abs(checksum(newid())) % 15)
  N1.N as '@Value',
  (
  select top(1 + abs(checksum(newid())) % 10)
    N2.N as '@Value',
    (
    select top(1 + abs(checksum(newid())) % 5)
      N3.N as '@Value'
    from N as N3
    where N2.N > 0
    for xml path('Level3'), type
    )
  from N as N2
  where N1.N > 0
  for xml path('Level2'), type
  )
from N as N1
for xml path('Level1'), root('Root');

O truque para fazer o SQL Server usar valores diferentes top()para cada nó é correlacionar as subconsultas. N1.N > 0e N2.N > 0.

Nivelando o XML:

declare @X xml;

with N as
(
  select T.N
  from (values(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),
              (12),(13),(14),(15),(16),(17),(18),(19),(20)) as T(N)
)
select @X  = (
             select top(5 + abs(checksum(newid())) % 15)
               N1.N as '@Value',
               (
               select top(1 + abs(checksum(newid())) % 10)
                 N2.N as '@Value',
                 (
                 select top(1 + abs(checksum(newid())) % 5)
                   N3.N as '@Value'
                 from N as N3
                 where N2.N > 0
                 for xml path('Level3'), type
                 )
               from N as N2
               where N1.N > 0
               for xml path('Level2'), type
               )
             from N as N1
             for xml path('Level1')
             );


select L1.X.value('@Value', 'varchar(10)')+'\'+
       L2.X.value('@Value', 'varchar(10)')+'\'+
       L3.X.value('@Value', 'varchar(10)')
from @X.nodes('/Level1') as L1(X)
  cross apply L1.X.nodes('Level2') as L2(X)
  cross apply L2.X.nodes('Level3') as L3(X);

E uma versão totalmente nula de XML.

with N as
(
  select T.N
  from (values(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),
              (12),(13),(14),(15),(16),(17),(18),(19),(20)) as T(N)
)
select cast(N1.N as varchar(10))+'\'+
       cast(N2.N as varchar(10))+'\'+
       cast(N3.N as varchar(10))
from (
     select top(5 + abs(checksum(newid())) % 15)
       N.N
     from N
     ) as N1
  cross apply
     (
     select top(1 + abs(checksum(newid())) % 10)
       N.N
     from N
     where N1.N > 0
     ) as N2
  cross apply
     (
     select top(1 + abs(checksum(newid())) % 5)
       N.N
     from N
     where N2.N > 0
     ) as N3;

Correlação N1.N > 0e N2.N > 0ainda é importante.

Uma versão usando uma tabela com 20 nomes a serem usados ​​em vez de apenas números inteiros.

declare @Elements table
(
  Name nvarchar(50) not null
);

insert into @Elements(Name)
select top(20) C.name 
from sys.columns as C
group by C.name;

select N1.Name + N'\' + N2.Name + N'\' + N3.Name
from (
     select top(5 + abs(checksum(newid())) % 15)
       E.Name
     from @Elements as E
     ) as N1
  cross apply
     (
     select top(1 + abs(checksum(newid())) % 10)
       E.Name
     from @Elements as E
     where N1.Name > ''
     ) as N2
  cross apply
     (
     select top(1 + abs(checksum(newid())) % 5)
       E.Name
     from @Elements as E
     where N2.Name > ''
     ) as N3;
Mikael Eriksson
fonte
11
Eu gosto mais da nova versão. É quase a mesma coisa que inventei na minha primeira tentativa, mas por algum motivo não consegui fazer o TOP(n)trabalho corretamente nos CROSS APPLY2s. Não sei o que fiz de maneira diferente / incorreta desde que me livrei desse código depois que consegui outra coisa funcionando. Vou postar isso em breve, agora que você forneceu esta atualização. E eu limpei a maioria dos meus comentários acima.
Solomon Rutzky
Acabei de publicar minha versão. As principais diferenças são: 1) como não consegui fazer o TOP (n) funcionar, fui obtendo nelementos através de uma condição WHERE e 2) possuo o namecomponente que é mais controlado do que o diretório aleatório e / ou nomes de arquivos .
Solomon Rutzky
Desculpe estar longe por tanto tempo, mas eu estive louco ocupado. Ainda assim, estive pensando sobre isso e não consigo decidir entre minha resposta e sua versão não XML. Gosto da sua simplicidade e flexibilidade, mas preciso da capacidade de retornar nomes para criar uma estrutura de pastas que a minha possui. Então eu percebi que Vlad atualizava o dele para ter uma tabela de pesquisa e JOIN para obter a saída ideal. Portanto, se não for inapropriado perguntar, você poderia atualizar o seu para incluir a mesma pesquisa? Todas as três respostas forneceriam uma saída equivalente (ideal para comparar todas as 3) e eu aceitaria a sua. Tudo bem?
Solomon Rutzky
11
@srutzky Atualizei a resposta. Foi há um tempo atrás, então espero que eu acertei e o que você estava procurando. Obviamente, você pode adicionar uma coluna de nível @Elemetspara obter um conjunto diferente de nomes para cada nível à sua escolha.
Mikael Eriksson
11
@srutzky não se preocupe. Estou feliz que a resposta tenha sido útil para você.
Mikael Eriksson
6

Isso foi interessante.

Meu objetivo era gerar um determinado número de níveis com um número aleatório de linhas filhas por cada nível em uma estrutura hierárquica adequadamente vinculada. Quando essa estrutura estiver pronta, é fácil adicionar informações extras, como nomes de arquivos e pastas.

Então, eu queria gerar uma tabela clássica para armazenar uma árvore:

ID int NOT NULL
ParentID int NULL
Lvl int NOT NULL

Como estamos lidando com recursão, a CTE recursiva parece uma escolha natural.

Vou precisar de uma tabela de números . Os números na tabela deve começar a partir de 1. Deve haver, pelo menos, 20 números na tabela: MAX(LvlMax).

CREATE TABLE [dbo].[Numbers](
    [Number] [int] NOT NULL,
CONSTRAINT [PK_Numbers] PRIMARY KEY CLUSTERED 
(
    [Number] ASC
));

INSERT INTO Numbers(Number)
SELECT TOP(1000)
    ROW_NUMBER() OVER(ORDER BY S.object_id)  AS Number
FROM
    sys.all_objects AS S
ORDER BY Number;

Os parâmetros para geração de dados devem ser armazenados em uma tabela:

DECLARE @Intervals TABLE (Lvl int, LvlMin int, LvlMax int);
INSERT INTO @Intervals (Lvl, LvlMin, LvlMax) VALUES
(1, 5, 20),
(2, 1, 10),
(3, 1, 5);

Observe que a consulta é bastante flexível e todos os parâmetros são separados em um único local. Você pode adicionar mais níveis, se necessário, basta adicionar uma linha extra de parâmetros.

Para possibilitar essa geração dinâmica, tive que me lembrar do número aleatório de linhas para o próximo nível, então tenho uma coluna extra ChildRowCount.

Gerar exclusivo IDs também é um pouco complicado. Codifiquei o limite de 100 linhas filho por 1 linha pai para garantir que IDsisso não se repita. É disso que POWER(100, CTE.Lvl)se trata. Como resultado, existem grandes lacunas IDs. Esse número pode ser um MAX(LvlMax), mas coloquei constante 100 na consulta por simplicidade. O número de níveis não é codificado, mas é determinado por @Intervals.

Esta fórmula

CAST(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5

gera um número de ponto flutuante aleatório no intervalo [0..1), que é escalado para o intervalo necessário.

A lógica da consulta é simples. É recursivo. O primeiro passo gera um conjunto de linhas do primeiro nível. O número de linhas é determinado pelo número aleatório em TOP. Além disso, para cada linha, há um número aleatório separado de linhas filho armazenadas ChildRowCount.

A parte recursiva é usada CROSS APPLYpara gerar um determinado número de linhas filhas por cada linha pai. Eu tive que usar em WHERE Numbers.Number <= CTE.ChildRowCountvez de TOP(CTE.ChildRowCount), porque TOPnão é permitido na parte recursiva do CTE. Não sabia sobre essa limitação do SQL Server antes.

WHERE CTE.ChildRowCount IS NOT NULL interrompe a recursão.

SQL Fiddle

WITH
CTE
AS
(
    SELECT 
        TOP(CAST(
            (CAST(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) * 
            (
                1 + (SELECT I.LvlMax FROM @Intervals AS I WHERE I.Lvl = 1)
                  - (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = 1)
            )
            + (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = 1)
            AS int))
        Numbers.Number AS ID
        ,NULL AS ParentID
        ,1 AS Lvl
        ,CAST(
            (CAST(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) * 
            (
                1 + (SELECT I.LvlMax FROM @Intervals AS I WHERE I.Lvl = 2)
                  - (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = 2)
            )
            + (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = 2)
            AS int) AS ChildRowCount
    FROM Numbers
    ORDER BY Numbers.Number

    UNION ALL

    SELECT
        CA.Number + CTE.ID * POWER(100, CTE.Lvl) AS ID
        ,CTE.ID AS ParentID
        ,CTE.Lvl + 1 AS Lvl
        ,CA.ChildRowCount
    FROM
        CTE
        CROSS APPLY
        (
            SELECT
                Numbers.Number
                ,CAST(
                    (CAST(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) * 
                    (
                    1 + (SELECT I.LvlMax FROM @Intervals AS I WHERE I.Lvl = CTE.Lvl + 2)
                      - (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = CTE.Lvl + 2)
                    )
                    + (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = CTE.Lvl + 2)
                    AS int) AS ChildRowCount
            FROM Numbers
            WHERE Numbers.Number <= CTE.ChildRowCount
        ) AS CA
    WHERE
        CTE.ChildRowCount IS NOT NULL
)
SELECT *
FROM CTE
ORDER BY Lvl, ParentID, ID;

Resultado (pode haver até 20 + 20 * 10 + 200 * 5 = 1220 linhas, se você tiver sorte)

+---------+----------+-----+-------------------+
|   ID    | ParentID | Lvl | ChildRowCount     |
+---------+----------+-----+-------------------+
|       1 | NULL     |   1 | 3                 |
|       2 | NULL     |   1 | 1                 |
|       3 | NULL     |   1 | 6                 |
|       4 | NULL     |   1 | 5                 |
|       5 | NULL     |   1 | 3                 |
|       6 | NULL     |   1 | 7                 |
|       7 | NULL     |   1 | 1                 |
|       8 | NULL     |   1 | 6                 |
|     101 | 1        |   2 | 3                 |
|     102 | 1        |   2 | 5                 |
|     103 | 1        |   2 | 1                 |
|     201 | 2        |   2 | 5                 |
|     301 | 3        |   2 | 4                 |
|     302 | 3        |   2 | 5                 |
|     303 | 3        |   2 | 1                 |
|     304 | 3        |   2 | 2                 |
|     305 | 3        |   2 | 4                 |
|     306 | 3        |   2 | 3                 |
|     401 | 4        |   2 | 3                 |
|     402 | 4        |   2 | 1                 |
|     403 | 4        |   2 | 2                 |
|     404 | 4        |   2 | 2                 |
|     405 | 4        |   2 | 4                 |
|     501 | 5        |   2 | 1                 |
|     502 | 5        |   2 | 3                 |
|     503 | 5        |   2 | 5                 |
|     601 | 6        |   2 | 2                 |
|     602 | 6        |   2 | 5                 |
|     603 | 6        |   2 | 3                 |
|     604 | 6        |   2 | 3                 |
|     605 | 6        |   2 | 4                 |
|     606 | 6        |   2 | 5                 |
|     607 | 6        |   2 | 4                 |
|     701 | 7        |   2 | 2                 |
|     801 | 8        |   2 | 2                 |
|     802 | 8        |   2 | 3                 |
|     803 | 8        |   2 | 3                 |
|     804 | 8        |   2 | 3                 |
|     805 | 8        |   2 | 5                 |
|     806 | 8        |   2 | 2                 |
| 1010001 | 101      |   3 | NULL              |
| 1010002 | 101      |   3 | NULL              |
| 1010003 | 101      |   3 | NULL              |
| 1020001 | 102      |   3 | NULL              |
| 1020002 | 102      |   3 | NULL              |
| 1020003 | 102      |   3 | NULL              |
| 1020004 | 102      |   3 | NULL              |
| 1020005 | 102      |   3 | NULL              |
| 1030001 | 103      |   3 | NULL              |
| 2010001 | 201      |   3 | NULL              |
| 2010002 | 201      |   3 | NULL              |
| 2010003 | 201      |   3 | NULL              |
| 2010004 | 201      |   3 | NULL              |
| 2010005 | 201      |   3 | NULL              |
| 3010001 | 301      |   3 | NULL              |
| 3010002 | 301      |   3 | NULL              |
| 3010003 | 301      |   3 | NULL              |
| 3010004 | 301      |   3 | NULL              |
| 3020001 | 302      |   3 | NULL              |
| 3020002 | 302      |   3 | NULL              |
| 3020003 | 302      |   3 | NULL              |
| 3020004 | 302      |   3 | NULL              |
| 3020005 | 302      |   3 | NULL              |
| 3030001 | 303      |   3 | NULL              |
| 3040001 | 304      |   3 | NULL              |
| 3040002 | 304      |   3 | NULL              |
| 3050001 | 305      |   3 | NULL              |
| 3050002 | 305      |   3 | NULL              |
| 3050003 | 305      |   3 | NULL              |
| 3050004 | 305      |   3 | NULL              |
| 3060001 | 306      |   3 | NULL              |
| 3060002 | 306      |   3 | NULL              |
| 3060003 | 306      |   3 | NULL              |
| 4010001 | 401      |   3 | NULL              |
| 4010002 | 401      |   3 | NULL              |
| 4010003 | 401      |   3 | NULL              |
| 4020001 | 402      |   3 | NULL              |
| 4030001 | 403      |   3 | NULL              |
| 4030002 | 403      |   3 | NULL              |
| 4040001 | 404      |   3 | NULL              |
| 4040002 | 404      |   3 | NULL              |
| 4050001 | 405      |   3 | NULL              |
| 4050002 | 405      |   3 | NULL              |
| 4050003 | 405      |   3 | NULL              |
| 4050004 | 405      |   3 | NULL              |
| 5010001 | 501      |   3 | NULL              |
| 5020001 | 502      |   3 | NULL              |
| 5020002 | 502      |   3 | NULL              |
| 5020003 | 502      |   3 | NULL              |
| 5030001 | 503      |   3 | NULL              |
| 5030002 | 503      |   3 | NULL              |
| 5030003 | 503      |   3 | NULL              |
| 5030004 | 503      |   3 | NULL              |
| 5030005 | 503      |   3 | NULL              |
| 6010001 | 601      |   3 | NULL              |
| 6010002 | 601      |   3 | NULL              |
| 6020001 | 602      |   3 | NULL              |
| 6020002 | 602      |   3 | NULL              |
| 6020003 | 602      |   3 | NULL              |
| 6020004 | 602      |   3 | NULL              |
| 6020005 | 602      |   3 | NULL              |
| 6030001 | 603      |   3 | NULL              |
| 6030002 | 603      |   3 | NULL              |
| 6030003 | 603      |   3 | NULL              |
| 6040001 | 604      |   3 | NULL              |
| 6040002 | 604      |   3 | NULL              |
| 6040003 | 604      |   3 | NULL              |
| 6050001 | 605      |   3 | NULL              |
| 6050002 | 605      |   3 | NULL              |
| 6050003 | 605      |   3 | NULL              |
| 6050004 | 605      |   3 | NULL              |
| 6060001 | 606      |   3 | NULL              |
| 6060002 | 606      |   3 | NULL              |
| 6060003 | 606      |   3 | NULL              |
| 6060004 | 606      |   3 | NULL              |
| 6060005 | 606      |   3 | NULL              |
| 6070001 | 607      |   3 | NULL              |
| 6070002 | 607      |   3 | NULL              |
| 6070003 | 607      |   3 | NULL              |
| 6070004 | 607      |   3 | NULL              |
| 7010001 | 701      |   3 | NULL              |
| 7010002 | 701      |   3 | NULL              |
| 8010001 | 801      |   3 | NULL              |
| 8010002 | 801      |   3 | NULL              |
| 8020001 | 802      |   3 | NULL              |
| 8020002 | 802      |   3 | NULL              |
| 8020003 | 802      |   3 | NULL              |
| 8030001 | 803      |   3 | NULL              |
| 8030002 | 803      |   3 | NULL              |
| 8030003 | 803      |   3 | NULL              |
| 8040001 | 804      |   3 | NULL              |
| 8040002 | 804      |   3 | NULL              |
| 8040003 | 804      |   3 | NULL              |
| 8050001 | 805      |   3 | NULL              |
| 8050002 | 805      |   3 | NULL              |
| 8050003 | 805      |   3 | NULL              |
| 8050004 | 805      |   3 | NULL              |
| 8050005 | 805      |   3 | NULL              |
| 8060001 | 806      |   3 | NULL              |
| 8060002 | 806      |   3 | NULL              |
+---------+----------+-----+-------------------+

Gerando caminho completo em vez de hierarquia vinculada

Se estivermos interessados ​​apenas nos Nníveis do caminho completo , podemos omitir IDe ParentIDdo CTE. Se tivermos uma lista de nomes possíveis na tabela suplementar Names, é fácil selecioná-los nessa tabela no CTE. A Namestabela deve ter linhas suficientes para cada nível: 20 para o nível 1, 10 para o nível 2, 5 para o nível 3; 20 + 10 + 5 = 35 no total. Não é necessário ter conjuntos diferentes de linhas para cada nível, mas é fácil configurá-lo corretamente, então eu o fiz.

DECLARE @Names TABLE (Lvl int, Name nvarchar(4000), SeqNumber int);

-- First level: AAA, BBB, CCC, etc.
INSERT INTO @Names (Lvl, Name, SeqNumber)
SELECT 1, REPLICATE(CHAR(Number+64), 3) AS Name, Number AS SeqNumber
FROM Numbers
WHERE Number <= 20;

-- Second level: 001, 002, 003, etc.
INSERT INTO @Names (Lvl, Name, SeqNumber)
SELECT 2, REPLACE(STR(Number, 3), ' ', '0') AS Name, Number AS SeqNumber
FROM Numbers
WHERE Number <= 10;

-- Third level: I, II, III, IV, V
INSERT INTO @Names (Lvl, Name, SeqNumber) VALUES
(3, 'I',   1),
(3, 'II',  2),
(3, 'III', 3),
(3, 'IV',  4),
(3, 'V',   5);

SQL Fiddle Aqui está a consulta final. Eu divido o FullPathem FilePathe FileName.

WITH
CTE
AS
(
    SELECT 
        TOP(CAST(
            (CAST(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) * 
            (
                1 + (SELECT I.LvlMax FROM @Intervals AS I WHERE I.Lvl = 1)
                  - (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = 1)
            )
            + (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = 1)
            AS int))

        1 AS Lvl
        ,CAST(
            (CAST(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) * 
            (
                1 + (SELECT I.LvlMax FROM @Intervals AS I WHERE I.Lvl = 2)
                  - (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = 2)
            )
            + (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = 2)
            AS int) AS ChildRowCount
        ,N.Name AS FullPath
        ,N.Name AS [FilePath]
        ,CAST(N'' AS nvarchar(4000)) AS [FileName]
    FROM
        Numbers
        INNER JOIN @Names AS N ON 
            N.SeqNumber = Numbers.Number AND N.Lvl = 1
    ORDER BY Numbers.Number

    UNION ALL

    SELECT
        CTE.Lvl + 1 AS Lvl
        ,CA.ChildRowCount
        ,CTE.FullPath + '\' + CA.Name AS FullPath

        ,CASE WHEN CA.ChildRowCount IS NOT NULL 
            THEN CTE.FullPath + '\' + CA.Name
            ELSE CTE.FullPath END AS [FilePath]

        ,CASE WHEN CA.ChildRowCount IS NULL 
            THEN CA.Name
            ELSE N'' END AS [FileName]
    FROM
        CTE
        CROSS APPLY
        (
            SELECT
                Numbers.Number
                ,CAST(
                    (CAST(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) * 
                    (
                    1 + (SELECT I.LvlMax FROM @Intervals AS I WHERE I.Lvl = CTE.Lvl + 2)
                      - (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = CTE.Lvl + 2)
                    )
                    + (SELECT I.LvlMin FROM @Intervals AS I WHERE I.Lvl = CTE.Lvl + 2)
                    AS int) AS ChildRowCount
                ,N.Name
            FROM
                Numbers
                INNER JOIN @Names AS N ON 
                    N.SeqNumber = Numbers.Number AND N.Lvl = CTE.Lvl + 1
            WHERE Numbers.Number <= CTE.ChildRowCount
        ) AS CA
    WHERE
        CTE.ChildRowCount IS NOT NULL
)
SELECT
    CTE.FullPath
    ,CTE.[FilePath]
    ,CTE.[FileName]
FROM CTE
WHERE CTE.ChildRowCount IS NULL
ORDER BY FullPath;

Resultado

+-------------+----------+----------+
|  FullPath   | FilePath | FileName |
+-------------+----------+----------+
| AAA\001\I   | AAA\001  | I        |
| AAA\001\II  | AAA\001  | II       |
| AAA\002\I   | AAA\002  | I        |
| AAA\002\II  | AAA\002  | II       |
| AAA\002\III | AAA\002  | III      |
| AAA\002\IV  | AAA\002  | IV       |
| AAA\002\V   | AAA\002  | V        |
| AAA\003\I   | AAA\003  | I        |
| AAA\003\II  | AAA\003  | II       |
| AAA\003\III | AAA\003  | III      |
| AAA\004\I   | AAA\004  | I        |
| AAA\004\II  | AAA\004  | II       |
| AAA\004\III | AAA\004  | III      |
| AAA\004\IV  | AAA\004  | IV       |
| BBB\001\I   | BBB\001  | I        |
| BBB\001\II  | BBB\001  | II       |
| CCC\001\I   | CCC\001  | I        |
| CCC\001\II  | CCC\001  | II       |
| CCC\001\III | CCC\001  | III      |
| CCC\001\IV  | CCC\001  | IV       |
| CCC\001\V   | CCC\001  | V        |
| CCC\002\I   | CCC\002  | I        |
| CCC\003\I   | CCC\003  | I        |
| CCC\003\II  | CCC\003  | II       |
| CCC\004\I   | CCC\004  | I        |
| CCC\004\II  | CCC\004  | II       |
| CCC\005\I   | CCC\005  | I        |
| CCC\005\II  | CCC\005  | II       |
| CCC\005\III | CCC\005  | III      |
| CCC\006\I   | CCC\006  | I        |
| CCC\006\II  | CCC\006  | II       |
| CCC\006\III | CCC\006  | III      |
| CCC\006\IV  | CCC\006  | IV       |
| CCC\007\I   | CCC\007  | I        |
| CCC\007\II  | CCC\007  | II       |
| CCC\007\III | CCC\007  | III      |
| CCC\007\IV  | CCC\007  | IV       |
| CCC\008\I   | CCC\008  | I        |
| CCC\008\II  | CCC\008  | II       |
| CCC\008\III | CCC\008  | III      |
| CCC\009\I   | CCC\009  | I        |
| CCC\009\II  | CCC\009  | II       |
| CCC\009\III | CCC\009  | III      |
| CCC\009\IV  | CCC\009  | IV       |
| CCC\010\I   | CCC\010  | I        |
| CCC\010\II  | CCC\010  | II       |
| CCC\010\III | CCC\010  | III      |
| DDD\001\I   | DDD\001  | I        |
| DDD\001\II  | DDD\001  | II       |
| DDD\001\III | DDD\001  | III      |
| DDD\001\IV  | DDD\001  | IV       |
| DDD\002\I   | DDD\002  | I        |
| DDD\003\I   | DDD\003  | I        |
| DDD\003\II  | DDD\003  | II       |
| DDD\003\III | DDD\003  | III      |
| DDD\003\IV  | DDD\003  | IV       |
| DDD\004\I   | DDD\004  | I        |
| DDD\004\II  | DDD\004  | II       |
| DDD\004\III | DDD\004  | III      |
| DDD\005\I   | DDD\005  | I        |
| DDD\006\I   | DDD\006  | I        |
| DDD\006\II  | DDD\006  | II       |
| DDD\006\III | DDD\006  | III      |
| DDD\007\I   | DDD\007  | I        |
| DDD\007\II  | DDD\007  | II       |
| DDD\008\I   | DDD\008  | I        |
| DDD\008\II  | DDD\008  | II       |
| DDD\008\III | DDD\008  | III      |
| DDD\009\I   | DDD\009  | I        |
| DDD\009\II  | DDD\009  | II       |
| DDD\010\I   | DDD\010  | I        |
| DDD\010\II  | DDD\010  | II       |
| DDD\010\III | DDD\010  | III      |
| DDD\010\IV  | DDD\010  | IV       |
| DDD\010\V   | DDD\010  | V        |
| EEE\001\I   | EEE\001  | I        |
| EEE\001\II  | EEE\001  | II       |
| FFF\001\I   | FFF\001  | I        |
| FFF\002\I   | FFF\002  | I        |
| FFF\002\II  | FFF\002  | II       |
| FFF\003\I   | FFF\003  | I        |
| FFF\003\II  | FFF\003  | II       |
| FFF\003\III | FFF\003  | III      |
| FFF\003\IV  | FFF\003  | IV       |
| FFF\003\V   | FFF\003  | V        |
| FFF\004\I   | FFF\004  | I        |
| FFF\004\II  | FFF\004  | II       |
| FFF\004\III | FFF\004  | III      |
| FFF\004\IV  | FFF\004  | IV       |
| FFF\005\I   | FFF\005  | I        |
| FFF\006\I   | FFF\006  | I        |
| FFF\007\I   | FFF\007  | I        |
| FFF\007\II  | FFF\007  | II       |
| FFF\007\III | FFF\007  | III      |
| GGG\001\I   | GGG\001  | I        |
| GGG\001\II  | GGG\001  | II       |
| GGG\001\III | GGG\001  | III      |
| GGG\002\I   | GGG\002  | I        |
| GGG\003\I   | GGG\003  | I        |
| GGG\003\II  | GGG\003  | II       |
| GGG\003\III | GGG\003  | III      |
| GGG\004\I   | GGG\004  | I        |
| GGG\004\II  | GGG\004  | II       |
| HHH\001\I   | HHH\001  | I        |
| HHH\001\II  | HHH\001  | II       |
| HHH\001\III | HHH\001  | III      |
| HHH\002\I   | HHH\002  | I        |
| HHH\002\II  | HHH\002  | II       |
| HHH\002\III | HHH\002  | III      |
| HHH\002\IV  | HHH\002  | IV       |
| HHH\002\V   | HHH\002  | V        |
| HHH\003\I   | HHH\003  | I        |
| HHH\003\II  | HHH\003  | II       |
| HHH\003\III | HHH\003  | III      |
| HHH\003\IV  | HHH\003  | IV       |
| HHH\003\V   | HHH\003  | V        |
| HHH\004\I   | HHH\004  | I        |
| HHH\004\II  | HHH\004  | II       |
| HHH\004\III | HHH\004  | III      |
| HHH\004\IV  | HHH\004  | IV       |
| HHH\004\V   | HHH\004  | V        |
| HHH\005\I   | HHH\005  | I        |
| HHH\005\II  | HHH\005  | II       |
| HHH\005\III | HHH\005  | III      |
| HHH\005\IV  | HHH\005  | IV       |
| HHH\005\V   | HHH\005  | V        |
| HHH\006\I   | HHH\006  | I        |
| HHH\007\I   | HHH\007  | I        |
| HHH\007\II  | HHH\007  | II       |
| HHH\007\III | HHH\007  | III      |
| HHH\008\I   | HHH\008  | I        |
| HHH\008\II  | HHH\008  | II       |
| HHH\008\III | HHH\008  | III      |
| HHH\008\IV  | HHH\008  | IV       |
| HHH\008\V   | HHH\008  | V        |
+-------------+----------+----------+
Vladimir Baranov
fonte
Abordagem interessante :). Eu gosto disso. Para fins de completude, você pode adicionar a consulta para preencher a tabela Numbers (do SQL Fiddle) ou apenas incluir essa linha como parte do CTE? Então é mais fácil alguém copiar e colar. Para esta resposta, a saída final pode ser expressa como cada linha sendo um caminho completo do Nível1 até o Nível3 para todos os valores do Nível3? Eu acho que levaria apenas 2 INNER JOINs na final SELECT. Por fim, nomes / etiquetas podem ser atribuídos a cada nó para que não sejam apenas números? Vou atualizar a pergunta para esclarecer esses dois pontos.
Solomon Rutzky 10/10
De onde vêm esses nomes / etiquetas? Devo ter uma tabela 'Names', que possui 20 linhas e escolher um nome? Assim, o mesmo conjunto de nomes apareceria em cada nível. Ou cada nível deve ter seu próprio conjunto separado de nomes?
Vladimir Baranov 10/10
Eu acho que os nomes podem vir de uma tabela (temp, real ou variável) ou embutidos como parte do CTE. Originalmente, eu os coloquei no CTE, mas os movi para uma tabela temporária local para que a parte principal da consulta fosse mais legível aqui. Eu acho que com a estrutura que você tem, seria fácil o suficiente separar por nível. Mas se fosse apenas um conjunto de 20 que também seria suficiente, apenas forneceria uma variação ligeiramente menor nos dados de teste. O único requisito verdadeiro é que nenhum nome se repita dentro de um nó, pois isso seria um erro ao tentar criar os diretórios ou arquivos :).
Solomon Rutzky
11
@srutzky, adicionei uma segunda variante.
Vladimir Baranov
11
@srutzky, eu me divido FullPathem FilePathe FileName.
Vladimir Baranov
4

Então aqui está o que eu criei. Com o objetivo de criar uma estrutura de diretórios, eu estava procurando por "nomes" utilizáveis ​​para os diretórios e arquivos. Como não consegui TOP(n)trabalhar no CROSS APPLYs (acho que tentei correlacionar as consultas usando um valor do pai como nno TOP(n)mas não foi aleatório), decidi criar um tipo de "números" tabela que permitiria que uma INNER JOINou WHEREcondição produzisse um conjunto de nelementos simplesmente randomizando um número e especificando-o como WHERE table.Level = random_number. O truque é que há apenas 1 linha para o Nível1, 2 linhas para o Nível2, 3 linhas para o Nível3 e assim por diante. Portanto, usar WHERE LevelID = 3me dará três linhas e cada linha tem um valor que eu posso usar como um nome de diretório.

CONFIGURAÇÃO

Esta parte foi originalmente especificada inline, como parte do CTE. Mas, para facilitar a leitura (para que você não precise rolar muitas INSERTinstruções para chegar às poucas linhas da consulta real), eu a dividi em uma tabela temporária local.

IF (OBJECT_ID(N'tempdb..#Elements') IS NULL)
BEGIN
  PRINT 'Creating #Elements table...';
  CREATE TABLE #Elements (
     ElementLevel TINYINT NOT NULL,
     LevelName NVARCHAR(50) NOT NULL
                         );

  PRINT 'Populating #Elements table...';
  INSERT INTO #Elements (ElementLevel, LevelName)
    SELECT tmp.[Level], tmp.[Name]
    FROM (
                  SELECT 1,  N'Ella'
       UNION ALL  SELECT 2,  N'Itchy'
       UNION ALL  SELECT 2,  N'Scratchy'
       UNION ALL  SELECT 3,  N'Moe'
       UNION ALL  SELECT 3,  N'Larry'
       UNION ALL  SELECT 3,  N'Curly'
       UNION ALL  SELECT 4,  N'Ian'
       UNION ALL  SELECT 4,  N'Stephen'
       UNION ALL  SELECT 4,  N'Peter'
       UNION ALL  SELECT 4,  N'Bernard'
       UNION ALL  SELECT 5,  N'Michigan'
       UNION ALL  SELECT 5,  N'Erie'
       UNION ALL  SELECT 5,  N'Huron'
       UNION ALL  SELECT 5,  N'Ontario'
       UNION ALL  SELECT 5,  N'Superior'
       UNION ALL  SELECT 6,  N'White'
       UNION ALL  SELECT 6,  N'Orange'
       UNION ALL  SELECT 6,  N'Blonde'
       UNION ALL  SELECT 6,  N'Pink'
       UNION ALL  SELECT 6,  N'Blue'
       UNION ALL  SELECT 6,  N'Brown'
       UNION ALL  SELECT 7,  N'Asia'
       UNION ALL  SELECT 7,  N'Africa'
       UNION ALL  SELECT 7,  N'North America'
       UNION ALL  SELECT 7,  N'South America'
       UNION ALL  SELECT 7,  N'Antarctica'
       UNION ALL  SELECT 7,  N'Europe'
       UNION ALL  SELECT 7,  N'Australia'
       UNION ALL  SELECT 8,  N'AA'
       UNION ALL  SELECT 8,  N'BB'
       UNION ALL  SELECT 8,  N'CC'
       UNION ALL  SELECT 8,  N'DD'
       UNION ALL  SELECT 8,  N'EE'
       UNION ALL  SELECT 8,  N'FF'
       UNION ALL  SELECT 8,  N'GG'
       UNION ALL  SELECT 8,  N'HH'
       UNION ALL  SELECT 9,  N'I'
       UNION ALL  SELECT 9,  N'II'
       UNION ALL  SELECT 9,  N'III'
       UNION ALL  SELECT 9,  N'IV'
       UNION ALL  SELECT 9,  N'V'
       UNION ALL  SELECT 9,  N'VI'
       UNION ALL  SELECT 9,  N'VII'
       UNION ALL  SELECT 9,  N'VIII'
       UNION ALL  SELECT 9,  N'IX'
       UNION ALL  SELECT 10, N'Million'
       UNION ALL  SELECT 10, N'Billion'
       UNION ALL  SELECT 10, N'Trillion'
       UNION ALL  SELECT 10, N'Quadrillion'
       UNION ALL  SELECT 10, N'Quintillion'
       UNION ALL  SELECT 10, N'Sestillion'
       UNION ALL  SELECT 10, N'Sextillion'
       UNION ALL  SELECT 10, N'Octillion'
       UNION ALL  SELECT 10, N'Nonillion'
       UNION ALL  SELECT 10, N'Decillion'
     ) tmp([Level], [Name]);
END;

CONSULTA PRINCIPAL

Para o nível 1, retirei os [name]valores, sys.objectspois sempre há muitas linhas lá. Mas, se eu precisasse ter mais controle sobre os nomes, poderia expandir a #Elementstabela para conter níveis adicionais.

;WITH topdir(Level1, Randy) AS
(
    SELECT TOP ( (CONVERT(INT, CRYPT_GEN_RANDOM(1)) % 20) + 5 ) so.[name],
                ( (CONVERT(INT, CRYPT_GEN_RANDOM(1)) % 10) + 1 )
    FROM sys.objects so
    ORDER BY CRYPT_GEN_RANDOM(8) ASC
)
SELECT  td.Level1, tmp1.Level2, tmp2.Level3
FROM    topdir td
CROSS APPLY (SELECT help.LevelName, (CONVERT(INT, CRYPT_GEN_RANDOM(1)) % 5) + 1
            FROM #Elements help
            WHERE help.ElementLevel = td.Randy
            ) tmp1 (Level2, Bandy)
CROSS APPLY (SELECT help.LevelName
            FROM #Elements help
            WHERE help.ElementLevel = tmp1.Bandy
            ) tmp2 (Level3);

CONSULTA ADAPTADA PARA PRODUZIR O CAMINHO, O NOME E O CONTEÚDO DE ARQUIVOS

Para gerar os caminhos completos para os arquivos e o conteúdo do arquivo, fiz o SELECT principal do CTE apenas mais um CTE e adicionei um novo SELECT principal que fornecia as saídas apropriadas que simplesmente precisam ser inseridas nos arquivos.

DECLARE @Template NVARCHAR(4000);
SET @Template = N'<?xml version="1.0" encoding="ISO-8859-1"?>
<ns0:P4131 xmlns:ns0="http://switching/xi">
<R000000>
    <R00000010>R000000</R00000010>
    <R00000020>I</R00000020>
    <R00000030>{{Tag30}}</R00000030>
    <R00000040>{{Tag40}}</R00000040>
    <R00000050>{{Tag50}}</R00000050>
    <R00000060>2</R00000060>
</R000000>
</ns0:P4131>
';


;WITH topdir(Level1, Thing1) AS
(
    SELECT TOP ( (CONVERT(INT, CRYPT_GEN_RANDOM(1)) % 20) + 5 ) so.[name],
                ( (CONVERT(INT, CRYPT_GEN_RANDOM(1)) % 10) + 1 )
    FROM sys.objects so
    ORDER BY CRYPT_GEN_RANDOM(8) ASC
), main AS
(
   SELECT  td.Level1, tmp1.Level2, tmp2.Level3,
           td.Level1 + N'\' + tmp1.Level2 AS [FullPath],
           RIGHT('000' + CONVERT(VARCHAR(10),
                          (CONVERT(INT, CRYPT_GEN_RANDOM(2)) % 9999) + 1), 4) AS [R30],
           RIGHT('000' + CONVERT(VARCHAR(10),
                          (CONVERT(INT, CRYPT_GEN_RANDOM(2)) % 500) + 100), 4) AS [R50],
           ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS [RowNum]
   FROM    topdir td
   CROSS APPLY (SELECT help.LevelName, (CONVERT(INT, CRYPT_GEN_RANDOM(1)) % 5) + 1
                FROM #Elements help
                WHERE help.ElementLevel = td.Thing1
               ) tmp1 (Level2, Thing2)
   CROSS APPLY (SELECT help.LevelName
                FROM #Elements help
                WHERE help.ElementLevel = tmp1.Thing2
               ) tmp2 (Level3)
)
SELECT  mn.FullPath,
        mn.Level3 + N'.xml' AS [FileName],
        REPLACE(
            REPLACE(
                REPLACE(
                    @Template,
                    N'{{Tag30}}',
                    mn.R30),
                N'{{Tag40}}',
                mn.RowNum),
            N'{{Tag50}}',
            mn.R50) AS [Contents]
FROM    main mn;

CRÉDITO EXTRA

Embora não faça parte dos requisitos estabelecidos na pergunta, o objetivo (mencionado) era criar arquivos para testar as funções recursivas do sistema de arquivos. Então, como pegamos esse conjunto de resultados de nomes de caminhos, nomes de arquivos e conteúdo de arquivos e fazemos algo com ele? Nós apenas precisamos de duas funções SQLCLR: uma para criar as pastas e outra para criar os arquivos.

Para tornar esses dados funcionais, modifiquei o principal SELECTdo CTE mostrado diretamente acima, da seguinte maneira:

SELECT  SQL#.File_CreateDirectory(
            N'C:\Stuff\TestXmlFiles\' + mn.FullPath) AS [CreateTheDirectory],
        SQL#.File_WriteFile(
            N'C:\Stuff\TestXmlFiles\' + mn.FullPath + N'\' + mn.Level3 + N'.xml',
            REPLACE(
                REPLACE(
                    REPLACE(
                        @Template,
                        N'{{Tag30}}',
                        mn.R30),
                    N'{{Tag40}}',
                    mn.RowNum),
                N'{{Tag50}}',
                mn.R50), -- @FileData
            0, -- @AppendData
            '' -- @FileEncoding
                            ) AS [WriteTheFile]
FROM    main mn;
Solomon Rutzky
fonte