SQL/DAX 中的多父层次结构传播

multi-parent hierarchy propagation in SQL/DAX

假设我有一个 table 描述每个员工的主要和次要报告渠道。让我们想象一个组织结构,其中 CEO,员工 0,有 2 位经理(12)向他汇报。

经理 2 的团队中有 2 名员工(34),但是员工 4 实际上在经理 1 的团队中工作时区,所以虽然他有 2 作为他的主要报告,但他也作为次要报告向经理 1 报告,以便 1 可以履行正常的信托管理义务(提供支持等) .

除了担任员工 4 的二级管理角色外,经理 2 还有一名团队成员向他汇报 (5)。

编辑: 为了说明多亲问题,让我们给团队成员 4 一个实习生,工作人员 6团队成员 6 现在是经理 12 的下属 - 后者通过二级报告线继承。

组织结构如下所示:

+--+-------+---------+
|ID|Primary|Secondary|
|0 |NULL   |NULL     |
|1 |0      |NULL     |
|2 |0      |NULL     |
|3 |1      |NULL     |
|4 |1      |2        |
|5 |2      |NULL     |
|6 |4      |NULL     |
+--+-------+---------+

现在我想将其扩展为一个 SQL 视图,为我提供 下方 任何给定员工的人员​​列表,涵盖主要和次要报告。因此,对于工作人员 2(有主要和次要报告的经理),我希望看到团队成员 45,对于 CEO (0)我希望能见到 CEO 以外的任何员工。我们的新实习生6是CEO、经理12的下属,也是他的直属经理4

这看起来像这样:

+--+-----------+
|ID|Subordinate|
|0 |1          |
|0 |2          |
|0 |3          |
|0 |4          |
|0 |5          |
|0 |6          |
|1 |3          |
|1 |4          |
|1 |6          |
|2 |4          |
|2 |5          |
|2 |6          |
|4 |6          |
+--+-----------+

我如何在 SQL 中实现这一点?我正在考虑对 ID 进行某种 OUTER APPLY 操作,但我正在努力解决(我认为)解决此问题所需的重入问题。我的背景是过程编程,我认为这是我在这里苦苦挣扎的部分原因。

注意:我想在这里预料到一个明显的问题是"Surely this is an XY problem - why on earth would you want to do this?"

我想在 PowerBI 中使用 row-level security 让每个员工都能访问组织结构中低于他们的个人的特定信息。不幸的是,RLS 不允许对每个人执行存储过程,所以我坚持进行这种组合扩展,然后根据登录简单地过滤上面的 table。

话虽如此,我愿意接受更好的方法来解决这个问题。

您需要展平报告层次结构和次要报告层次结构,将它们加载到表格模型中的单独表格中。

有关如何在 DAX 中完全执行此操作,请参阅 DAX Patterns: Parent-Child Hierarchies。或者,您可以使用 SQL 服务器查询,使用递归通用 Table 表达式来展平两个层次结构。

在任何一种情况下,它们都会成为模型中的两个单独的表和两个单独的关系,然后您可以在 RLS 过滤器中引用它们。

使用 DAX 中的父子层次结构函数可以很容易地解决这个问题。我认为您不需要构建任何额外的表,只需在 RLS 规则中加入以下条件即可:

对于员工N,您只需要检查是否

PATHCONTAINS(PATH('Hierarchy'[ID], 'Hierarchy'[Primary]), N)

PATHCONTAINS(PATH('Hierarchy'[ID], 'Hierarchy'[Secondary]), N)

请注意,这允许员工 N 看到他们自己和他们的下属,但如果您不想这样,您可以添加一个额外的条件。


编辑: 当你的结构不是树时,问题就变得更难了。这是一种应该有效的方法。

对每个ID,查找下属得到Level1,搜索Level1下一级下属,依此类推,直到没有下属。 (如果你的结构中有一个循环 returns 你会达到更高的水平,那么你将陷入递归。)

在本例中,顶部以下有三层,因此我们需要三个步骤。

| ID | Primary | Secondary | Level1 | Level2 | Level3 |
|----|---------|-----------|--------|--------|--------|
| 0  |         |           | 1      | 4      | 6      |
| 0  |         |           | 2      | 4      | 6      |
| 0  |         |           | 2      | 5      |        |
| 0  |         |           | 3      |        |        |
| 1  | 0       |           | 4      | 6      |        |
| 2  | 0       |           | 4      | 6      |        |
| 2  | 0       |           | 5      |        |        |
| 3  | 0       |           |        |        |        |
| 4  | 1       | 2         | 6      |        |        |
| 5  | 2       |           |        |        |        |
| 6  | 4       |           |        |        |        |

这是在 Power Query 编辑器中执行此操作的 M 代码:

