Создайте иерархию нескольких уровней, где каждый узел имеет случайное количество дочерних элементов.

16

Мне нужно создать некоторые тестовые данные, которые включают в себя иерархию. Я мог бы сделать это легко и сделать пару CROSS JOINшагов, но это дало бы мне структуру, которая была бы абсолютно однородной / без каких-либо изменений. Это не только кажется скучным, но отсутствие изменений в тестовых данных иногда маскирует проблемы, которые в противном случае были бы обнаружены. Итак, я хочу создать неоднородную иерархию, которая следует этим правилам:

  • 3 уровня в глубину
    • Уровень 1 случайным образом 5 - 20 узлов
    • Уровень 2 - это 1-10 узлов, случайных для каждого узла уровня 1
    • Уровень 3 - это 1 - 5 узлов, случайных для каждого узла уровня 2
  • Все ветви будут 3 уровня глубиной. Равномерность по глубине в порядке на этом этапе.
  • Могут перекрываться имена дочерних узлов на любом данном уровне (т.е. имена дочерних узлов не обязательно должны быть уникальными для всех узлов на одном уровне).
  • Термин «случайный» определяется здесь как псевдослучайный, а не однозначно случайный. Это необходимо упомянуть, поскольку термин «случайный» часто используется для обозначения «случайного упорядочения данного набора, который не создает дубликатов». Я принимаю, что random = random, и если число дочерних элементов на каждый узел уровня 1 составляет всего 4, 7 и 8, даже в 20 узлах на уровне 1, у которых потенциальный разброс составляет 1-10 дочерних элементов на каждый из этих узлов, тогда это нормально, потому что это то, что случайное.
  • Даже если это можно сделать с помощью вложенных WHILEциклов, предпочтительнее найти подход, основанный на множестве. Вообще говоря, генерация тестовых данных не имеет требований к эффективности, которые предъявлялись бы к производственному коду, но использование подхода на основе множеств, вероятно, будет более познавательным и поможет в будущем найти подходы к задачам на основе множеств. Таким образом, WHILEциклы не исключаются, но могут использоваться, только если невозможен подход на основе множеств.
  • На основе набора = в идеале один запрос, независимо от CTE, APPLY и т. Д. Таким образом, использование существующей или встроенной таблицы чисел подойдет. Использование WHILE / CURSOR / процедурного подхода не будет работать. Я полагаю, что размещение частей данных во временных таблицах или табличных переменных нормально, только если все операции основаны на множествах, без циклов. Однако, как говорится, подход с одним запросом, вероятно, будет предпочтительнее, чем несколько запросов, если только не будет показано, что подход с несколькими запросами действительно лучше. Пожалуйста, также имейте в виду, что то, что составляет «лучше», обычно субъективно ;-). Также имейте в виду, что использование слова «как правило» в предыдущем предложении также субъективно.
  • Подойдет любая версия и выпуск SQL Server (я полагаю, 2005 и новее).
  • Только чистый T-SQL: ничего такого глупого в SQLCLR !! По крайней мере, с точки зрения генерации данных. Создание каталогов и файлов будет выполнено с использованием SQLCLR. Но здесь я просто сосредоточен на создании ценностей того, что нужно создавать.
  • T-SQL TVF с несколькими утверждениями считаются процедурными, а не основанными на множествах, хотя снаружи они маскируют процедурный подход в наборе. Есть моменты, когда это абсолютно уместно. Это не один из тех времен. В том же духе скалярные функции T-SQL также не разрешены не только потому, что они также являются процедурными, но оптимизатор запросов иногда кэширует их значение и повторяет его так, что вывод не соответствует ожидаемому.
  • Встроенные TVF-файлы T-SQL (также известные как iTVF) являются окей-dokey, поскольку они основаны на множествах и фактически аналогичны использованию [ CROSS | OUTER ] APPLY, которое было указано выше как нормальное.
  • Повторные выполнения запроса (-ов) должны давать в основном результаты, отличные от предыдущего запуска.
  • Пояснение Обновление 1: Окончательный набор результатов должен быть выражен как наличие одной строки для каждого отдельного узла уровня 3, с полным путем, начиная с уровня 1. Это означает, что значения Level1 и Level2 будут обязательно повторяться в одной или нескольких строках, за исключением случаев, когда существует только один узел Level2, содержащий только один узел Level3.
  • Пояснение Обновление 2: Существует очень сильное предпочтение для каждого узла, имеющего имя или метку, а не просто номер. Это позволит сделать полученные результаты испытаний более значимыми и реалистичными.

