Introduction
Hierarchy
is a very useful funcion in Qlik Sense, that can transform a parent-child hierarchy table. A similar funcionality can be replicated with a recursive CTE in T-SQL. A hierarchy where a child node has a single parent can also be transformed using Parent and Child functions, such as PATH
in DAX in Power BI, however these functions cannot handle data where a child node might have multiple parents. This is often the case with components and assemblies in a Bill of Materials, because a single component might be required for multiple assemblies.
Qlik Sense
Hierarchy syntax
Hierarchy (NodeID, ParentID, NodeName, [ParentName, [ParentSource, [PathName, [PathDelimiter, Depth]]]])(loadstatement | selectstatement)
input
NodeID | ParentID | NodeName |
---|---|---|
1 | 4 | London |
2 | 3 | Munich |
3 | 5 | Germany |
4 | 5 | UK |
5 | Europe |
Qlik function
Hierarchy(NodeID, ParentID, NodeName, ParentName, NodeName, PathName, '\', Depth)
output
NodeID | ParentID | NodeName | NodeName1 | NodeName2 | NodeName3 | ParentName | PathName | Depth |
---|---|---|---|---|---|---|---|---|
1 | 4 | London | Europe | UK | London | UK | Europe\UK\London | 3 |
2 | 3 | Munich | Europe | Germany | Munich | Germany | Europe\Germany\Munich | 3 |
3 | 5 | Germany | Europe | Germany | - | Europe | Europe\Germany | 2 |
4 | 5 | UK | Europe | UK | - | Europe | Europe\UK | 2 |
5 | Europe | Europe | - | - | - | Europe | 1 |
AdventureWorks
This demonstration utilizes a sample dataset, that is provided by Microsoft, and it can be downloaded here: AdventureWorks
Qlik Sense
The solution is Qlik is fairly straight-forward as all the work is done by the Hierarchy
function. The BOMLevel
is adjusted by 1
, so that it aligns with the Depth
in the output. BOMLevel
starts at 0
and Depth
starts counting the levels in the hierarchy at 1
.
BOM_Hierarchy:
Hierarchy(ComponentID, ProductAssemblyID, ComponentName, AssemblyName, ComponentName, PathName, ' <- ', Depth)
SQL
SELECT
BillOfMaterialsID
, ProductAssemblyID
, ComponentID
, Product.Name as ComponentName
, BOMLevel + 1 as BOMLevel
, PerAssemblyQty
, UnitMeasureCode
FROM
Production.BillOfMaterials
INNER JOIN Production.Product
ON BillOfMaterials.ComponentID = Product.ProductID
WHERE
EndDate IS NULL;
T-SQL
1. Load Data into CTE
The first step is to load it into a CTE. This step can be omitted, but it is helpful to see, that the query is the same as in Qlik so far.
WITH BOM_CTE as (
SELECT
BillOfMaterialsID
, ProductAssemblyID
, ComponentID
, Product.Name as ComponentName
, BOMLevel + 1 as BOMLevel
, PerAssemblyQty
, UnitMeasureCode
FROM
Production.BillOfMaterials
INNER JOIN Production.Product
ON BillOfMaterials.ComponentID = Product.ProductID
WHERE
EndDate IS NULL
)
2. Recursive_CTE
The recursive CTE is able to flatten the hierarchy. This step adds the name of the parent node - AssemblyName
, complete path - PathName
, as well as Depth
. PathJson
is a slightly modified step of PathName
, which then allows us to use the JSON_VALUE
in the last step.
, Recursive_CTE as (
SELECT
*
, CAST(NULL as varchar(50)) as AssemblyName
, CAST(ComponentName as varchar(max)) as PathName
, '"' + CAST(ComponentName as varchar(max)) + '"' as PathJson
, 1 as Depth
FROM
BOM_CTE
WHERE
ProductAssemblyID IS NULL
UNION ALL
SELECT
t.BillOfMaterialsID
, t.ProductAssemblyID
, t.ComponentID
, t.ComponentName
, t.BOMLevel
, t.PerAssemblyQty
, t.UnitMeasureCode
, CAST(Recursive_CTE.ComponentName as varchar(50)) as AssemblyName
, Recursive_CTE.PathName + ' <- ' + CAST(t.ComponentName as varchar(max)) as PathName
, Recursive_CTE.PathJson + ', "' + CAST(t.ComponentName as varchar(max)) + '"' as PathJson
, Recursive_CTE.Depth + 1 as Depth
FROM
BOM_CTE as t
INNER JOIN Recursive_CTE
ON t.ProductAssemblyID = Recursive_CTE.ComponentID
)
3. Final output table
T-SQL
lacks proper functionality for splitting of a string by a delimiter. A workaround I found utilizes the JSON_VALUE
, which is able to extract a value by its index. All that is required is to add double quoutes "
and a comma ,
between each string, and then encapsulate the result in square brackets [
]
. Howerver, with this approach, it is necessary to know the maximum number of levels in the Hierarchy. The function in Qlik, on the other hand, can dynamically determine the number of levels and it adds the appropriate number of extra columns, one for each level.
SELECT
BillOfMaterialsID
, ProductAssemblyID
, ComponentID
, ComponentName
, BOMLevel
, PerAssemblyQty
, UnitMeasureCode
, JSON_VALUE('[' + PathJson + ']', '$[0]') as ComponentName1
, JSON_VALUE('[' + PathJson + ']', '$[1]') as ComponentName2
, JSON_VALUE('[' + PathJson + ']', '$[2]') as ComponentName3
, JSON_VALUE('[' + PathJson + ']', '$[3]') as ComponentName4
, JSON_VALUE('[' + PathJson + ']', '$[4]') as ComponentName5
, AssemblyName
, PathName
, Depth
FROM
Recursive_CTE;