5
votes

propagation de hiérarchie multiparentale dans SQL / DAX

Supposons que je dispose d'un tableau décrivant les liens hiérarchiques principaux et secondaires pour chaque membre du personnel. Imaginons une structure organisationnelle où le PDG, employé 0 , a 2 managers ( 1 et 2 ) qui lui sont rattachés.

Manager 2 a 2 membres du personnel dans son équipe ( 3 et 4 ), mais le membre du personnel 4 travaille en fait dans Manager 1 , donc bien qu'il ait 2 comme rapport principal, il fait également rapport au gestionnaire 1 comme rapport secondaire de sorte que 1 peut remplir les obligations de gestion fiduciaire normales (fournir un soutien, etc.).

En plus de prendre un rôle de gestion secondaire pour l'employé 4 , Manager 2 a également un membre de l'équipe qui relève de lui ( 5 ).

Modifier: Pour illustrer le problème multiparentale, donnons au membre de l'équipe 4 un stagiaire, membre du personnel 6 . Le membre de l'équipe 6 est désormais le subordonné des deux responsables 1 et 2 - ce dernier étant hérité du reporting secondaire ligne.

La structure organisationnelle ressemblerait à ceci:

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

Maintenant, je veux développer cela dans une vue SQL qui me donne une liste de personnes ci-dessous tout membre du personnel donné, couvrant à la fois les rapports primaires et secondaires. Donc, pour le membre du personnel 2 (le responsable avec un rapport principal et secondaire), je m'attendrais à voir les membres de l'équipe 4 et 5 , et pour le PDG ( 0 ) Je m'attendrais à voir un membre du personnel autre que le PDG. Notre nouveau stagiaire, 6 , est le subordonné du PDG, des managers 1 et 2 , ainsi que de son manager direct, 4 .

Cela ressemblerait à ceci:

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

Comment pourrais-je y parvenir en SQL? Je pense à une sorte d'opération OUTER APPLY sur l'ID, mais j'ai du mal à comprendre la réentrance qui serait nécessaire (je pense) pour résoudre ce problème. Mon expérience est en programmation procédurale, ce qui, je pense, est en partie la raison pour laquelle je me bats ici.

NB : Une question évidente que j'aimerais anticiper ici est "C'est sûrement un problème XY - pourquoi diable voudriez-vous faire ça?"

Je veux utiliser sécurité au niveau des lignes dans PowerBI pour donner à chaque membre du personnel l'accès à certaines informations sur les personnes en dessous d'eux dans la structure organisationnelle. Malheureusement, RLS ne permet pas l'exécution de procédures stockées par individu, donc je suis obligé de faire cette expansion combinatoire et de filtrer simplement le tableau ci-dessus en fonction de la connexion.

Cela dit, je suis ouvert à de meilleures façons d'aborder ce problème.


0 commentaires

4 Réponses :


2
votes

Vous devrez aplatir la hiérarchie des rapports et la hiérarchie des rapports secondaires, en les chargeant dans des tableaux séparés dans le modèle tabulaire.

Voir DAX Patterns: Parent-Child Hierarchies pour savoir comment procéder complètement. dans DAX. Ou vous pouvez utiliser une requête SQL Server à l'aide d'une expression de table commune récursive pour aplatir les deux hiérarchies.

Dans les deux cas, ils deviennent deux tables séparées dans le modèle et deux relations séparées, que vous pouvez ensuite référencer dans vos filtres RLS.


3 commentaires

Dans mon exemple, ils ne sont pas séparés. Voir mon commentaire sur la réponse d'Alexis Olson.


Eh bien, il existe une solution générale à ce genre de problème: matérialiser toutes les paires dans la relation. Donc, pour chaque employé, ayez une ligne pour chaque autre employé que l'employé peut voir.


C'est ce que j'espère réaliser (voir le dernier tableau de ma question) mais je ne sais pas comment faire cela.



4
votes

Ceci est assez facilement résolu en utilisant les fonctions de hiérarchie parent-enfant dans DAX. Je ne pense pas que vous ayez besoin de créer des tables supplémentaires, respectez simplement les conditions suivantes dans vos règles RLS:

Pour l'employé N , il vous suffit de vérifier si

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"

ou

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

Notez que cela permet à l'employé N de se voir ainsi que ses subordonnés, mais vous pouvez ajouter une condition supplémentaire si vous ne le souhaitez pas.


Modifier: Lorsque votre structure n'est pas un arbre, le problème devient plus difficile. Voici une approche qui devrait fonctionner.

Pour chaque ID , trouvez les subordonnés pour obtenir Level1 , recherchez Level1 pour le niveau suivant de subordonnés, et ainsi de suite jusqu'à ce qu'aucun subordonné n'existe. (Si vous avez une boucle dans votre structure qui vous ramène à un niveau supérieur, alors vous serez coincé dans la récursivité.)

Dans ce cas, il y a trois niveaux en dessous du sommet donc nous avons besoin de trois étapes .

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"