Я не уверен, имеет ли значение эта дополнительная информация, но на случай, если она поможет иметь некоторый контекст, данные теста относятся к моему ответу на этот вопрос:

Импорт файлов XML в SQL Server 2012

Хотя это и не актуально в данный момент, конечной целью создания этой иерархии является создание структуры каталогов для тестирования рекурсивных методов файловой системы. Уровни 1 и 2 будут каталогами, а уровень 3 в конечном итоге будет именем файла. Я искал вокруг (и здесь, и через Google) и нашел только одну ссылку на генерацию случайной иерархии:

Linux: создать случайную директорию / файловую иерархию

Этот вопрос (на StackOverflow) на самом деле довольно близок с точки зрения желаемого результата, поскольку он также стремится создать структуру каталогов для тестирования. Но этот вопрос (и ответы) сосредоточены на сценариях оболочки Linux / Unix, а не на мире, основанном на множествах, в котором мы живем.

Теперь я знаю, как генерировать случайные данные, и уже делаю это для создания содержимого файлов, чтобы они также могли отображать изменения. Сложность в том, что количество элементов в каждом наборе является случайным, а не конкретным полем. И количество элементов в каждом узле должно быть случайным по сравнению с другими узлами на тех же уровнях.

Пример иерархии

     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

Пример результирующего набора, описывающего вышеприведенную иерархию

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
Соломон Руцкий
источник

Ответы:

9

( Примечание ОП: предпочтительным решением является четвертый / последний кодовый блок)

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 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');

Хитрость в том, чтобы заставить SQL Server использовать разные значения для top()каждого узла, состоит в том, чтобы сделать подзапросы взаимосвязанными. N1.N > 0и N2.N > 0.

Сглаживание 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);

И версия полностью лишена 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;

Корреляция N1.N > 0и N2.N > 0до сих пор важна.

Версия, использующая таблицу с 20 именами вместо целых чисел.

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;
Микаэль Эрикссон
источник
1
Мне больше нравится новая версия. Это почти то же самое, что я придумал в своей первой попытке, но по какой-то причине я не смог заставить TOP(n)работать корректно в течение 2 CROSS APPLYс. Не уверен, что я сделал по-другому / неправильно, так как я избавился от этого кода, как только я получил что-то еще работающее. Я опубликую это в ближайшее время, теперь, когда вы предоставили это обновление. И я убрал большинство моих комментариев выше.
Соломон Руцкий,
Я только что опубликовал свою версию. Основные отличия: 1) так как я не мог заставить TOP (n) работать, я пошел с получением nэлементов через условие WHERE, и 2) у меня есть nameкомпонент, который более управляем, чем рандомизация имен каталогов и / или файлов ,
Соломон Руцкий
Извините, что так долго отсутствовал, но я был безумно занят. Тем не менее, я думал об этом и не могу выбрать между моим ответом и вашей версией без XML. Мне нравится ваша простота и гибкость, но мне нужна возможность возвращать имена, чтобы использовать их для создания структуры папок, которая есть у меня. Затем я понял, что у меня есть Влад, который обновил его, чтобы иметь таблицу поиска и присоединиться к ней, чтобы получить идеальный результат. Итак, если неуместно спрашивать, не могли бы вы обновить свой, чтобы включить тот же поиск? Тогда все 3 ответа дали бы эквивалентный результат (идеально для сравнения всех 3), и я бы принял ваш. Это нормально?
Соломон Руцкий
1
@srutzky Я обновил ответ. Было некоторое время назад, так что надеюсь, что я правильно понял и что вы искали. Конечно, вы можете добавить столбец уровня, @Elemetsчтобы получить различный набор имен для каждого уровня на выбор.
Микаэль Эрикссон
1
@srutzky не беспокойся. Я рад, что ответ был полезен для вас.
Микаэль Эрикссон
6

