Альтернативы рекурсивному CTE внутри коррелированного подзапроса «существует»?

У меня есть ситуация, когда мне нужно иметь возможность видеть, находится ли данный человек в иерархии пользователя/менеджера.

Мне нужно иметь возможность сделать это для набора пользователей в соответствии с набором правил (не беспокойтесь об этом, просто чтобы дать немного контекста)

В идеале я хотел бы использовать рекурсивный CTE внутри коррелированного подзапроса exists в предложении where.

Но это создает массу проблем..

  1. Я не думаю, что вы вообще можете использовать CTE в подзапросе.
  2. Я использую SQL 2005 с режимом совместимости 80, поэтому я не могу использовать перекрестное применение... поэтому для меня нет параметров столбца в UDF :-(

Я думаю, что я пытаюсь сделать следующее:

WITH UserHierarchy(UserId, ManagerId)
    AS
    (
        --Anchor Definition
        SELECT [UserId], [ManagerId] FROM [Users] WHERE [ManagerId] = [Rules].[RuleAddedByUserId] -- this needs to bind to an outer query....
        UNION ALL
        --Recursive Member definiation
        SELECT [Users].[UserId], [Users].[ManagerId] FROM [Users] 
        INNER JOIN [UserHierarchy] ON [Users].[ManagerId] = [UserHierarchy].[UserId]
        WHERE [Users].[UserId] <> [Users].[ManagerId] --don't recurse if the anchor definition matches itself (to avoid an infinate loop).
    )

Есть ли способ сделать определение привязки динамическим в режиме совместимости 80? Или альтернативный подход?


person Alex KeySmith    schedule 01.12.2011    source источник
comment
И чем вам одной анкорной части в предикате EXISTS недостаточно? Я не вижу смысла рекурсии дальше от привязки, если рекурсивная часть не коррелирует с внешним запросом, а только часть привязки. Что мне не хватает?   -  person Andriy M    schedule 01.12.2011
comment
Привет, @AndriyM, спасибо за внимание. Проблема в том, что оператор WITH вообще не может быть внутри подзапроса, поэтому я не могу привязаться к внешнему запросу (мой код выше - это просто псевдокод - извините, я должен упомянуть тот). Хорошая новость в том, что я думаю, что нашел решение, я опубликую его, как только закончу.   -  person Alex KeySmith    schedule 01.12.2011
comment
Да, кажется, я понял это с первого раза. Вы хотите что-то вроде рекурсивного запроса внутри EXISTS, но WITH, который используется для построения рекурсивных запросов, не может использоваться в подзапросах, и поэтому вы задаете этот вопрос. Это правильно? Предположим, вам разрешено иметь WITH в подзапросах, и поэтому вы можете построить коррелированный рекурсивный подзапрос. Зачем вам нужна рекурсия, если только якорная часть коррелирует с внешним запросом? Если бы якорь возвращал строки, этого было бы достаточно, чтобы предикат EXISTS вернул true, и аналогично для false.   -  person Andriy M    schedule 01.12.2011
comment
Итак, суть в том, что вам, скорее всего, не нужна рекурсия и, следовательно, вам не нужно использовать WITH. Это, в свою очередь, означает, что вы можете иметь коррелированный подзапрос в форме простого SELECT [UserId], [ManagerId] FROM [Users] WHERE [ManagerId] = [Rules].[RuleAddedByUserId]. (Или SELECT * FROM [Users] WHERE [ManagerId] = [Rules].[RuleAddedByUserId], так как фактический список столбцов не имеет значения для предиката EXISTS.)   -  person Andriy M    schedule 01.12.2011
comment
Спасибо за отзыв @AndriyM, теперь я понимаю, что вы имеете в виду. В моем примере я пытался воспроизвести проблему, с которой я столкнулся, с более крупным запросом. Глядя на это сейчас, я не думаю, что мой пример очень хорош. Чего я пытаюсь добиться, так это выяснить, находится ли пользователь ниже менеджера в данной иерархии... Я попытаюсь обновить свой вопрос более точным примером. Я должен признать, что мне сложно объяснить, что я ищу, поскольку проблема довольно эпическая. Еще раз спасибо за помощь, постараюсь обновить в ближайшее время.   -  person Alex KeySmith    schedule 01.12.2011


Ответы (3)


Один из способов сделать это — создать рекурсивное CTE, в котором для каждого пользователя есть по одной строке для каждого предка этого пользователя в дереве. Затем вы можете использовать CTE для фильтрации предков. Например, с этим деревом:

Bob
|-Alice
  |-Jim

CTE вернет что-то вроде:

User  Ancestor Level
----  -------- -----
Bob   NULL     1
Alice Bob      1
Jim   Alice    1
Jim   Bob      2

Столбец Level оказался не очень важным, но я обнаружил, что это помогло, когда я писал запрос.

Вот пример скрипта, который идентифицирует всех пользователей, которые находятся под Алисой в иерархии:

CREATE TABLE Users(
    UserId int NOT NULL PRIMARY KEY,
    Name nvarchar(25),
    ManagerId int
);
GO

INSERT INTO Users (UserId, Name, ManagerId)
SELECT 1, 'Bob', NULL UNION ALL
SELECT 2, 'Steve', 1 UNION ALL
SELECT 3, 'Chris', 2 UNION ALL
SELECT 4, 'Alice', 1 UNION ALL
SELECT 5, 'Roger', 4 UNION ALL
SELECT 6, 'Tony', 5;
GO

WITH all_ancestors AS (
    SELECT
        u.UserId,
        u.Name,
        u.ManagerId AS AncestorId,
        1 AS level
    FROM
        Users AS u
    UNION ALL
    SELECT
        alla.UserId,
        alla.Name,
        u.ManagerId AS AncestorId,
        alla.level + 1
    FROM
            all_ancestors AS alla
        INNER JOIN
            Users AS u
        ON
            alla.AncestorId = u.UserId
)
SELECT
    u.*
FROM
        Users AS u
    INNER JOIN
        all_ancestors AS a
    ON
        u.UserId = a.UserId
WHERE
    a.AncestorId = 4; -- Alice
GO

DROP TABLE Users;
GO
person Cheran Shunmugavel    schedule 04.12.2011
comment
Спасибо за помощь, Черан, интересно, что я выбрал именно такой подход. +1 от меня, я опубликую свое решение через несколько минут. - person Alex KeySmith; 05.12.2011
comment
Я искал что-то похожее, но мои мысли в итоге приняли другой оборот, и то, что я тогда нашел, меня не удовлетворило. Ваше решение действительно хорошее. - person Andriy M; 05.12.2011

Сколько уровней иерархии может быть в таблице Users? Я ожидаю, что он будет достаточно низким. Интересно, достаточно ли это низко, чтобы попробовать несколько вложенных тестов EXISTS, например:

… /* your main query here */
WHERE …
  AND EXISTS (
    SELECT *
    FROM [Users] u1
    WHERE [UserID] = @UserID
      AND (
        [ManagerId] = [Rules].[RuleAddedByUserId]
        OR EXISTS (
          SELECT *
          FROM [Users] u2
          WHERE [UserID] = u1.[ManagerID]
            AND (
              [ManagerId] = [Rules].[RuleAddedByUserId]
              OR EXISTS (
                SELECT *
                FROM [Users] u3
                WHERE [UserID] = u2.[ManagerID]
                  AND (
                    [ManagerId] = [Rules].[RuleAddedByUserId]
                    OR EXISTS ( … /* and so on, until you've covered
                                     all possible levels */
                    )
                  )
              )
            )
        )
      )
  )
person Andriy M    schedule 03.12.2011
comment
Спасибо за помощь, к сожалению, я не могу гарантировать уровень вложенности, и я должен заставить его работать в любом сценарии вложенности, но спасибо за пост. - person Alex KeySmith; 05.12.2011

Я добрался туда в конце концов! Спасибо, ребята, за помощь.

Вот фрагмент SQL, над которым я работаю.

Мне просто нужно было изменить свое мышление, вместо того, чтобы видеть, существует ли пользователь под менеджером в предложении where. Мне нужно было рассматривать CTE как предварительный фильтр и создавать все детали, которые мне нужны, заранее, а затем выполнять обычную фильтрацию после, т.е. существующих операторов (примечание: я не включил их для краткости).

RulesUserHierarchy(UserId, ManagerId, PushRuleId, OnlyForSubOrdinates) -- Gets only subordinates for rules created by managers. And all users for those created by admin.
AS
(
    --Anchor Definition
    SELECT
         [Users].[UserId]
        ,[Users].[ManagerId]
        ,[RulesAnchor].[PushRuleId]
        ,[RulesAnchor].[OnlyForSubOrdinates]
    FROM [Users]
        CROSS JOIN [Rules] [RulesAnchor] --assume every user is doing every rule at this point (because the recursive statement has to be the first statement), we'll filter later.
    WHERE (([OnlyForSubOrdinates]) = 0 OR ([OnlyForSubOrdinates] = 1 AND [UserId] = [RulesAnchor].[AddedByUserId]))
    UNION ALL

    --Recursive Member definiation
    SELECT
         [Users].[UserId]
        ,[Users].[ManagerId]
        ,[RulesUserHierarchy].[PushRuleId]
        ,[RulesUserHierarchy].[OnlyForSubOrdinates]
    FROM [Users]
        INNER JOIN [RulesUserHierarchy]
            ON [Users].[ManagerId] = [RulesUserHierarchy].[UserId] --recursive hook 
            AND [RulesUserHierarchy].[OnlyForSubOrdinates] = 1 -- no point recursing if it's for everyone, as the anchor will pull back everything for us.
    WHERE [Users].[UserId] <> [Users].[ManagerId] --don't recurse if the anchor definition matches itself (to avoid an infinate loop).
)       
-- simple statement to test recursion above, will be filtering the inclusions here (e.g. the other mega exists statements)
SELECT [UserId], [ManagerId], [PushRuleId], [OnlyForSubOrdinates] FROM [RulesUserHierarchy]

ИЗМЕНИТЬ

Я понял, что мне не нужно перекрестное соединение в рекурсивном определении. Хотя это не повлияет на результат последних операторов exists, это не очень хорошо для производительности.

person Alex KeySmith    schedule 05.12.2011