【问题标题】:multi-parent hierarchy propagation in SQL/DAXSQL/DAX 中的多父层次结构传播
【发布时间】:2019-07-17 04:05:06
【问题描述】:

假设我有一个表格,其中描述了每个员工的主要和次要报告路线。让我们想象一个组织结构,其中 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,对于首席执行官(0),我希望看到任何员工总裁以外的成员。我们的新实习生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 操作,但我正在努力解决(我认为)解决此问题所需的重入性。我的背景是程序编程,我认为这是我在这里苦苦挣扎的部分原因。

NB:我想在这里预料到的一个明显问题是“这肯定是一个 XY 问题 - 你到底为什么要这样做?”

我想在 PowerBI 中使用row-level security,让每个员工都可以访问有关组织结构中其下属个人的某些信息。不幸的是,RLS 不允许每个人执行存储过程,所以我坚持做这种组合扩展,然后简单地根据登录名过滤上表。

话虽如此,我对解决这个问题的更好方法持开放态度。

【问题讨论】:

    标签: sql-server tsql powerbi dax ssas-tabular


    【解决方案1】:

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

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

    以扩展方式使用项目的一个示例是为部门/公司/设施/办公室/房间/供应商/职位或您可以想到的任何其他“分组”添加一个正在进行的“使命宣言”项目层次结构需要分辨率。为什么要让生活变得更复杂?如果需要历史信息,您可能有一天需要做的最糟糕的事情是将已完成项目的条目卸载到某种存档中。

    【讨论】:

    • 欢迎来到 SO :) 我不太清楚这个回复在说什么。除非你能澄清它,否则我会标记这个。
    • 作为提供功能的链接表,可能不是您想要的,但如果遇到同样的问题,允许未来范围蔓延或只是扩展功能,我会这样做。
    • 不是 dax 解决方案,只是 t-sql,80% 只是内部连接,以获取所需的信息,从您想要启动的任何地方,从人员、项目、关系级别的入口点。成为 sql dev 近 20 年了,从 sql 2000 开始,大约在 1999 年,所以我首先看 sql 并尽可能简单地开始。我没有提出疑问,因为入口点非常多,而且感觉太明显了,并不意味着看起来令人困惑。请接受我的入侵道歉。随着时间的推移,我会在这里站稳脚跟,感谢您的欢迎! :)
    【解决方案2】:

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

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

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

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

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


    编辑:当你的结构不是一棵树时,问题变得更加困难。这是一种应该可行的方法。

    对于每个ID,查找下属得到Level1,搜索Level1下一级下属,以此类推,直到没有下属。 (如果你的结构中有一个循环让你回到更高的层次,那么你就会陷入递归。)

    在这种情况下,顶部下方有三个级别,因此我们需要三个步骤。

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

    它将不断扩展关卡,直到扩展至下一个关卡产生所有空值或达到最大关卡数以防止无限循环。

    【讨论】:

    • 如果4 有一个主要的下属怎么办?在这种情况下,我希望它们也显示为 2 的下属,但这不适用于上述情况,因为这些行被视为单独的。
    • 所以你的层次结构不是一棵树,也没有唯一的向上路径。这肯定会让事情变得更加困难。我建议编辑您的帖子以添加此示例(即添加 6,4,NULL 行)。
    • 是的,没错 - 我添加了一名实习生(员工6),其经理拥有主要和次要报告,这说明了这一要求。很抱歉之前没有说得足够清楚。
    【解决方案3】:

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

    在下面的示例中,我将工作分为两个 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
    

    【讨论】:

    • 干杯这按预期工作。我不明白的一件事是递归的停止条件是什么。为什么descendant 上的ancestorINNER JOIN 不会导致无限循环?
    • 这可能是个问题,但有帮助的是在每次迭代中它只会发送上一次迭代的行。因此,只要您在层次结构中没有后代可以成为其祖先经理之一的经理的情况,那么您就可以了。
    【解决方案4】:

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

    请参阅DAX Patterns: Parent-Child Hierarchies,了解如何在 DAX 中完全做到这一点。或者,您可以使用使用递归公用表表达式的 SQL Server 查询来展平两个层次结构。

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

    【讨论】:

    • 在我的例子中,它们并不是分开的。请参阅我对 Alexis Olson 的回答的评论。
    • 嗯,这类问题有一个通用的解决方案:实现关系中的所有对。因此,对于每个员工,每个员工都可以看到一行。
    • 这就是我希望实现的目标,(请参阅我的问题中的最后一个表格)但我不知道该怎么做。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多