Это было интересно.

Моя цель состояла в том, чтобы создать заданное количество уровней со случайным количеством дочерних строк на каждом уровне в правильно связанной иерархической структуре. Когда эта структура готова, в нее легко добавить дополнительную информацию, например, имена файлов и папок.

Итак, я хотел создать классическую таблицу для хранения дерева:

ID int NOT NULL
ParentID int NULL
Lvl int NOT NULL

Поскольку мы имеем дело с рекурсией, рекурсивный CTE кажется естественным выбором.

Мне понадобится таблица чисел . Числа в таблице должны начинаться с 1. Там должно быть по крайней мере , 20 числа в таблице 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;

Параметры для генерации данных должны храниться в таблице:

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);

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

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

Генерирование уникальности IDs также несколько сложно. Я жестко прописал ограничение в 100 дочерних строк на 1 родительскую строку, чтобы гарантировать, что IDsони не повторятся. Вот о чем это POWER(100, CTE.Lvl). В результате есть большие пробелы в IDs. Это число может быть MAX(LvlMax), но я для простоты поставил константу 100 в запросе. Количество уровней не является жестко заданным, но определяется @Intervals.

Эта формула

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

генерирует случайное число с плавающей запятой в диапазоне [0..1), которое затем масштабируется до необходимого интервала.

Логика запроса проста. Это рекурсивно. Первый шаг генерирует набор строк первого уровня. Количество строк определяется случайным числом в TOP. Также для каждой строки есть отдельное случайное количество дочерних строк, хранящихся в ChildRowCount.

Рекурсивная часть используется CROSS APPLYдля генерации заданного количества дочерних строк на каждую родительскую строку. Мне пришлось использовать WHERE Numbers.Number <= CTE.ChildRowCountвместо TOP(CTE.ChildRowCount), потому что TOPне допускается в рекурсивной части CTE. Не знал об этом ограничении SQL Server раньше.

WHERE CTE.ChildRowCount IS NOT NULL останавливает рекурсию

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;

Результат (если вам повезет, может быть до 20 + 20 * 10 + 200 * 5 = 1220 строк)

+---------+----------+-----+-------------------+
|   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              |
+---------+----------+-----+-------------------+

Генерация полного пути вместо связанной иерархии

Если нас интересуют только Nглубокие уровни пути , мы можем опустить IDи ParentIDCTE. Если у нас есть список возможных имен в дополнительной таблице Names, их легко выбрать из этой таблицы в CTE. В Namesтаблице должно быть достаточно строк для каждого уровня: 20 для уровня 1, 10 для уровня 2, 5 для уровня 3; 20 + 10 + 5 = 35 всего. Нет необходимости иметь разные наборы строк для каждого уровня, но это легко настроить правильно, поэтому я сделал это.

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 Вот последний запрос. Я разделил FullPathна FilePathи 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;

Результат