let
    Source = Table.FromRows({{0,null,null},{1,0,null},{2,0,null},{3,0,null},{4,1,2},{5,2,null},{6,4,null}},{"ID", "Primary", "Secondary"}),
    #"Changed Type" = Table.TransformColumnTypes(Source,{{"ID", Int64.Type}, {"Primary", Int64.Type}, {"Secondary", Int64.Type}}),
    SearchNextLevel = ExpandNext(ExpandNext(ExpandNext(#"Changed Type", "Level1", "ID"), "Level2", "Level1"), "Level3", "Level2"),
    #"Appended Query" =
        Table.Combine(
            {Table.RenameColumns(Table.SelectColumns(SearchNextLevel, {"ID", "Level1"}), {"Level1","Subordinate"}),
             Table.RenameColumns(Table.SelectColumns(SearchNextLevel, {"ID", "Level2"}), {"Level2","Subordinate"}),
             Table.RenameColumns(Table.SelectColumns(SearchNextLevel, {"ID", "Level3"}), {"Level3","Subordinate"})}
        ),
    #"Filtered Rows" = Table.SelectRows(#"Appended Query", each ([Subordinate] <> null)),
    #"Removed Duplicates" = Table.Distinct(#"Filtered Rows"),
    #"Sorted Rows" = Table.Sort(#"Removed Duplicates",{{"ID", Order.Ascending}, {"Subordinate", Order.Ascending}})
in
    #"Sorted Rows"

这里是多次使用的自定义函数,可以更上一层楼:

let
    ExpandToNextLevel = (T as table, NextLevel as text, ThisLevel as text) as table =>
    let
        SearchNextLevel =
        Table.AddColumn(T,
            NextLevel,
            (C) =>
                Table.SelectRows(
                    T, each Record.Field(C, ThisLevel) <> null and
                       ([Primary] = Record.Field(C, ThisLevel) or
                        [Secondary] = Record.Field(C, ThisLevel))
                    )[ID]
        ),
        ExpandColumn = Table.ExpandListColumn(SearchNextLevel, NextLevel)
    in
        ExpandColumn
in
    ExpandToNextLevel

为了使这个通用,我显然需要将扩展​​和追加放入递归循环中。我会在时间允许的情况下回到这里。


编辑:这是查询的递归版本,它使用逆透视而不是追加。

let
    Source = Table.FromRows({{0,null,null},{1,0,null},{2,0,null},{3,0,null},{4,1,2},{5,2,null},{6,4,null}},{"ID", "Primary", "Secondary"}),
    #"Changed Types" = Table.TransformColumnTypes(Source,{{"ID", Int64.Type}, {"Primary", Int64.Type}, {"Secondary", Int64.Type}}),
    IDCount = List.Count(List.Distinct(#"Changed Types"[ID])),
    RecursiveExpand = List.Generate(
        () => [i=0, InputTable = #"Changed Types"],
        each [i] < IDCount and
             List.NonNullCount(List.Last(Table.ToColumns([InputTable]))) > 0,
        each [
             CurrentLevel = if [i] = 0 then "ID" else "Level" & Text.From([i]),
             NextLevel = if [i] = 0 then "Level1" else "Level" & Text.From([i]+1),
             InputTable = ExpandNext([InputTable], NextLevel, CurrentLevel),
             i = [i] + 1
        ]
    ),
    FinalTable = List.Last(RecursiveExpand)[InputTable],
    #"Unpivoted Other Columns" = Table.UnpivotOtherColumns(FinalTable, {"Secondary", "Primary", "ID"}, "Level", "Subordinate"),
    #"Removed Other Columns" = Table.SelectColumns(#"Unpivoted Other Columns",{"ID", "Subordinate"}),
    #"Removed Duplicates" = Table.Distinct(#"Removed Other Columns"),
    #"Sorted Rows" = Table.Sort(#"Removed Duplicates",{{"ID", Order.Ascending}, {"Subordinate", Order.Ascending}})
in
    #"Sorted Rows"

它将继续扩展级别,直到扩展到下一个级别产生所有空值或达到最大级别数以防止无限循环。

要在 SQL 中获得您想要的结果,最简单的方法是使用递归 CTE。

在下面的示例中,我将工作分为两个 CTE。第一个将集合转换为成对的经理和下属。第二个 CTE 从第一个获取所有结果,然后使用 UNION ALL 连接到自身,其中第一个 CTE 的管理器是递归 CTE 中的下属。这将不断重复,直到没有可以进行的匹配。

因为一个下属可能有多个经理,所以可能会为每个祖先返回重复的行。因为从递归 CTE 返回结果时使用了 DISTINCT。

WITH all_reports AS (
    SELECT [Primary] [ManagerID], ID [Subordinate]
    FROM tbl
    WHERE [Primary] IS NOT NULL
    UNION
    SELECT [Secondary], ID
    FROM tbl
    WHERE [Secondary] IS NOT NULL
)
, recursive_cte AS (
    SELECT ManagerID, Subordinate
    FROM all_reports
    UNION ALL
    SELECT ancestor.ManagerID, descendant.Subordinate
    FROM recursive_cte ancestor
    INNER JOIN all_reports descendant ON descendant.ManagerID = ancestor.Subordinate
)
SELECT DISTINCT ManagerID, Subordinate
FROM recursive_cte

如果你想要经理和下属之间的距离,那么重写递归 CTE 如下:

SELECT ManagerID, Subordinate, 1 [Distance]
FROM all_reports
UNION ALL
SELECT ancestor.ManagerID, descendant.Subordinate, ancestor.Distance + 1
FROM recursive_cte ancestor
INNER JOIN all_reports descendant ON descendant.ManagerID = ancestor.Subordinate

简单的存储方式,恕我直言。所有诠释。只是一个连接点,但将满足我所看到的所有需求,并在各个方向上提供极大的灵活性空间。项目可以是一个小项目,也可以是一组项目,甚至可以是 department/company 层次结构。似乎动态和适应性是优先或排序。

+--+-------+---------+-------+--------+
|ID|project|over     |under  |level   |
|0 |14     |0        |9      |1       |
|1 |53     |4        |1      |2       |
|2 |4      |4        |4      |2       |
|3 |1      |4        |2      |3       |
|4 |1      |0        |7      |1       |
|5 |2      |4        |6      |1       |
|6 |4      |4        |8      |5       |
+--+-------+---------+-------+--------+

以扩展方式使用项目的一个示例是为 dept/company/facility/office/room/vendor/position 或您能想到的任何其他 "grouping" 添加正在进行的 "Mission Statement" 项目.为什么要让生活变得更复杂?如果需要历史信息,您有一天可能需要做的最糟糕的事情是将已完成项目的条目卸载到某种存档中。