· Par Équipe HeyData

Série : Les Technical Briefs d’Henric Cronström, relus pour Qlik Sense · article 6 sur 7

Hiérarchies dans Qlik Sense : de la table parent-enfant aux droits d’accès

Une table parent-enfant décrit parfaitement un arbre, mais Qlik Sense ne sait pas l’exploiter telle quelle. Deux préfixes de script, Hierarchy et HierarchyBelongsTo, la transforment en champs utilisables. Avec eux, on obtient des niveaux pour les tableaux croisés, la sélection d’un sous-arbre entier et des droits d’accès par branche.

Cet article synthétise et adapte à Qlik Sense le Technical Brief « Hierarchies » d’Henric Cronström (Qlik, novembre 2013). Ses documents techniques restent des références : nous les relisons avec les outils d’aujourd’hui.

Le problème

Une hiérarchie est un ensemble de nœuds qui ont chacun un seul parent. Le calendrier, la gamme de produits, la géographie ou l’organigramme en sont des exemples. Les feuilles, elles, sont en général les transactions de la table de faits, rattachées chacune à un nœud.

Toutes les hiérarchies ne se ressemblent pas. Quatre questions permettent de les qualifier :

  • Équilibrée ou non ? Dans une hiérarchie équilibrée, toutes les transactions pointent vers le même niveau. Le calendrier l’est en général : chaque commande a une date.
  • Nombre de niveaux fixe ou variable ? Une hiérarchie peut garder un nombre fixe de niveaux et être déséquilibrée : un budget saisi au mois face à des ventes saisies au jour.
  • Niveaux nommés ou non ? « Pays » ou « Ville » méritent leur propre champ. Les appellations viticoles, elles, n’ont pas de nom de niveau stable : un district peut être au troisième comme au cinquième rang.
  • Irrégulière (ragged) ? Il manque un niveau sur certaines branches. Washington DC appartient aux États-Unis sans appartenir à aucun État.

Le cas simple, des niveaux nommés et fixes, se règle avec une dimension drill-down. Le cas difficile est la hiérarchie déséquilibrée à profondeur variable. Elle arrive presque toujours sous forme de table d’adjacence.

Comment la source stocke l’arbre

Les bases relationnelles ont plusieurs façons de ranger une hiérarchie. Il faut d’abord identifier la sienne :

  • Horizontale : un champ par niveau (client, pays, région), parfois réparti sur plusieurs tables liées.
  • Table d’adjacence : une ligne par nœud, avec une clé vers son parent. C’est le format le plus répandu, car déplacer une branche revient à modifier un seul ParentID.
  • Énumération de chemins : chaque nœud stocke son chemin complet (1/178/281).
  • Ensembles imbriqués : chaque nœud porte une borne basse et une borne haute qui englobent celles de ses descendants.
  • Table des ancêtres : une ligne par couple ancêtre / descendant. Rare comme stockage, fréquente comme vue SQL.

Adjacence, chemins et ensembles imbriqués décrivent entièrement l’arbre, mais aucun n’est exploitable tel quel. Les préfixes Hierarchy et HierarchyBelongsTo partent de la table d’adjacence.

Table d’adjacence à trois colonnes NodeID, ParentID et Name, avec neuf districts viticoles de The World à Médoc ; à droite, l’arbre correspondant en retrait : The World a pour enfants Americas et Europe, Europe a pour enfants France et Germany, France contient Bordeaux qui contient Médoc. La ligne racine, sans ParentID, est surlignée.

Une table d’adjacence : chaque ligne ne connaît que son parent.

Niveaux nommés : une dimension drill-down suffit

Si les niveaux ont un nom et que toutes les transactions pointent vers le même niveau, chargez la table telle quelle. Créez ensuite une dimension drill-down dans les éléments principaux (Année > Trimestre > Mois, par exemple). Elle remplace le groupe hiérarchique de QlikView et s’utilise dans tous les graphiques. Quand une seule valeur reste possible à un niveau, la visualisation descend au suivant.

Si les faits pointent vers des niveaux différents, comme le budget au mois et le réel au jour, la hiérarchie est déséquilibrée. Le Technical Brief renvoie alors aux clés génériques, traitées dans un autre article de la série.

Hierarchy : un champ par niveau

Le préfixe Hierarchy se place devant un LOAD qui lit la table d’adjacence. Il exige trois champs : l’identifiant du nœud, celui du parent et un nom. Un parent introuvable ou vide fait du nœud une racine. Si la source n’a pas de libellé, NodeID as Name fait l’affaire.

// Expanded nodes table: one row per node, one column per level
[Nodes]:
Hierarchy (NodeID, ParentID, Node, NodeParent, Node, NodePath, '/', NodeDepth)
LOAD
    NodeID,
    ParentID,
    NodeName as Node