+-------------+----------+----------+
|  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        |
+-------------+----------+----------+
Владимир Баранов
источник
Интересный подход :). Мне это нравится. Ради полноты, не могли бы вы добавить запрос, чтобы заполнить таблицу Numbers (из скрипты SQL), или просто включить эту строку как часть CTE? Тогда кому-то проще просто скопировать и вставить. Для этого ответа, может ли конечный результат быть выражен как каждая строка, представляющая собой полный путь от уровня 1 до уровня 3 для всех значений уровня 3? Я думаю, что это займет всего 2 INNER JOINс в финале SELECT. Наконец, можно ли присваивать имена / метки каждому узлу, чтобы они не были просто числами? Я уточню вопрос, чтобы уточнить оба эти момента.
Соломон Руцки
Откуда эти названия / ярлыки? Должен ли я иметь таблицу «Имена», которая имеет 20 строк и выбрать имя из нее? Таким образом, один и тот же набор имен будет появляться на каждом уровне. Или каждый уровень должен иметь свой отдельный набор имен?
Владимир Баранов
Я думаю, что имена могут происходить из таблицы (временная, реальная или переменная) или из строки как часть CTE. Сначала я поместил их в CTE, но затем переместил их в локальную временную таблицу, чтобы основная часть запроса была здесь более читабельной. Я думаю, что с той структурой, которая у вас есть, было бы достаточно легко разделить каждый уровень. Но если бы этого хватило только на один набор из 20, он бы обеспечил чуть меньшую вариацию в тестовых данных. Единственное истинное требование заключается в том, чтобы ни одно имя не повторялось внутри узла, поскольку это могло бы привести к ошибке при попытке создать каталоги или файлы :).
Соломон Руцкий
1
@srutzky, я добавил второй вариант.
Владимир Баранов
1
@srutzky, я разделил FullPathна FilePathи FileName.
Владимир Баранов
4

Итак, вот что я придумала. С целью создания структуры каталогов я искал полезные «имена» для каталогов и файлов. Поскольку я не смог получить TOP(n)работу в CROSS APPLYs (я думаю, что я пытался сопоставить запросы, используя значение из родительского элемента в качестве nin, TOP(n)но тогда это не было случайным), я решил создать тип «числа» таблица, которая позволяет условию INNER JOINили WHEREпроизводить набор nэлементов просто путем рандомизации числа и указания его как WHERE table.Level = random_number. Хитрость в том, что есть только 1 строка для уровня 1, 2 строки для уровня 2, 3 строки для уровня 3 и так далее. Следовательно, использование WHERE LevelID = 3даст мне 3 строки, и каждая строка имеет значение, которое я могу использовать в качестве имени каталога.

НАСТРОИТЬ

Эта часть была первоначально указана как встроенная, как часть CTE. Но для удобства чтения (чтобы вам не нужно было пролистывать множество INSERTоператоров, чтобы добраться до нескольких строк реального запроса), я разбил его на локальную временную таблицу.

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;

ОСНОВНОЙ ЗАПРОС

Для уровня 1 я просто выбрал [name]значения, sys.objectsпотому что там всегда много строк. Но если бы мне нужно было больше контролировать имена, я мог бы просто расширить #Elementsтаблицу, чтобы она содержала дополнительные уровни.

;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);

ЗАПРОС, ПРИНЯТЫЙ ДЛЯ ПРОИЗВОДСТВА КАЖДОГО ФАЙЛА, НАИМЕНОВАНИЯ И СОДЕРЖАНИЯ

Чтобы сгенерировать полные пути к файлам и их содержимому, я сделал основной SELECT CTE просто еще одним CTE и добавил новый основной SELECT, который дал надлежащие выходные данные, которые просто должны были войти в файлы.

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;

ДОПОЛНИТЕЛЬНЫЙ КРЕДИТ

Хотя это и не является частью требований, изложенных в вопросе, цель (которая была упомянута) состояла в создании файлов для тестирования рекурсивных функций файловой системы. Итак, как нам взять этот результирующий набор имен путей, имен файлов и содержимого файлов и что-то с этим сделать? Нам просто нужны две функции SQLCLR: одна для создания папок и одна для создания файлов.

Чтобы сделать эти данные функциональными, я изменил основную SELECTчасть CTE, показанную непосредственно выше, следующим образом:

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;
Соломон Руцкий
источник