Voici le code M pour faire cela dans l'éditeur Power Query:

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

Voici la fonction personnalisée utilisée plusieurs fois pour passer au niveau suivant:

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

Pour rendre cela général, j'ai évidemment besoin de mettre l'expansion et l'ajout dans une boucle récursive. J'y reviendrai dès que le temps me le permettra.


Modifier: Voici une version récursive de la requête qui utilise un pivotement au lieu de l'ajout.

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

Il continuera à augmenter les niveaux jusqu'à ce que l'expansion au niveau suivant produise toutes les valeurs nulles ou atteigne le nombre maximum de niveaux pour éviter une boucle infinie.


3 commentaires

Que faire si 4 a un subordonné principal? Dans ce cas, je voudrais qu'ils apparaissent également en tant que subordonnés de 2 , mais cela ne fonctionnerait pas avec ce qui précède car les lignes sont traitées comme séparées.


Donc, votre hiérarchie n'est pas un arbre alors et il n'y a pas de chemin unique vers le haut. Cela rend certainement les choses plus difficiles. Je vous recommande de modifier votre message pour ajouter cet exemple (c'est-à-dire ajouter une ligne 6,4, NULL ).


Ouais c'est vrai - j'ai ajouté un stagiaire (staff 6 ) dont le responsable a des rapports primaires et secondaires, ce qui illustre cette exigence. Toutes mes excuses pour ne pas avoir expliqué cela suffisamment avant.



2
votes

Pour obtenir le résultat souhaité en SQL, le moyen le plus simple d'y parvenir est d'utiliser un CTE récursif.

Dans l'exemple ci-dessous, je divise le travail en deux CTE. Le premier transforme l'ensemble en paires de managers et de subordonnés. Le deuxième CTE obtient tous les résultats du premier, puis se joint à lui-même en utilisant UNION ALL où le gestionnaire du premier CTE est un subordonné dans le CTE récursif. Cela continuera à se répéter jusqu'à ce qu'il n'y ait aucune correspondance qui puisse être faite.

Comme il est possible qu'un subordonné ait plus d'un gestionnaire, des lignes en double peuvent être retournées pour chaque ancêtre. Pour cette raison, DISTINCT est utilisé lors du renvoi des résultats du CTE récursif.

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

Si vous voulez la distance entre le manager et le subordonné, réécrivez le CTE récursif comme suit:

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


2 commentaires

Bravo, cela fonctionne comme prévu. Une chose que je ne comprends pas, c'est quelle est la condition d'arrêt de la récursivité. Pourquoi le INNER JOIN de ancêtre sur descendant ne provoque-t-il pas une boucle infinie?


Cela peut être un problème, mais ce qui aide, c'est qu'à chaque itération, il n'enverra que les lignes de la dernière itération. Donc, tant que vous n'avez pas de situation dans la hiérarchie où un descendant peut être le gestionnaire de l'un des gestionnaires de son ancêtre, tout ira bien.



-1
votes

Un moyen simple de stocker, à mon humble avis. Tout int. Seul un point de jonction mais qui comblera tous les besoins que je peux voir exprimés avec place pour une grande flexibilité dans toutes les directions. Le projet peut être soit un petit projet, soit un regroupement de projets et même une hiérarchie de service / entreprise. On dirait que dynamique et adaptable sont une priorité ou des sortes.

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

Un exemple d'utilisation d'un projet de manière étendue serait l'ajout d'un projet "Énoncé de mission" en cours pour un service / entreprise / établissement / bureau / room / vendor / position ou tout autre "groupement" auquel vous pouvez penser où une résolution de hiérarchie est souhaitée. Pourquoi rendre la vie plus compliquée? La pire chose que vous ayez à faire un jour est de décharger les entrées des projets terminés dans une archive quelconque si le besoin d'informations historiques est nécessaire.


3 commentaires

Bienvenue à SO :) Je ne vois pas vraiment ce que dit cette réponse. À moins que vous ne puissiez clarifier cela, je vais signaler cela.


en tant que table de liaison fournissant des fonctionnalités, ce n'est peut-être pas ce que vous recherchez, mais c'est ce que je ferais si on me présentait le même problème permettant une dérive future de la portée ou simplement des fonctionnalités étendues.


pas une solution dax, juste t-sql, 80% ne sont que des jointures internes pour accéder aux informations souhaitées, des points d'entrée d'où vous souhaitez initier, de la personne, du projet, du niveau de relation. été un développeur sql depuis près de 20 ans maintenant, a commencé en sql 2000, vers 1999, donc je regarde d'abord sql et commence aussi simple que possible. Je n'ai pas donné de questions parce que les points d'entrée sont si nombreux et que cela semblait trop évident, ne voulait pas paraître obscurcissant. veuillez accepter mes excuses pour votre intrusion. Je vais prendre pied ici au fil du temps et merci pour l'accueil! :)