FROM [lib://DataFiles/WineDistricts.qvd] (qvd);

Le résultat garde une ligne par nœud et ajoute Node1 à NodeN, un champ par niveau. Les paramètres optionnels ajoutent le nom du parent, le chemin (NodePath) et la profondeur (NodeDepth).

Table des nœuds développés : pour chacun des neuf districts, les colonnes Node1 à Node5 reprennent la lignée depuis The World ; la ligne Médoc est surlignée et remplit les cinq niveaux, The World, Europe, France, Bordeaux, Médoc, tandis que les niveaux inexistants des autres nœuds sont marqués d’un tiret.

Après Hierarchy : la lignée de chaque nœud se lit de gauche à droite.

Dans Qlik Sense, Node1, Node2… servent de lignes à un tableau croisé dynamique. Les montants rattachés directement à un nœud intermédiaire apparaissent sur une ligne de niveau vide (« – ») sous ce nœud. Selon la version, l’option d’indentation des lignes rapproche l’affichage d’un arbre.

Capture d’une table Qlik Cloud avec les colonnes Node2 à Node5 et Sum(Amount), total 470. Americas, United States, California, Node5 vide : 100. Europe, France, Bordeaux, Médoc : 250. Europe, France, Bordeaux, Node5 vide : 40. Europe, Germany, Node4 et Node5 vides : 80. Les niveaux vides s’affichent par un tiret sur fond gris.

Dans Qlik Cloud : les 40 rattachés à Bordeaux, nœud intermédiaire, s’affichent sur une ligne dont le niveau suivant est vide.

QlikView affichait NodePath dans une list box en vue arborescente. Qlik Sense n’a pas d’équivalent natif dans le volet de filtres. Pour visualiser l’arbre, l’organigramme du Visualization bundle travaille à partir d’un couple identifiant / parent. Pour naviguer, nous utilisons plutôt une dimension drill-down sur les niveaux.

HierarchyBelongsTo : sélectionner un sous-arbre

La table des nœuds a une limite. Elle permet de sélectionner un nœud, mais pas « Europe et tout ce qu’elle contient ». C’est pourtant la sélection la plus naturelle. Le préfixe HierarchyBelongsTo répond à ce besoin.

// Ancestor table: one row per node / ancestor pair
[TreeBridge]:
HierarchyBelongsTo (NodeID, ParentID, NodeName, TreeID, TreeName)
LOAD
    NodeID,
    ParentID,
    NodeName
FROM [lib://DataFiles/WineDistricts.qvd] (qvd);

Il produit une table des ancêtres : chaque nœud y figure une fois par ancêtre, lui-même compris. Un sixième paramètre optionnel ajoute l’écart de profondeur entre le nœud et l’ancêtre.

Table des ancêtres à quatre colonnes NodeID, NodeName, TreeID, TreeName : France apparaît trois fois, rattachée à The World, Europe et France ; les lignes dont TreeName vaut Europe sont surlignées. À droite, l’arbre montre la sélection TreeName = Europe : Europe, France, Bordeaux, Médoc et Germany sont mis en évidence, soit cinq nœuds possibles.

Sélectionner Europe dans TreeName rend possible toute la branche.

Le champ qui porte le nom du nœud doit s’appeler autrement que dans la table Nodes : NodeName ici, Node là-bas. Sinon, les deux tables partagent deux champs et Qlik crée une clé synthétique. La visionneuse du modèle de données permet de le vérifier après chargement.

On peut enfin charger une seconde fois la hiérarchie avec Hierarchy, cette fois sur les identifiants d’arbre. Ses champs Tree1 à TreeN alimentent une dimension drill-down qui navigue de sous-arbre en sous-arbre.

[Trees]:
Hierarchy (TreeID, TreeParentID, Tree)
LOAD
    NodeID as TreeID,
    ParentID as TreeParentID,
    NodeName as Tree
FROM [lib://DataFiles/WineDistricts.qvd] (qvd);

Le modèle complet

Les trois tables jouent chacune un rôle. Toutes ne sont pas nécessaires : ne chargez que celles dont vos analyses ont besoin.

Modèle de données : la table Facts (NodeID, TransactionID, Amount) est reliée par NodeID à Nodes, qui porte Node, Node1 à Node5 et NodePath, et à TreeBridge, qui porte TreeName et PERMISSIONS ; TreeBridge est reliée par TreeID à Trees, qui porte Tree1 à Tree5. Une table Authorization en Section Access (ACCESS, USERID, PERMISSIONS) est reliée à TreeBridge par PERMISSIONS.

Nodes pour les niveaux, TreeBridge pour les sous-arbres, Trees pour le drill-down.

Des droits d’accès par branche

La hiérarchie sert aussi à sécuriser. Dans un organigramme, un responsable doit voir son département et tous ses sous-départements, mais pas les autres. Le champ TreeName est le bon champ de réduction : autoriser « Product », c’est autoriser tout le sous-arbre Product.

Quand la source est une table d’adjacence, il suffit de dériver une copie en majuscules de TreeName. Les valeurs de réduction de la Section Access doivent en effet être en majuscules.

// TempTreeBridge was loaded with HierarchyBelongsTo
[TreeBridge]:
LOAD *, Upper(TreeName) as PERMISSIONS
RESIDENT [TempTreeBridge];
DROP TABLE [TempTreeBridge];

Section Access;
Authorization:
LOAD
    ACCESS,
    USERID,
    Upper(Permissions) as PERMISSIONS
FROM [lib://DataFiles/Organization.xlsx] (ooxml, embedded labels, table is Access);
Section Application;

En Client-Managed, USERID prend la forme DOMAINE\utilisateur. N’oubliez pas la ligne INTERNAL\SA_SCHEDULER en ADMIN, sans quoi les rechargements planifiés échouent. En SaaS, vérifiez le format d’identifiant attendu par votre tenant. Le Technical Brief évoquait aussi la réduction par QlikView Publisher ; Qlik Sense passe par la Section Access.

Quand la hiérarchie est horizontale, HierarchyBelongsTo ne s’applique pas. On construit alors la table de pont à la main : une boucle sur les champs de niveau suffit.

FOR EACH vLevel IN 'Board level', 'Top level', 'Department', 'Unit'
    Trees:
    LOAD DISTINCT
        DepartmentID,
        Upper([$(vLevel)]) as PERMISSIONS
    RESIDENT [Horizontal Hierarchy];
NEXT vLevel

Chaque département reçoit une ligne par niveau qui le contient : une permission « CEO » ouvre toute la branche du CEO.

Contrôler l’intégrité de la source

Les hiérarchies déséquilibrées souffrent souvent de données incohérentes. Trois défauts méritent un contrôle avant de charger :

  • Doublons et parents multiples : un nœud décrit deux fois, ou rattaché à deux parents, multiplie les lignes produites (deux, quatre, huit…) selon le nombre d’ancêtres concernés. L’affichage devient incohérent et les performances se dégradent.
  • Parents absents : un ParentID sans ligne correspondante, souvent la racine, crée plusieurs racines. Ces racines ne sont pas sélectionnables.
  • Références circulaires : A a B pour parent, B a C, C a A. La résolution casse la boucle en supprimant un lien, voire exclut des nœuds. Henric Cronström recommande de toujours les éviter.
// Duplicates and multiple parents
CheckNodes:
LOAD
    NodeID,
    Count(NodeID) as NoOfInstances,
    Count(DISTINCT ParentID) as NoOfParents
RESIDENT Source
GROUP BY NodeID;

// Parents that are not listed as nodes
CheckParents:
LOAD
    NodeID,
    If(Exists(NodeID, ParentID), 'Existing', 'Missing') as ParentExistence
RESIDENT Source;

Les racines légitimes, sans parent, sortent aussi en « Missing » : c’est attendu. Pour les cycles, le document propose une boucle qui descend l’arbre niveau par niveau. Elle s’arrête quand plus aucun nœud n’est ajouté, ou quand un nœud réapparaît parmi ses propres ancêtres. Supprimez ces tables de contrôle une fois l’analyse faite.

À retenir

  • Identifiez le format source. Horizontale, adjacence, chemins, ensembles imbriqués ou ancêtres : le chargement en dépend.
  • Niveaux nommés, drill-down. Une dimension drill-down dans les éléments principaux suffit quand les niveaux sont nommés et fixes.
  • Hierarchy pour les niveaux. Le préfixe ajoute un champ par niveau, le chemin et la profondeur, sans changer le nombre de lignes.
  • HierarchyBelongsTo pour les sous-arbres. La table des ancêtres permet de sélectionner une branche entière d’un clic.
  • Des noms de champs distincts. Nommez le nœud différemment dans chaque table pour éviter les clés synthétiques.
  • Une réduction par branche. Le nom de l’ancêtre, en majuscules, devient le champ de réduction de la Section Access.
  • Contrôlez la source. Doublons, parents absents et cycles faussent l’arbre : vérifiez-les avant de charger.

Essayer dans Qlik Sense

Ce script reprend les exemples de l’article avec les données des schémas, chargées en INLINE : il ne dépend d’aucun fichier ni d’aucune base. Collez-le dans l’éditeur de chargement de données d’une application vide, choisissez l’exemple avec la variable vExemple, puis rechargez.

// Hiérarchies dans Qlik Sense : script d'essai
// D'après « Hierarchies », Technical Brief d'Henric Cronström (Qlik, 2013), adapté par HeyData.
// Collez-le dans l'éditeur de chargement de données d'une application vide, choisissez
// l'exemple, rechargez, puis ouvrez la visionneuse du modèle de données.
SET vExemple = 1;   // 1 à 3

IF $(vExemple) = 1 THEN
    // Exemple 1 : le modèle complet (quatre schémas de l'article)
    // Table d'adjacence : un nœud par ligne, ParentID vide pour la racine
    Source:
    LOAD NodeID, If(Len(ParentID), ParentID) as ParentID, NodeName
    INLINE [
    NodeID, ParentID, NodeName
    1, , The World
    20, 1, Americas
    79, 20, United States
    85, 79, California
    178, 1, Europe
    281, 178, France
    283, 281, Bordeaux
    288, 283, Médoc
    354, 178, Germany
    ];

    // Nœuds développés : une colonne par niveau
    [Nodes]:
    Hierarchy (NodeID, ParentID, Node, NodeParent, Node, NodePath, '/', NodeDepth)
    LOAD NodeID, ParentID, NodeName as Node
    RESIDENT Source;

    // Table des ancêtres : un couple nœud / ancêtre par ligne
    [TempTreeBridge]:
    HierarchyBelongsTo (NodeID, ParentID, NodeName, TreeID, TreeName)
    NoConcatenate LOAD NodeID, ParentID, NodeName
    RESIDENT Source;

    [TreeBridge]:
    NoConcatenate
    LOAD *, Upper(TreeName) as PERMISSIONS
    RESIDENT [TempTreeBridge];
    DROP TABLE [TempTreeBridge];

    // Arbres : Tree1 à Tree5 pour une dimension drill-down
    [Trees]:
    Hierarchy (TreeID, TreeParentID, Tree)
    LOAD NodeID as TreeID, ParentID as TreeParentID, NodeName as Tree
    RESIDENT Source;

    DROP TABLE Source;

    Facts:
    LOAD * INLINE [
    TransactionID, NodeID, Amount
    T1, 85, 100
    T2, 288, 250
    T3, 283, 40
    T4, 354, 80
    ];

    // Droits par branche, chargés ici comme une table ordinaire.
    // En Section Access, ils réduiraient les données ; ajoutez-la seulement sur une copie,
    // avec une ligne ADMIN pour votre propre compte.
    Authorization:
    LOAD ACCESS, USERID, Upper(Permissions) as PERMISSIONS
    INLINE [
    ACCESS, USERID, Permissions
    USER, ALICE, Europe
    USER, BOB, Americas
    ];
    // TreeName = Europe : 5 nœuds possibles, Sum(Amount) = 370.
    // USERID = ALICE : mêmes données, par PERMISSIONS.
END IF

IF $(vExemple) = 2 THEN
    // Exemple 2 : contrôler l'intégrité d'une source défectueuse
    Source:
    LOAD * INLINE [
    NodeID, ParentID, NodeName
    178, 1, Europe
    281, 178, France
    283, 281, Bordeaux
    283, 178, Bordeaux
    400, 999, Orphan
    ];

    // Doublons et parents multiples
    CheckNodes:
    LOAD
        NodeID,
        Count(NodeID) as NoOfInstances,
        Count(DISTINCT ParentID) as NoOfParents
    RESIDENT Source
    GROUP BY NodeID;

    // Parents absents de la liste des nœuds
    CheckParents:
    LOAD
        NodeID,
        If(Exists(NodeID, ParentID), 'Existing', 'Missing') as ParentExistence
    RESIDENT Source;
    // 283 a deux parents ; 178 (parent 1) et 400 (parent 999) ont un parent absent.
END IF

IF $(vExemple) = 3 THEN
    // Exemple 3 : une hiérarchie horizontale, sans HierarchyBelongsTo
    [Horizontal Hierarchy]:
    LOAD * INLINE [
    DepartmentID, Board level, Top level, Department, Unit
    D1, CEO, Sales, Retail, Shops
    D2, CEO, Sales, Wholesale, Key accounts
    D3, CEO, Operations, Logistics, Warehouse
    ];

    FOR EACH vLevel IN 'Board level', 'Top level', 'Department', 'Unit'
        Trees:
        LOAD DISTINCT
            DepartmentID,
            Upper([$(vLevel)]) as PERMISSIONS
        RESIDENT [Horizontal Hierarchy];
    NEXT vLevel
    // PERMISSIONS = SALES : D1 et D2 ; PERMISSIONS = CEO : les trois départements.
END IF

Source

« Hierarchies », QlikView Technical Brief, Henric Cronström (alias « hic » sur Qlik Community), 26 novembre 2013, publié sur Qlik Community dans la rubrique QlikView Documents. Tout le mérite du contenu original lui revient ; l’adaptation à Qlik Sense et ses éventuelles erreurs sont de HeyData.

Cet article fait partie de notre relecture des Technical Briefs : découvrir la série et les autres documents adaptés à Qlik Sense.