<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>hierarchy &#8211; SQLpowered.com</title>
	<atom:link href="https://sqlpowered.com/tag/hierarchy/feed/" rel="self" type="application/rss+xml" />
	<link>https://sqlpowered.com</link>
	<description>SQL Server + BI</description>
	<lastBuildDate>Sat, 15 Apr 2023 22:16:52 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://sqlpowered.com/wp-content/uploads/2020/07/FavIcon-e1594067873682-99x100.png</url>
	<title>hierarchy &#8211; SQLpowered.com</title>
	<link>https://sqlpowered.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Storing Hierarchical Data in SQL Server (Available Options)</title>
		<link>https://sqlpowered.com/storing-hierarchical-data-in-sql-server-available-options/</link>
					<comments>https://sqlpowered.com/storing-hierarchical-data-in-sql-server-available-options/#respond</comments>
		
		<dc:creator><![CDATA[Jan Dvořák]]></dc:creator>
		<pubDate>Sun, 20 Feb 2022 08:09:37 +0000</pubDate>
				<category><![CDATA[T-SQL]]></category>
		<category><![CDATA[hierarchy]]></category>
		<guid isPermaLink="false">https://sqlpowered.com/?p=5445</guid>

					<description><![CDATA[There are various options on how to save hierarchical data in SQL Server. This article is an overview of them demonstrating their usage on a simple dataset of employees and their managers. I will extend this article when a new option will be available or if you will advise it...]]></description>
										<content:encoded><![CDATA[<p>There are various options on how to save hierarchical data in SQL Server. This article is an overview of them demonstrating their usage on a simple dataset of employees and their managers. I will extend this article when a new option will be available or if you will advise it in Comment.</p>
<p>[toc]</p>
<h3>Preparing data</h3>
<p>Let&#8217;s create some sample data first. Nothing complicated: simple list of employees and their managers like it&#8217;s common in the organization hierarchy. It has two parent nodes (AMY and LAURA) and up to five child levels. I kept it simple for demonstration purposes and each employee&#8217;s name is unique and every employee has only one manager. Most of the options will work for multiple parents (navigation) paths too but it may require some changes. I will post on the multiple-parent hierarchy later.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE TABLE [#Source] (
	[Employee_Id] INT NOT NULL PRIMARY KEY,
	[Name] NVARCHAR(100) NOT NULL,
	[Manager] NVARCHAR(100) NULL
)

;WITH [c] ( [Employee_Id], [Name], [Manager] ) AS
(
	SELECT 1,  'AMY',	NULL        UNION ALL
	SELECT 2,  'DANIEL',	'AMY'       UNION ALL
	SELECT 3,  'EMILY',	'AMY'       UNION ALL
	SELECT 4,  'HANNAH',	'AMY'       UNION ALL
	SELECT 5,  'JACK',	'HANNAH'    UNION ALL
	SELECT 6,  'JAMES',	'HANNAH'    UNION ALL
	SELECT 7,  'JESSICA',	'JAMES'     UNION ALL
	SELECT 8,  'JOSHUA',	'JAMES'     UNION ALL
	SELECT 9,  'LAURA',	NULL        UNION ALL
	SELECT 10, 'LUKE',	'LAURA'     UNION ALL
	SELECT 11, 'MATTHEW',	'LAURA'     UNION ALL
	SELECT 12, 'OLIVIA',	'MATTHEW'   UNION ALL
	SELECT 13, 'REBECCA',	'MATTHEW'   UNION ALL
	SELECT 14, 'RYAN',	'REBECCA'   UNION ALL
	SELECT 15, 'SOPHIE',	'RYAN'	    UNION ALL
	SELECT 16, 'THOMAS',	'SOPHIE'
) 
INSERT INTO [#Source]
   ( [Employee_Id], [Name], [Manager] )
   SELECT [c].[Employee_Id], [c].[Name], [c].[Manager]
   FROM [c]

SELECT * FROM [#Source]
GO</pre>
<p>The script above will create [#Source] temporary table with the following data:</p>
<p><img fetchpriority="high" decoding="async" class="alignnone wp-image-5464" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Sample_Data_1.png" alt="" width="242" height="300" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Sample_Data_1.png 454w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Sample_Data_1-242x300.png 242w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Sample_Data_1-81x100.png 81w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Sample_Data_1-360x446.png 360w" sizes="(max-width: 242px) 100vw, 242px" /></p>
<p>Please note that AMY and LAURA have Manager fields empty. They are our super bosses, two top-level nodes where the hierarchy path tree is starting.</p>
<p>We will a little bit transform this basic dataset user recursive CTE and save it as an optimized dataset into the [#Data] table.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE TABLE [#Data] (
	[Employee_Id] INT NOT NULL PRIMARY KEY,
	[Name] NVARCHAR(100) NOT NULL,
	[Manager] NVARCHAR(100) NULL,
	[Manager_Id] INT NULL,
	[Level] INT NOT NULL,
	[Path] NVARCHAR(MAX) NOT NULL
)

;WITH [c] AS 
(
	SELECT [Employee_Id], [Name], [Manager], CAST(NULL AS INT) [Manager_Id], 1 [Level], CAST([Name] AS NVARCHAR(MAX)) AS [Path]
	FROM [#Source]
	WHERE [Manager] IS NULL

	UNION ALL
	
	SELECT [d].[Employee_Id], [d].[Name], [d].[Manager], [c].[Employee_Id], [c].[Level] + 1, [c].[Path] + '-&gt;' + [d].[Name]
	FROM [#Source] [d]
		INNER JOIN [c] ON [c].[Name] = [d].[Manager]
)
INSERT INTO [#Data]
	(	[Employee_Id], [Name], [Manager], [Manager_Id], [Level], [Path] )
	SELECT [Employee_Id], [Name], [Manager], [Manager_Id], [Level], [Path]
	FROM [c]

SELECT * FROM [#Data]
GO</pre>
<p>We can see the source hierarchy presented visually now and we will save some data processing later.</p>
<p><img decoding="async" class="alignnone wp-image-5447" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_2.png" alt="" width="719" height="291" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_2.png 1388w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_2-300x121.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_2-1024x415.png 1024w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_2-150x61.png 150w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_2-768x311.png 768w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_2-360x146.png 360w" sizes="(max-width: 719px) 100vw, 719px" /></p>
<h3>Adjacency list (Parent/Child)</h3>
<p>This is probably the most used option to persist hierarchical data in SQL Server (and other RDBMS). It persists the reference to the parent node in every child node.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-highlight="6-7">CREATE TABLE [dbo].[Employees_Adjacency]
(
   [Employee_Id] INT NOT NULL PRIMARY KEY,
   [Name] NVARCHAR(100) NOT NULL,
   [Parent_Employee_Id] INT NULL,
   CONSTRAINT [FK_Employees_Adjacency] 
      FOREIGN KEY ( [Parent_Employee_Id]) REFERENCES [dbo].[Employees_Adjacency] ([Employee_Id])
)
GO

INSERT INTO [dbo].[Employees_Adjacency]
(   [Employee_Id], [Name], [Parent_Employee_Id] )
   SELECT [Employee_Id], [Name], [Manager_Id]
   FROM [#Data]
GO

SELECT * FROM [dbo].[Employees_Adjacency]
GO</pre>
<p>We have created [dbo].[Employees_Adjacency] with the self-referencing [Parent_Employee_Id] column (note the highlighted FK constraint) and populated it with data from [#Data] table:</p>
<p><img decoding="async" class="alignnone wp-image-5449" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List.png" alt="" width="305" height="308" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List.png 557w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List-297x300.png 297w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List-99x100.png 99w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List-360x363.png 360w" sizes="(max-width: 305px) 100vw, 305px" /></p>
<p>It&#8217;s very similar to the original [#Source] except that Manager of every employee is expressed in the [Parent_Employee_Id] as a reference to the [Employee_Id] unique employee Id (Primary Key).</p>
<p>We can revert the process now and check if the parent-child hierarchy is correct using recursive CTE:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">;WITH [c] AS 
(
	SELECT [Employee_Id], [Name], [Parent_Employee_Id], CAST(NULL AS NVARCHAR(100)) [Parent_Employee_Name], 1 [Level], CAST([Name] AS NVARCHAR(MAX)) AS [Path]
	FROM [dbo].[Employees_Adjacency]
	WHERE [Parent_Employee_Id] IS NULL

	UNION ALL
	
	SELECT 
		[d].[Employee_Id], [d].[Name], [d].[Parent_Employee_Id], [d].[Name], [c].[Level] + 1, [c].[Path] + '-&gt;' + [d].[Name]
	FROM [dbo].[Employees_Adjacency] [d]
		INNER JOIN [c] ON [c].[Employee_Id] = [d].[Parent_Employee_Id]
)
SELECT [c].[Employee_Id], [c].[Name], [c].[Parent_Employee_Id], [c].[Parent_Employee_Name], [c].[Level], [c].[Path]
FROM [c]
</pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5450" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List_1.png" alt="" width="821" height="283" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List_1.png 1633w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List_1-300x103.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List_1-1024x352.png 1024w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List_1-150x52.png 150w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List_1-768x264.png 768w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List_1-1536x529.png 1536w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Adjacency_List_1-360x124.png 360w" sizes="auto, (max-width: 821px) 100vw, 821px" /></p>
<p>It perfectly matches our original source.</p>
<h3>Closure table</h3>
<p>This is similar to an adjacency list. The only main difference is that the parent-child relationship is stored separately from the data itself.</p>
<p>This can be done in two ways: We will simply move the hierarchy to a different table as it is or we will transform it a little bit and switch the key and parent-key column.</p>
<h4>Simple movement of the hierarchy to a dedicated table</h4>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE TABLE [dbo].[Employees]
(
	[Employee_Id] INT NOT NULL PRIMARY KEY,
	[Name] NVARCHAR(100) NOT NULL
)
GO

INSERT INTO [dbo].[Employees]
(	[Employee_Id], [Name] )
	SELECT 
		[Employee_Id], [Name]
	FROM [#Data]
GO

SELECT * FROM [dbo].[Employees]
GO</pre>
<p>We have created [dbo].[Employees] table and filled just employee ids and names. There is nothing about the hierarchy.</p>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5453" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_1.png" alt="" width="158" height="286" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_1.png 310w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_1-165x300.png 165w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_1-55x100.png 55w" sizes="auto, (max-width: 158px) 100vw, 158px" /></p>
<p>We need to create a dedicated table [dbo].[Employees_Hierarchy_1] to save all the hierarchical relations between individual employees now. Please note that the [Employee_Id] is the only participant in the unique primary key constraint.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-highlight="3">CREATE TABLE [dbo].[Employees_Hierarchy_1]
(
	[Employee_Id] INT NOT NULL PRIMARY KEY,
	[Parent_Employee_Id] INT NULL,
	CONSTRAINT [FK_Employees_Hierarchy_1_Employees_Employee_Id] FOREIGN KEY ( [Employee_Id]) REFERENCES [dbo].[Employees] ([Employee_Id]),
	CONSTRAINT [FK_Employees_Hierarchy_1_Employees_Parent_Employee_Id] FOREIGN KEY ( [Parent_Employee_Id]) REFERENCES [dbo].[Employees] ([Employee_Id]),
	CONSTRAINT [FK_Employees_Hierarchy_1] FOREIGN KEY ( [Parent_Employee_Id]) REFERENCES [dbo].[Employees_Hierarchy_1] ([Employee_Id])
)
GO

INSERT INTO [dbo].[Employees_Hierarchy_1]
( [Employee_Id], [Parent_Employee_Id] )
   SELECT [Employee_Id], [Manager_Id]
   FROM [#Data]
GO

SELECT * FROM [dbo].[Employees_Hierarchy_1]
GO</pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5465" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_0.png" alt="" width="213" height="289" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_0.png 413w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_0-220x300.png 220w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_0-73x100.png 73w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_0-360x490.png 360w" sizes="auto, (max-width: 213px) 100vw, 213px" /></p>
<p>We can bind it back together using two joins between the main data table and hierarchy table:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SELECT 
	e.[Employee_Id], e.[Name], pe.[Employee_Id], pe.[Name]
FROM [dbo].[Employees_Hierarchy_1] [eh1]
	INNER JOIN [dbo].[Employees] [e] ON [e].[Employee_Id] = [eh1].[Employee_Id]
	LEFT JOIN [dbo].[Employees] pe ON pe.[Employee_Id] = [eh1].[Parent_Employee_Id]
GO</pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5454" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_2.png" alt="" width="333" height="303" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_2.png 618w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_2-300x273.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_2-110x100.png 110w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_2-360x327.png 360w" sizes="auto, (max-width: 333px) 100vw, 333px" /></p>
<p>We will verify it using the recursive CTE again:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">;WITH [c] AS 
(
	SELECT e.[Employee_Id], e.[Name], eh1.[Parent_Employee_Id], CAST(NULL AS NVARCHAR(100)) [Parent_Employee_Name], 1 [Level], CAST([Name] AS NVARCHAR(MAX)) AS [Path]
	FROM [dbo].[Employees_Hierarchy_1] eh1
		INNER JOIN dbo.[Employees] e ON [e].[Employee_Id] = [eh1].[Employee_Id]
	WHERE eh1.[Parent_Employee_Id] IS NULL

	UNION ALL
	
	SELECT 
		[e].[Employee_Id], [e].[Name], [eh1].[Parent_Employee_Id], [e].[Name], [c].[Level] + 1, [c].[Path] + '-&gt;' + [e].[Name]
	FROM [dbo].[Employees_Hierarchy_1] eh1
		INNER JOIN [c] ON [c].[Employee_Id] = [eh1].[Parent_Employee_Id]
		INNER JOIN [dbo].[Employees] e ON [e].[Employee_Id] = [eh1].[Employee_Id]
)
SELECT [c].[Employee_Id], [c].[Name], [c].[Parent_Employee_Id], [c].[Parent_Employee_Name], [c].[Level], [c].[Path]
FROM [c]
ORDER BY 1
GO</pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5455" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_3.png" alt="" width="759" height="261" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_3.png 1633w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_3-300x103.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_3-1024x352.png 1024w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_3-150x52.png 150w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_3-768x264.png 768w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_3-1536x529.png 1536w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_3-360x124.png 360w" sizes="auto, (max-width: 759px) 100vw, 759px" /></p>
<p>This solution may look as it&#8217;s more complicated, with more tables and joins at first look. That&#8217;s right. But it may have various advantages if we will think about it from the data modification point of view. We can edit employees separately from their hierarchy. We can optimize the hierarchy for searching, nodes movements, etc. much easier in a dedicated table. I will come back to it in a related post.</p>
<h4>Moving the hierarchy to a real closure table (parent/node columns switch)</h4>
<p>The second option how to move the hierarchy to a separated table is very similar to the first one. The main difference is that the [Parent_Employee_Id] is moved to be the first column in the table and it&#8217;s forming a unique primary key with the [Employee_Id] column.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-highlight="3,4,6">CREATE TABLE [dbo].[Employees_Hierarchy_2]
(
	[Parent_Employee_Id] INT NOT NULL,
	[Employee_Id] INT NOT NULL,
	[Depth] INT NOT NULL,
	CONSTRAINT [PK_Employees_Hierarchy_2] PRIMARY KEY CLUSTERED ([Parent_Employee_Id], [Employee_Id]),
	CONSTRAINT [FK_Employees_Hierarchy_2_Employees_Parent_Employee_Id] FOREIGN KEY ( [Parent_Employee_Id]) REFERENCES [dbo].[Employees] ([Employee_Id]),
	CONSTRAINT [FK_Employees_Hierarchy_2_Employees_Employee_Id] FOREIGN KEY ( [Employee_Id]) REFERENCES [dbo].[Employees] ([Employee_Id])
)
GO</pre>
<p>We need to adjust the data load process. We can&#8217;t import the data as is because the two top-level nodes (AMY and LAURE) haven&#8217;t a manager assigned (NULL  value). This will lead to an error when we will try to insert NULL or duplicated value to the unique primary key. To override it we will generate self-referencing parents for top-level nodes. Same time we will generate a self-referencing node for every employee.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">;WITH [c] AS
(
	
	SELECT  [d].[Employee_Id] [Manager_Id], [d].[Employee_Id], 0 AS [Depth], CAST([d].[Name] AS NVARCHAR(MAX)) [Name]
	FROM [#Data] [d]

	UNION ALL

	SELECT [c].[Manager_Id], [d].[Employee_Id], [c].[Depth] + 1, [c].[Name] + '-&gt;' + [d].[Name]
	FROM [#Data] [d]
		INNER JOIN [c] ON [d].[Manager_Id] = [c].[Employee_Id]
)
INSERT INTO [dbo].[Employees_Hierarchy_2]
	(	[Parent_Employee_Id], [Employee_Id], [Depth] )
	SELECT 
		[c].[Manager_Id], [c].[Employee_Id], [c].[Depth]
	FROM [c]
GO

SELECT * FROM [dbo].[Employees_Hierarchy_2]
GO</pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5457" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_4.png" alt="" width="264" height="342" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_4.png 499w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_4-231x300.png 231w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_4-77x100.png 77w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_4-360x467.png 360w" sizes="auto, (max-width: 264px) 100vw, 264px" /></p>
<p>The new [Depth] column is one of the biggest benefits the closure table approach is bringing to us. In combination with self-referencing nodes it allows us very effectively search for all nodes at a given dept-level in a hierarchy:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-highlight="5">SELECT [h].[Parent_Employee_Id], [pe].[Name], [e].[Employee_Id], [e].[Name]
FROM [dbo].[Employees_Hierarchy_2] [h]
	INNER JOIN [dbo].[Employees] [e] ON [e].[Employee_Id] = [h].[Employee_Id]
	INNER JOIN [dbo].[Employees] [pe] ON [pe].[Employee_Id] = [h].[Parent_Employee_Id]
WHERE [h].[Depth] = 3
GO</pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5458" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_5.png" alt="" width="356" height="104" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_5.png 682w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_5-300x88.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_5-150x44.png 150w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Closure_Tables_5-360x105.png 360w" sizes="auto, (max-width: 356px) 100vw, 356px" /></p>
<p>If you will check the data returned with the visual presentation of the hierarch above it&#8217;s pretty good visible that we have obtained all employees that are in the distance of 3 levels from their parents.</p>
<p>See more in related links below on how to mine as much as possible from the closure tables concept.</p>
<h3>Nested Set</h3>
<p>Nested Sets or the <a href="https://en.wikipedia.org/wiki/Nested_set_model" target="_blank" rel="noopener">Nested Set Model</a> is another technique how to store hierarchical data. It&#8217;s using the concept of left and right bowers to number nodes according to tree traversal navigation. I will recommend reading the Wiki for more information because this option isn&#8217;t as intuitive as the others. I have prepared the sample using an amazing CTE-based solution published by <a href="https://www.sqlservercentral.com/articles/hierarchies-on-steroids-2-a-replacement-for-nested-sets-calculations-1" target="_blank" rel="noopener">Jeff Moden</a>. Read it for more details on how exactly the nested set&#8217;s generator works.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE TABLE [#Tally] ([N] INT NOT NULL PRIMARY KEY)

;WITH [Tally] ([N]) AS
(
	SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
	FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) [a] ([n])
		CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) [b] ([n])
		CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) [c] ([n])
)
INSERT INTO [#Tally] ([N])
	SELECT * FROM [Tally]
GO

;WITH [c] AS 
( 
	SELECT [Employee_Id], [Manager_Id], 1 AS [Level], CAST(CAST([Employee_Id] AS BINARY(4)) AS VARBINARY(4000)) AS [Sort]
	FROM [#Data]
	WHERE [Manager_Id] IS NULL
	
	UNION ALL 
 
	SELECT [e].[Employee_Id], [e].[Manager_Id], [c].[Level] + 1, CAST([c].[Sort] + CAST([e].[Employee_Id] AS BINARY(4)) AS VARBINARY(4000))
	FROM [#Data] [e]
		INNER JOIN [c] ON [c].[Employee_Id] = [e].[Manager_Id]
)
SELECT
    ISNULL([Employee_Id], 0) [Employee_Id], [Manager_Id], ISNULL([Level], 0) [Level], 
    ISNULL(CAST(0 AS INT),0) [LeftBower], ISNULL(CAST(0 AS INT),0) [RightBower],
    ROW_NUMBER() OVER (ORDER BY [Sort]) [NodeNumber],
    ISNULL(CAST(0 AS INT),0) [NodeCount], ISNULL([Sort],[Sort]) [Sort]
   INTO [dbo].[Employees_NestedSets]
FROM [c] 
OPTION (MAXRECURSION 0)

DECLARE @LeftBower INT;

;WITH [c] AS
( 
	SELECT CAST(SUBSTRING([h].[Sort],[t].[N],4) AS INT) [Employee_Id], 
        COUNT(*) [NodeCount]
	FROM [dbo].[Employees_NestedSets] [h], [#Tally] [t]
	WHERE [t].[N] BETWEEN 1 AND DATALENGTH([h].[Sort])
	GROUP BY SUBSTRING([h].[Sort], [t].[N], 4)
) 
UPDATE [h]
	SET @LeftBower = [LeftBower] = 2 * [NodeNumber] - [Level],
        [h].[NodeCount]  = [c].[NodeCount],
        [h].[RightBower] = ([c].[NodeCount] - 1) * 2 + @LeftBower + 1
FROM [dbo].[Employees_NestedSets] [h]
	INNER JOIN [c] ON [h].[Employee_Id] = [c].[Employee_Id]

ALTER TABLE [dbo].[Employees_NestedSets] ADD CONSTRAINT [PK_Employees_NestedSets] PRIMARY KEY CLUSTERED ([LeftBower], [RightBower])
CREATE UNIQUE INDEX [UX_Employees_NestedSets] ON [dbo].[Employees_NestedSets] ([Employee_Id]) 
ALTER TABLE [dbo].[Employees_NestedSets] ADD CONSTRAINT [FK_Employees_NestedSets_Employees_NestedSets_Manager_Id] FOREIGN KEY ([Manager_Id]) REFERENCES [dbo].[Employees_NestedSets] ([Employee_Id]) 

SELECT * FROM [dbo].[Employees_NestedSets]
GO</pre>
<p>We have created [#Tally] table with 1000 numbers to support quick processing for CTEs following. The final result set is generated into [dbo].[Employees_NestedSets] and this table is enriched with some constraints to enforce data consistency and support search performance.</p>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5470" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_0.png" alt="" width="892" height="300" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_0.png 1671w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_0-300x101.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_0-1024x344.png 1024w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_0-150x50.png 150w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_0-768x258.png 768w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_0-1536x517.png 1536w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_0-360x121.png 360w" sizes="auto, (max-width: 892px) 100vw, 892px" /></p>
<p>Green marked columns are responsible for the magic. These are the left and right boundary values used for navigation in the hierarchy. I.e., we would like to get the full path for THOMAS. There is no need for recursive CTE like with parent/child or other options. We will perform a simple WHERE condition search using the BETWEEN operator and concatenate Employee names using STRING_AGG() function into the final path:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-highlight="3,11">DECLARE @ThomasLB INT

SELECT @ThomasLB = [LeftBower] 
FROM [dbo].[Employees_NestedSets] [h]
	INNER JOIN [#Data] [e] ON [e].[Employee_Id] = [h].[Employee_Id]
WHERE [e].[Name] = 'THOMAS'

SELECT STRING_AGG([e].[Name], '=&gt;') WITHIN GROUP (ORDER BY [h].[Level]) [Path]
FROM [dbo].[Employees_NestedSets] [h]
	INNER JOIN [#Data] [e] ON [e].[Employee_Id] = [h].[Employee_Id]
WHERE @ThomasLB BETWEEN [LeftBower] AND [RightBower]
GO</pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5471" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_1.png" alt="" width="384" height="35" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_1.png 736w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_1-300x27.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_1-150x14.png 150w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_Nested_Sets_1-360x33.png 360w" sizes="auto, (max-width: 384px) 100vw, 384px" /></p>
<p>Amazing, right? But every solution has its own pros and cons so read the article carefully. For nested sets, the complexity is mainly at the level of data modifications when bowers should be recalculated.</p>
<h3>XML</h3>
<p>Storing hierarchical data in XML format is easy. XML is designed to be a set of nested nodes by its nature and it is natively supported in SQL Server.</p>
<p>We will build the hierarchical XML nodes tree using a recursive scalar function. Because temporary tables cannot be used in scalar functions we need to move data from the [#Data] table to the physical table [dbo].[Data]. Finally, FOR XML PATH will do the jobs and convert the dataset to an XML tree.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SELECT *
	INTO [dbo].[Data]
FROM [#Data]
GO

CREATE OR ALTER FUNCTION [SelectChild] (
	@key AS INT
)
RETURNS XML
BEGIN
    RETURN (
        SELECT 
            [Employee_Id] AS '@Id', [Name] AS '@Name',
            [dbo].[SelectChild]([Employee_Id])
        FROM [Data]
        WHERE [Manager_Id] = @key
        FOR XML PATH('Employee'), TYPE
    )
END
GO

SELECT 
    [Employee_Id] AS '@Id', [Name] AS '@Name',
    [dbo].[SelectChild]([Employee_Id])     
FROM [Data]
WHERE [Manager_Id] IS NULL
FOR XML PATH ('Employee'), ROOT('Employees')
GO</pre>
<p>And the XML:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="xml">&lt;Employees&gt;
  &lt;Employee Id="1" Name="AMY"&gt;
    &lt;Employee Id="2" Name="DANIEL" /&gt;
    &lt;Employee Id="3" Name="EMILY" /&gt;
    &lt;Employee Id="4" Name="HANNAH"&gt;
      &lt;Employee Id="5" Name="JACK" /&gt;
      &lt;Employee Id="6" Name="JAMES"&gt;
        &lt;Employee Id="7" Name="JESSICA" /&gt;
        &lt;Employee Id="8" Name="JOSHUA" /&gt;
      &lt;/Employee&gt;
    &lt;/Employee&gt;
  &lt;/Employee&gt;
  &lt;Employee Id="9" Name="LAURA"&gt;
    &lt;Employee Id="10" Name="LUKE" /&gt;
    &lt;Employee Id="11" Name="MATTHEW"&gt;
      &lt;Employee Id="12" Name="OLIVIA" /&gt;
      &lt;Employee Id="13" Name="REBECCA"&gt;
        &lt;Employee Id="14" Name="RYAN"&gt;
          &lt;Employee Id="15" Name="SOPHIE"&gt;
            &lt;Employee Id="16" Name="THOMAS" /&gt;
          &lt;/Employee&gt;
        &lt;/Employee&gt;
      &lt;/Employee&gt;
    &lt;/Employee&gt;
  &lt;/Employee&gt;
&lt;/Employees&gt;</pre>
<p>To be honest: This is just a demonstration. It will have poor performance and huge complexity in case of large hierarchies and various data modifications like nodes movement. But it might be a really good format for a simple exchange of hierarchical data structure between SQL Server and different database platforms, API, or external data sources.</p>
<h3>JSON</h3>
<p>With the JSON is the situation exactly the same as with the XML. We will change the scalar function to work with JSON instead of XML using FOR JSON PATH clause. All the rest is similar.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE OR ALTER FUNCTION [SelectChild_JSON] (
	@key AS INT
)
RETURNS NVARCHAR(MAX)
BEGIN
    RETURN (
        SELECT 
            [Employee_Id] AS 'Id', 
            [Name] AS 'Name',
            JSON_QUERY((SELECT [dbo].[SelectChild_JSON]([Employee_Id]))) [Employees]
        FROM [Data]
        WHERE [Manager_Id] = @key
        FOR JSON PATH
    )
END
GO

SELECT 
    [Employee_Id] AS 'Id', [Name] AS 'Name',
    JSON_QUERY((SELECT [dbo].[SelectChild_JSON]([Employee_Id]))) [Employees]
FROM [Data]
WHERE [Manager_Id] IS NULL
FOR JSON PATH, ROOT( 'Employees')
GO</pre>
<p>And the JSON:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="json">{
    "Employees": [
        {   "Id": 1, "Name": "AMY",
             "Employees": [ { "Id": 2, "Name": "DANIEL" },
                            { "Id": 3, "Name": "EMILY"  },
                            { "Id": 4, "Name": "HANNAH",
                            "Employees": [  { "Id": 5, "Name": "JACK" },
                                            { "Id": 6, "Name": "JAMES",
                                            "Employees": [  { "Id": 7, "Name": "JESSICA" },
                                                            { "Id": 8, "Name": "JOSHUA"  }
                                                         ]
                                            }
                                        ]
                            }
                        ]
        },
        {   "Id": 9, "Name": "LAURA",
            "Employees": [  { "Id": 10, "Name": "LUKE" },
                            { "Id": 11, "Name": "MATTHEW",
                            "Employees": [  { "Id": 12, "Name": "OLIVIA" },
                                            { "Id": 13, "Name": "REBECCA",
                                            "Employees": [ { "Id": 14, "Name": "RYAN",
                                                           "Employees": [ { "Id": 15, "Name": "SOPHIE",
                                                                          "Employees": [ { "Id": 16, "Name": "THOMAS" }]
                                                                          }  
                                                                        ]
                                                            }
                                                        ]
                                            }
                                        ]
                            }
                        ]
        }
    ]
}</pre>
<p>Nothing will change from the XML format regarding the complexity of processing and modifying.</p>
<h3>HierarchyId Data Type</h3>
<p>HiearachyId is the only real natively supported structure for handling hierarchical data in SQL Server. It&#8217;s implemented using CLR and you can read more on it in the <a href="https://docs.microsoft.com/en-us/sql/t-sql/data-types/hierarchyid-data-type-method-reference?view=sql-server-ver15" target="_blank" rel="noopener">documentation</a>.</p>
<p>We will create [dbo].[Employees_Hierarchy_Id] where the [Path] column is of the HIERARCHYID type. Next, we will prepare the value for the [Path] column in a recursive CTE: [Employee_Id] column values will be converted to a string and contacted to form the path value which is then inserted into the [Path] column. the &#8216;All&#8217; node will be created to make the example completed with one master node but this is just for demonstration, it will perfectly work with two parent nodes too.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-highlight="5">CREATE TABLE [dbo].[Employees_Hierarchy_Id]
(
	[Employee_Id] INT NOT NULL PRIMARY KEY,
	[Name] NVARCHAR(100) NOT NULL,
	[Path] HIERARCHYID NOT NULL
	CONSTRAINT UC_Employees_Hierarchy_Id_Path UNIQUE NONCLUSTERED ([Path])
)
GO

;WITH [c] AS 
(
	SELECT [Employee_Id], [Name], CAST('/' AS NVARCHAR(MAX)) + CAST([Employee_Id] AS NVARCHAR(MAX)) + '/' [Path]
	FROM [#Source]
	WHERE [Manager] IS NULL

	UNION ALL
	
	SELECT [d].[Employee_Id], [d].[Name], [c].[Path] + CAST([d].[Employee_Id] AS NVARCHAR(MAX)) + '/' AS [Path]
	FROM [#Source] [d]
		INNER JOIN [c] ON [c].[Name] = [d].[Manager]
)
INSERT INTO [dbo].[Employees_Hierarchy_Id]
	(	[Employee_Id], [Name], [Path] )
	SELECT 
		[c].[Employee_Id], [c].[Name], [c].[Path]
	FROM [c]
GO

INSERT INTO [dbo].[Employees_Hierarchy_Id]
(	[Employee_Id], [Name], [Path] )
	SELECT 0, 'All', '/'
GO

SELECT 
	[h].[Employee_Id], [h].[Name], [h].[Path], 
	[h].[Path].ToString() [Path_String], [h].[Path].GetLevel() [Level],
	[p].[Name] [Manager_Name], [h].[Path].GetAncestor(1).ToString() [Manager_Path]
FROM [dbo].[Employees_Hierarchy_Id] [h]
	LEFT JOIN [dbo].[Employees_Hierarchy_Id] [p] ON [p].[Path] = [h].[Path].GetAncestor(1)
GO</pre>
<p>Our hierarchy is now stored in the HIERARCHYID datatype column. We have also concerted the binary path value to human-readable form using the .ToString(). Please note the .GetLevel() function returning at which level the current node is. Then .GetAncestor() function was used to get the right Manager name.</p>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5459" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_1.png" alt="" width="642" height="317" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_1.png 1204w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_1-300x148.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_1-1024x506.png 1024w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_1-150x74.png 150w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_1-768x380.png 768w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_1-360x178.png 360w" sizes="auto, (max-width: 642px) 100vw, 642px" /></p>
<p>We can use the recursive CTE similarly to the options above and build the real path visually:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">;WITH [c] AS (
	SELECT [Employee_Id], [Name], [Path] [Parent_Path], CAST('/' + [Name] AS NVARCHAR(MAX)) [String_Path]
	FROM [dbo].[Employees_Hierarchy_Id]
	WHERE [Path].[GetAncestor](1) = 0x

	UNION ALL

	SELECT [eh].[Employee_Id], [eh].[Name], [Path], [c].[String_Path] + '/' + [eh].[Name]
	FROM [dbo].[Employees_Hierarchy_Id] [eh]
		INNER JOIN [c] ON [eh].[Path].[GetAncestor](1) = [c].[Parent_Path]
)
SELECT *
FROM [c]
GO</pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5460" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_2.png" alt="" width="608" height="292" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_2.png 1170w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_2-300x144.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_2-1024x492.png 1024w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_2-150x72.png 150w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_2-768x369.png 768w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Server_HIerarchy_Id_2-360x173.png 360w" sizes="auto, (max-width: 608px) 100vw, 608px" /></p>
<p>There is much more fun we can do with the HIERARCHYID data type column. It&#8217;s mostly compared to the Parent/Child approach. You should check it in the documentation or internet for usage/performance comparison and carefully decide which option will better fit your needs.</p>
<h3>Graph</h3>
<p>Grap&#8211;databases capabilities were added to SQL Server 2017 version. They are primarily designed to support the persistence and querying of complex relationships in data. But we can use it for parent-child relations too.</p>
<p>Lets create [dbo].[Employees] as NODEs table and [dbo].[IsManagerFor] as EDGE table. The NODEs table will store just a list of all employees. The EDGEs table will persist relations between employees. It&#8217;s very similar to the concept of moving the hierarchy outside of the primary table in the <em>Closure tables</em> chapter above.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE TABLE [dbo].[Employees] (
    [Employee_Id] INT PRIMARY KEY,
    [Name] NVARCHAR(100) NOT NULL
  ) AS NODE
GO

CREATE TABLE [dbo].[IsManagerFor] AS EDGE;
GO

INSERT INTO [dbo].[Employees]
	( [Employee_Id], [Name] )
	SELECT 
		[Employee_Id], [Name]
	FROM [#Data]
GO

INSERT INTO [dbo].[IsManagerFor]
	( $from_id, $to_id )
	SELECT 
		(SELECT $node_id FROM [dbo].[Employees] WHERE [Employee_Id] = [d].[Employee_Id]),
		(SELECT $node_id FROM [dbo].[Employees] WHERE [Employee_Id] = [d].[Manager_Id])
	FROM [#Data] [d]
	WHERE [d].[Manager_Id] IS NOT NULL

SELECT * FROM [dbo].[Employees]
SELECT * FROM [dbo].[IsManagerFor]
GO</pre>
<p>If we will select nodes and edges new columns are presented: $node_id column is basically the new graph-specific unique id of an Employee. The edge is just and relation $from_id to $to_id node.</p>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5462" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_2.png" alt="" width="947" height="514" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_2.png 1964w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_2-300x163.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_2-1024x556.png 1024w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_2-150x81.png 150w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_2-768x417.png 768w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_2-1536x834.png 1536w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_2-360x196.png 360w" sizes="auto, (max-width: 947px) 100vw, 947px" /></p>
<p>We can use the graph MATCH() function in WHERE clause to get a Manager for each Employee.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SELECT [e1].[Employee_Id], [e1].[Name] [Employee], [e2].[Employee_Id] [Manager_Id], [e2].[Name] [Manager]
FROM [dbo].[Employees] [e1], [dbo].[IsManagerFor], [dbo].[Employees] [e2]
WHERE MATCH([e1]-([IsManagerFor])-&gt;[e2])
GO</pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5463" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_3.png" alt="" width="353" height="290" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_3.png 604w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_3-300x246.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_3-122x100.png 122w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_3-110x90.png 110w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_3-360x296.png 360w" sizes="auto, (max-width: 353px) 100vw, 353px" /></p>
<p>Recursive CTE can be used to get the hierarchy visually:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">;WITH [c] AS 
(
	SELECT [e].$node_id [Node_Id], [e].[Employee_Id], [e].[Name], 1 [Level], [m].$from_id [Parent_Node_Id],  CAST([Name] AS NVARCHAR(MAX)) AS [Path]
	FROM [dbo].[Employees] [e]
		LEFT JOIN [dbo].[IsManagerFor] [m] ON [e].$node_id = [m].$from_id 
	WHERE [m].$to_id IS NULL

	UNION ALL
	
	SELECT 
		[e].$node_id, [e].[Employee_Id], [e].[Name], [c].[Level] + 1, [m].$to_id, [c].[Path] + '-&gt;' + [e].[Name]
	FROM [dbo].[IsManagerFor] [m]
		INNER JOIN [dbo].[Employees] [e] ON [m].$from_Id = [e].$node_id
		INNER JOIN [c] ON [c].[Node_Id] = [m].$to_Id
)
SELECT *
FROM [c]
ORDER BY [c].[Employee_Id]
GO</pre>
<p>See graph specific nodes in the list:</p>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5461" src="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_1.png" alt="" width="1180" height="275" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_1.png 2408w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_1-300x70.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_1-1024x239.png 1024w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_1-150x35.png 150w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_1-768x179.png 768w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_1-1536x358.png 1536w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_1-2048x478.png 2048w, https://sqlpowered.com/wp-content/uploads/2022/02/Storing_Hierarchical_Data_In_SQL_Graph_1-360x84.png 360w" sizes="auto, (max-width: 1180px) 100vw, 1180px" /></p>
<p>You will aks probably where is the advantage to other options in this simple case. There is none. The EDGEs table is just a dedicated table like in the case of closure tables. The real value will come in the case of multiple different relations ships between employees (i.e. adding IsFriendOf). I will recommend this <a href="https://www.red-gate.com/simple-talk/databases/sql-server/t-sql-programming-sql-server/sql-server-graph-databases-part-1-introduction/" target="_blank" rel="noopener">article</a> to start with.</p>
<p>That&#8217;s all for now. I was trying to keep it as simple as possible to just demonstrate available options.</p>
<p>I will maintain this list of links over time for you to get more in-depth.</p>
<h3>Links</h3>
<h4>Adjacency list (Parent/Child)</h4>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Adjacency_list" target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Adjacency_list</a></li>
<li>https://blog.duncanworthy.me/sql/hierarchical-data-pt1-adjacency-list/</li>
<li>https://explainextended.com/2009/09/25/adjacency-list-vs-nested-sets-sql-server/</li>
<li>https://www.sqlservercentral.com/articles/hierarchies-on-steroids-1-convert-an-adjacency-list-to-nested-sets</li>
</ul>
<h4>Closure tables</h4>
<ul>
<li>https://www.red-gate.com/simple-talk/databases/sql-server/t-sql-programming-sql-server/sql-server-closure-tables/</li>
</ul>
<h4>Nested sets</h4>
<ul>
<li>https://www.sqlservercentral.com/articles/hierarchies-on-steroids-2-a-replacement-for-nested-sets-calculations-1</li>
</ul>
<h4>HierarchyId</h4>
<ul>
<li>https://docs.microsoft.com/en-us/sql/t-sql/data-types/hierarchyid-data-type-method-reference?view=sql-server-ver15</li>
<li>https://docs.microsoft.com/en-us/sql/relational-databases/tables/tutorial-using-the-hierarchyid-data-type?view=sql-server-ver15</li>
<li>https://www.sqlshack.com/use-hierarchyid-sql-server/</li>
<li>https://blog.matesic.info/post/HierarchyID-data-type-performance-tips-and-tricks</li>
</ul>
<h4>Graph</h4>
<ul>
<li>https://www.red-gate.com/simple-talk/databases/sql-server/t-sql-programming-sql-server/sql-server-graph-databases-part-1-introduction</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://sqlpowered.com/storing-hierarchical-data-in-sql-server-available-options/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Converting Literal Path (node1/node2/&#8230;) to Parent-Child Table</title>
		<link>https://sqlpowered.com/converting-literal-path-node1-node2-to-parent-child-table/</link>
					<comments>https://sqlpowered.com/converting-literal-path-node1-node2-to-parent-child-table/#respond</comments>
		
		<dc:creator><![CDATA[Jan Dvořák]]></dc:creator>
		<pubDate>Fri, 11 Feb 2022 17:54:12 +0000</pubDate>
				<category><![CDATA[T-SQL]]></category>
		<category><![CDATA[hierarchy]]></category>
		<guid isPermaLink="false">https://sqlpowered.com/?p=5438</guid>

					<description><![CDATA[There isn&#8217;t built-in support for working with hierarchies in the SQL Server except for the hierarchyid data type. This means that we should solve most of the hierarchical data processing tasks using the old-school portfolio of standard T-SQL language. One of these tasks is a simple conversion of hierarchical paths...]]></description>
										<content:encoded><![CDATA[<p>There isn&#8217;t built-in support for working with hierarchies in the SQL Server except for the <a href="https://docs.microsoft.com/en-us/sql/relational-databases/hierarchical-data-sql-server?view=sql-server-ver15" target="_blank" rel="noopener">hierarchyid</a> data type. This means that we should solve most of the hierarchical data processing tasks using the old-school portfolio of standard T-SQL language. One of these tasks is a simple conversion of hierarchical paths represented as a string to a classic parent-child table. There are a few options for how to do it. One of them is based on the replacement of the string using CHARINDEX(). I don&#8217;t like this function because it&#8217;s limited by the condition that every part of the path must be unique and it can&#8217;t be easily extended or reused for further processing of the hierarchy. Thus I was thinking about a more flexible solution.</p>
<p>Let&#8217;s start with some sample data. I will use the traditional example with the hierarchy of people like managers-employees or company organizational tree.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-highlight="7,15">DROP TABLE IF EXISTS [#Source]

CREATE TABLE [#Source] ( [Path] NVARCHAR(MAX) NOT NULL )

INSERT INTO [#Source]
 ( [Path] )
      SELECT 'AMY' UNION ALL
	SELECT 'AMY/DANIEL' UNION ALL
	SELECT 'AMY/EMILY' UNION ALL
	SELECT 'AMY/HANNAH' UNION ALL
	SELECT 'AMY/HANNAH/JACK' UNION ALL
	SELECT 'AMY/HANNAH/JAMES' UNION ALL
	SELECT 'AMY/HANNAH/JAMES/JESSICA' UNION ALL
	SELECT 'AMY/HANNAH/JAMES/JOSHUA' UNION ALL
      SELECT 'LAURA' UNION ALL
	SELECT 'LAURA/LUKE' UNION ALL
	SELECT 'LAURA/MATTHEW' UNION ALL
	SELECT 'LAURA/MATTHEW/OLIVIA' UNION ALL
	SELECT 'LAURA/MATTHEW/REBECCA' UNION ALL
	SELECT 'LAURA/MATTHEW/REBECCA/RYAN' UNION ALL
	SELECT 'LAURA/MATTHEW/REBECCA/RYAN/SOPHIE' UNION ALL
	SELECT 'LAURA/MATTHEW/REBECCA/RYAN/SOPHIE/THOMAS'

SELECT * FROM [#Source]
GO</pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5440" src="https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path1.png" alt="" width="306" height="273" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path1.png 631w, https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path1-300x267.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path1-112x100.png 112w, https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path1-360x321.png 360w" sizes="auto, (max-width: 306px) 100vw, 306px" /></p>
<p>There are two top-level nodes (AMY, LAURA) marked and they have a different number of child levels. I keep names to be unique to make it simple but the code following works properly for nodes of the same level in a different hierarchy path too.</p>
<p>Let&#8217;s do the first processing step: split path string to rows and calculate some useful values we will use later.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-highlight="20,21,24,46" data-enlighter-linenumbers="true">DROP TABLE IF EXISTS [#PathsCache]

CREATE TABLE [#PathsCache] (
	[Id] INT NOT NULL PRIMARY KEY IDENTITY(1,1),
	[Path] NVARCHAR(MAX) COLLATE Czech_CI_AS NULL,
	[Path_Hash] BINARY(32) NULL,
	[Node_Name] NVARCHAR(MAX) COLLATE Czech_CI_AS NULL,
	[Level] TINYINT NOT NULL,
	[Depth] TINYINT NULL,
	[Parent_Path] NVARCHAR(MAX) COLLATE Czech_CI_AS NULL,
	[Parent_Path_Hash] BINARY(32) NULL,
	UNIQUE NONCLUSTERED ([Path_Hash], [Level])
)    

INSERT INTO [#PathsCache]
	(	[Path], [Path_Hash],  [Node_Name], [Level] )
	SELECT 
		[src].[Path], HASHBYTES('SHA2_256', [src].[Path]), [l].[Value], CAST([l].[Key] AS INT)
	FROM [#Source] [src]
		OUTER APPLY OPENJSON(N'["' + REPLACE([src].[Path], N'/', N'","') + N'"]') [l] 
	ORDER BY [src].[Path], CAST([l].[Key] AS INT)

UPDATE [t]
	SET [t].[Depth] = [l].[Depth]
FROM [#PathsCache] [t]
	OUTER APPLY (	SELECT MAX([l].[Level]) [Depth]
					FROM [#PathsCache] [l] 
					WHERE [l].[Path] = [t].[Path]
				) [l]

;WITH [c] AS
(
	SELECT 
		[Id], [Node_Name], [Path_Hash], [Level], CAST([Node_Name] AS NVARCHAR(MAX)) [Parent_Path]
	FROM [#PathsCache]
	WHERE [Level] = 0

	UNION ALL

	SELECT 
		[t].[Id], [t].[Node_Name], [c].[Path_Hash], [t].[Level], [c].[Parent_Path] + N'/' + [t].[Node_Name]
	FROM [c] 
		INNER JOIN [#PathsCache] [t] ON [t].[Path_Hash] = [c].[Path_Hash] AND [t].[Level] = [c].[Level] + 1
)
UPDATE [t]
	SET [t].[Parent_Path] = [c].[Parent_Path],
	    [t].[Parent_Path_Hash] = HASHBYTES('SHA2_256', [c].[Parent_Path])
FROM [#PathsCache] [t] 
	LEFT JOIN [c] ON [c].[Path_Hash] = [t].[Path_Hash] AND [c].[Level] = [t].[Level] - 1
OPTION(MAXRECURSION 0)

SELECT * FROM [#PathsCache]
GO</pre>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5441" src="https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path2.png" alt="" width="897" height="428" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path2.png 1524w, https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path2-300x143.png 300w, https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path2-1024x488.png 1024w, https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path2-150x72.png 150w, https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path2-768x366.png 768w, https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path2-360x172.png 360w" sizes="auto, (max-width: 897px) 100vw, 897px" /></p>
<p>I have marked lines doing the main part of the job:</p>
<ul>
<li>20. &#8211; The <a href="https://docs.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql?view=sql-server-ver15" target="_blank" rel="noopener">OPENJSON()</a> function is used to split the literal path into rows. You will say that <a href="https://docs.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql?view=sql-server-ver15" target="_blank" rel="noopener">STRING_SPLIT()</a> function can be used but this has one major limitation: order of output rows isn&#8217;t guaranteed except in Azure SQL or Managed instance which makes this function unusable for on-premise SQL instances because we need rows to be sorted exactly as they appear in the original path string. Another disadvantage of the STRING_SPLIT() function is that the delimiter can be only a single character long which makes it risky that the path delimiter will be contained in node names too.</li>
<li>21. &#8211; Individual strings (Node_Names) for every single Path are sorted by Key column returned from the OPENJSON() function. This column maintains the original position of an item (node) in the JSON array (path).</li>
<li>24. &#8211; The Depth column value is updated to the maximum Level for every unique Path. If there is the same Depth and Level value on a row we know that this row is the leaf level of the current Path.</li>
<li>46. &#8211; CTE is used to calculate the parent path for every row (node). It&#8217;s the path without the name of the node itself. SUBSTRING() function can do the same (simply remove node name from the path), but this will cause an issue in a case where there are multiple nodes with the same name in a path. CTE brings a safe solution and data safety outperforms the potential performance hit. Sure there are other options like replacing strings from right to left. Just play with it.</li>
</ul>
<p>Hashes are calculated and used to prepare the solution for performance testing on a large number of paths because the Path is using the NVARCHAR(MAX) datatype to have unlimited length. NVARCHAR(MAX) can&#8217;t be used in indexes and this may lead to a significant performance bottleneck for used JOIN operators. We can create unique constraints over the hash values column to enforce certain data integrity rules like disabling duplicated node names etc. too.</p>
<p>The final step is pretty easy: We will select all leaf nodes (row 14.) and insert them into the final parent-child table. Same time we will LEFT JOIN the table (row. 21) to get the Parent_Id value:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-linenumbers="true" data-enlighter-highlight="14,21">DROP TABLE IF EXISTS [#Hierarchy]

CREATE TABLE [#Hierarchy] (
	[Id] INT NOT NULL PRIMARY KEY,
	[Node_Name] NVARCHAR(MAX) NOT NULL,
	[Parent_Id] INT NULL,
	[Level] TINYINT
)

;WITH [c] AS 
(
	SELECT [t].[Id], [t].[Node_Name], [t].[Path], [t].[Path_Hash], [t].[Parent_Path_Hash], [t].[Level]
	FROM [#PathsCache] [t]
	WHERE [t].[Level] = [t].[Depth]
)
INSERT INTO [#Hierarchy]
	(	[Id], [Node_Name], [Parent_Id], [Level] )
	SELECT 
		[c].[Id], [c].[Node_Name], [p].[Id], ISNULL([p].[Level] + 1, 0)
	FROM [c]
		LEFT JOIN [c] [p] ON [p].[Path_Hash] = [c].[Parent_Path_Hash]

SELECT * FROM [#Hierarchy]
GO</pre>
<p>And this is our brand new Parent-Child table created from a list of literal paths:</p>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5443" src="https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path3.png" alt="" width="242" height="328" srcset="https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path3.png 415w, https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path3-222x300.png 222w, https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path3-74x100.png 74w, https://sqlpowered.com/wp-content/uploads/2022/02/Build_Parent_Child_Table_From_Literal_Path3-360x488.png 360w" sizes="auto, (max-width: 242px) 100vw, 242px" /></p>
<p>There is much more to do with. I.e it&#8217;s easy to search for paths that are in the set but the set doesn&#8217;t contain the parent node itself and more. A big chapter is a performance: The whole solution is built using the traditional data processing stack and every single part of it can be rewritten or optimized and the main idea will stay alive. I will bring more on this in another post later.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sqlpowered.com/converting-literal-path-node1-node2-to-parent-child-table/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
