{"id":78417,"date":"2018-04-23T20:36:08","date_gmt":"2018-04-23T20:36:08","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=78417"},"modified":"2026-03-12T13:32:23","modified_gmt":"2026-03-12T13:32:23","slug":"sql-server-graph-databases-part-4-working-hierarchical-data-graph-database","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/sql-server-graph-databases-part-4-working-hierarchical-data-graph-database\/","title":{"rendered":"Graph Databases for Hierarchical Data in SQL Server (Part 4)"},"content":{"rendered":"<h4>The series so far:<\/h4>\n<ol>\n<li><a href=\"https:\/\/www.red-gate.com\/simple-talk\/sql\/sql-development\/sql-server-graph-databases-part-1-introduction\/\">SQL Server Graph Databases - Part 1: Introduction<\/a><\/li>\n<li><a href=\"https:\/\/www.red-gate.com\/simple-talk\/sql\/t-sql-programming\/sql-server-graph-databases-part-2-querying-data-graph-database\/\">SQL Server Graph Databases - Part 2: Querying Data in a Graph Database<\/a><\/li>\n<li><a href=\"https:\/\/www.red-gate.com\/simple-talk\/sql\/t-sql-programming\/sql-server-graph-databases-part-3-modifying-data-graph-database\/\">SQL Server Graph Databases - Part 3: Modifying Data in a Graph Database<\/a><\/li>\n<li><a href=\"https:\/\/www.red-gate.com\/simple-talk\/sql\/t-sql-programming\/sql-server-graph-databases-part-4-working-hierarchical-data-graph-database\">SQL Server Graph Databases - Part 4: Working with hierarchical data in a graph database<\/a><\/li>\n<li><a href=\"https:\/\/www.red-gate.com\/simple-talk\/sql\/t-sql-programming\/sql-server-graph-databases-part-5-importing-relational-data-graph-database\/\">SQL Server Graph Databases - Part 5:\u00a0Importing Relational Data into a Graph Database<\/a><\/li>\n<\/ol>\n\n<p>SQL Server graph databases can model hierarchical data (org charts, category trees, parts lists) using node tables for entities and edge tables for parent-child relationships. The MATCH clause navigates these hierarchies without recursive CTEs or self-joins. However, SQL Server\u2019s graph implementation has limitations for multi-hop traversals &#8211; MATCH patterns must be explicitly defined for each depth level.<\/p>\n<p>For hierarchies of unknown depth, a closure table workaround (storing all ancestor-descendant paths in a separate table) provides the flexibility that MATCH alone cannot. This article compares graph approaches to HierarchyID and closure tables for hierarchy modeling in SQL Server.<\/p>\n<h2>Introduction<\/h2>\n<p>This first three articles in this series focused on using SQL Server graph databases to work with data sets that contained relationships not easily handled in a typical relational structure, the types of relationships you might find in a social network site or product recommendation engine. One of the topics that the articles touched upon was how to work with hierarchical data and their relationships, as they applied to the <strong>FishGraph<\/strong> database used in the examples.<\/p>\n<p>This article digs deeper into the topic of hierarchies, particularly those that contain more complex relationships than what you saw in the examples. The article first provides an overview of how to create and populate the tables necessary to support a hierarchy and then focuses on how to query hierarchical data that contains multiple types of relationships.<\/p>\n<h2>Limitations of the HierarchyID Data Type<\/h2>\n<p>Working with hierarchical data is certainly not new in SQL Server. Since the product\u2019s early days, you could represent a hierarchical structure by including a foreign key in a table that pointed to the primary key in the same table. In this way, you were able to represent the parent-child relationships that existed within the data set.<\/p>\n<p>Since the release of SQL Server 2008, you\u2019ve been able to use the <strong>hierarchyid<\/strong> data type to represent a record\u2019s hierarchical position by defining values that reflect the relationships between a table\u2019s rows. For example, the data type makes it possible to represent how employees are organized within a company\u2019s hierarchical structure, such as the one shown in the following figure.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1144\" height=\"880\" class=\"wp-image-78418\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-176.png\" \/><\/p>\n<p>Although this is a very simple example, it demonstrates the types of relationships that typically make up a basic hierarchy:<\/p>\n<ul>\n<li>Dora sits at the top of the hierarchy.<\/li>\n<li>Fred and Nalini report to Dora.<\/li>\n<li>Rita, Filip, and Adil report to Fred.<\/li>\n<li>Ben, Barb, Chen, Gus, and Ane report to Nalini.<\/li>\n<li>Mao and Miguel report to Adil.<\/li>\n<li>Don and Joyce report to Chen.<\/li>\n<\/ul>\n<p>As with the above figure, a hierarchy is often represented as an inverted tree structure in which all branches lead to a common root, in this case, Dora.<\/p>\n<p>Unfortunately, not all hierarchies are this simple. For example, an employee might work part time in two positions, which means reporting to two different managers. Or an employee might report to one manager but actually work for another, that is, the employee is managed by someone different from whom the employee reports to.<\/p>\n<p>A hierarchy that reflects real-world situations will likely be a lot less straightforward that the one shown in the previous figure. Even an example as simple as this one can end up being a lot more complex after adding a few exceptions, as shown in the following figure.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1140\" height=\"876\" class=\"wp-image-78419\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-177.png\" \/><\/p>\n<p>Although many of the basic relationships are the same, the hierarchy now includes people who report to more than one manager or who report to one manager but work for another:<\/p>\n<ul>\n<li>Dora sits at the top of the hierarchy.<\/li>\n<li>Fred and Nalini report to Dora.<\/li>\n<li>Rita, Filip, and Adil report to Fred, but Rita works for Ben.<\/li>\n<li>Ben, Barb, Chen, Gus, and Ane report to Nalini, but Gus and Ane work for Ben.<\/li>\n<li>Mao and Miguel report to both Adil and Chen, working half-time in each position.<\/li>\n<li>Don and Joyce report to Chen.<\/li>\n<\/ul>\n<p>The <strong>hierarchyid<\/strong> data type is not equipped to handle anything that does not fit neatly into a basic structure. And even if the hierarchy does fit this model, too many levels can still be difficult to manipulate and maneuver. Graph databases, on the other hand, are made for more complex hierarchies, regardless of the number of levels or types of relationships, making it easier to accommodate the hierarchy, rather than trying to force the hierarchy into a relational structure.<\/p>\n<h2>Defining the Graph Tables<\/h2>\n<p>The previous articles in this series used the <strong>FishGraph<\/strong> database to demonstrate various graph concepts. This article also uses the database, but only to add tables based on the employee hierarchy shown in the above figure. The tables are unrelated to the existing <strong>FishGraph<\/strong> tables, so if you\u2019re trying out these examples, you can use any database you want. That said, including the tables in the <strong>FishGraph<\/strong> database opens up the potential for creating relationships between the new and original tables with very little effort, should you be inclined to do so.<\/p>\n<p>To implement the employee hierarchy, you need to create and populate three tables, one node table to store the list of employees and two edge tables to store the <em>reports to<\/em> and <em>works for<\/em> relationships defined between those nodes.<\/p>\n<p>The first table, <strong>FishEmployees<\/strong>, is a node table that contains the ID and first name of each employee, along with the auto-defined <strong>$node_id<\/strong> column. The following T-SQL script creates and populates that table:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">USE FishGraph;\nGO\nDROP TABLE IF EXISTS FishEmployees;\nGO\nCREATE TABLE FishEmployees (\n  EmpID INT IDENTITY PRIMARY KEY,\n  FirstName NVARCHAR(50) NOT NULL\n) AS NODE;\nINSERT INTO FishEmployees (FirstName) VALUES\n('Fred'), ('Rita'), ('Filip'), ('Adil'), ('Dora'),\n('Mao'), ('Miguel'), ('Nalini'), ('Ben'), ('Barb'),\n('Chen'), ('Gus'), ('Ane'), ('Don'), ('Joyce');<\/pre>\n<p>We won\u2019t spend too much time on how to create and populate graph tables because these concepts are covered extensively in the first article. The main point to keep in mind is that you must include the <strong>AS<\/strong> <strong>NODE<\/strong> clause when creating a node table and the <strong>AS<\/strong> <strong>EDGE<\/strong> clause when creating an edge table. The database engine will then add the necessary auto-defined columns.<\/p>\n<p>The next table is <strong>ReportsTo<\/strong>, an edge table that records each <em>reports to<\/em> relationship between employees. You can create and populate the table without including any user-defined columns, as shown in the following T-SQL script:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DROP TABLE IF EXISTS ReportsTo;\nGO\nCREATE TABLE ReportsTo AS EDGE;\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 1), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 5));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 2), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 1));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 3), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 1));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 4), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 1));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 6), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 4));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 7), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 4));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 8), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 5));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 9), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 8));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 10), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 8));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 11), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 8));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 6), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 11));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 7), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 11));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 12), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 8));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 13), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 8));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 14), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 11));\nINSERT INTO ReportsTo ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 15), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 11));<\/pre>\n<p>Each <strong>INSERT<\/strong> statement retrieves the <strong>$node_id<\/strong> values from the <strong>FishEmployees<\/strong> table in order to provide the relationship\u2019s originating and terminating nodes (the <strong>$from_id<\/strong> and <strong>$to_id<\/strong> values, respectively). You can take a similar approach for the third table, <strong>FishEmployees<\/strong>, which records each <em>works for<\/em> relationship between employees:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DROP TABLE IF EXISTS WorksFor;\nGO\nCREATE TABLE WorksFor AS EDGE;\nINSERT INTO WorksFor ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 2), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 9));\nINSERT INTO WorksFor ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 12), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 9));\nINSERT INTO WorksFor ($from_id, $to_id) VALUES (\n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 13), \n  (SELECT $node_id FROM FishEmployees WHERE EmpID = 9));<\/pre>\n<p>That\u2019s all there is to defining the employee hierarchy shown in the above figure. Although this is a simple example, it demonstrates many of the concepts associated with building a more complex hierarchy that contains numerous levels and multiple types of relationships. For a single hierarchy, you need a node table for the primary entities and an edge table for each type of relationship within that hierarchy. In this way, you can define as many types of relationships as necessary and define multiple relationships within a single type that share the same child node but different parent nodes. (If you have any questions about creating or populating graph tables, refer back to the first article in this series.)<\/p>\n<h2>Returning employee data<\/h2>\n<p>Querying graph tables that support a hierarchy is just like querying any tables in a graph database, something we covered in detail in the second article. For example, to view all the <em>reports to<\/em> relationships in the employee hierarchy, you can run the following <strong>SELECT<\/strong> statement:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT emp1.FirstName Employee, emp2.FirstName Manager\nFROM FishEmployees emp1, ReportsTo, FishEmployees emp2\nWHERE MATCH(emp1-(ReportsTo)-&gt;emp2)\nORDER BY Employee, Manager;<\/pre>\n<p>The statement uses the <strong>MATCH<\/strong> function to return a list of employees and the people they report to. Because the originating and terminating nodes reside in the same table, you must specify the table twice in the <strong>FROM<\/strong> clause, providing a different alias for each instance. You can then use those aliases in the <strong>MATCH<\/strong> clause, along with the name of the edge table, to specify the relationship <em>employee 1 reports to employee 2<\/em>. The <strong>SELECT<\/strong> statement returns the results shown in the following figure.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"498\" height=\"848\" class=\"wp-image-78420\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-178.png\" \/><\/p>\n<p>You can take a similar approach to view all the <em>works for<\/em> relationships:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT emp1.FirstName Employee, emp2.FirstName Manager\nFROM FishEmployees emp1, WorksFor, FishEmployees emp2\nWHERE MATCH(emp1-(WorksFor)-&gt;emp2)\nORDER BY Employee, Manager;<\/pre>\n<p>This time the query returns the results shown in the next figure, which indicate that three employees work for Ben.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"486\" height=\"316\" class=\"wp-image-78421\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-179.png\" \/><\/p>\n<p>So far, this is all fairly basic. The <strong>MATCH<\/strong> function makes it easy to find the relationships defined between nodes, whether those nodes are in the same table or in different tables. To return more specific information, you can modify the <strong>WHERE<\/strong> clause to include additional conditions. For example, the following <strong>SELECT<\/strong> statement returns the employee ID and name of the manager that Barb reports to:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT emp2.EmpID MgrID, emp2.FirstName Manager\nFROM FishEmployees emp1, ReportsTo, FishEmployees emp2\nWHERE MATCH(emp1-(ReportsTo)-&gt;emp2)\n  AND emp1.FirstName = 'barb';<\/pre>\n<p>When adding this type of condition to the <strong>WHERE<\/strong> clause, be sure to reference the correct node. In this case, you should use the <strong>emp1<\/strong> table alias, rather than <strong>emp2<\/strong>, because you need to reference the originating node, not the terminating one. The <strong>SELECT<\/strong> statement returns the results shown in the following figure.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"460\" height=\"248\" class=\"wp-image-78422\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-180.png\" \/><\/p>\n<p>As you can see, retrieving the name of an employee\u2019s manager is fairly straightforward. However, not all queries are this simple. For example, if you want to know which employees report to more than one manager, you must take a different approach. One solution is to create a common table expression (CTE) that retrieves the node IDs of the employees with more than one manager. You can then use the CTE as one of the conditions in the <strong>WHERE<\/strong> clause of the outer <strong>SELECT<\/strong> statement to return those relationships:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">WITH rt AS\n(\n  SELECT $from_id FromID\n  FROM ReportsTo\n  GROUP BY $from_id\n  HAVING COUNT(*) &gt; 1\n)\nSELECT emp1.FirstName Employee, emp2.FirstName Manager\nFROM FishEmployees emp1, ReportsTo, FishEmployees emp2\nWHERE MATCH(emp1-(ReportsTo)-&gt;emp2)\n  AND ReportsTo.$from_id IN (SELECT FromID FROM rt)\nORDER BY Employee, Manager;<\/pre>\n<p>The CTE retrieves the <strong>$from_id<\/strong> values from the <strong>ReportsTo<\/strong> table, using the <strong>GROUP<\/strong> <strong>BY<\/strong> and <strong>HAVING<\/strong> clauses to return only those IDs that include multiple instances. The <strong>WHERE<\/strong> clause in the outer <strong>SELECT<\/strong> statement then uses an <strong>IN<\/strong> expression to compare the <strong>$from_id<\/strong> values to those returned by the CTE, giving us the results shown in the following figure.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"564\" height=\"356\" class=\"wp-image-78423\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-181.png\" \/><\/p>\n<p>As you can see, the results show the two employees, Mao and Miguel, and their managers, Adil and Chen. Now suppose you want to view the name of employees who report to one manager, but work for another manager. In this case, you don\u2019t need to use a CTE because the <strong>MATCH<\/strong> function can give you the information you need:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT emp1.FirstName Employee, \n  emp2.FirstName ReportsTo, emp3.FirstName WorksFor\nFROM FishEmployees emp1, ReportsTo, FishEmployees emp2, \n  WorksFor, FishEmployees emp3\nWHERE MATCH(emp2&lt;-(ReportsTo)-emp1-(WorksFor)-&gt;emp3)\nORDER BY Employee;<\/pre>\n<p>As you\u2019ll recall from the second article, you can point the <strong>MATCH<\/strong> relationships in either direction. In this case, the function includes two relationships with both of them originating with the <strong>emp1<\/strong> node. The first relationship is a <em>reports to<\/em> relationship that terminates with the <strong>emp2<\/strong> node. The second relationship is a <em>works for<\/em> relationship that terminates with the <strong>emp3<\/strong> node. As a result, the <strong>SELECT<\/strong> statement will return only those rows in which an employee both reports to an individual <em>and<\/em> works for an individual, as shown in the following results.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"696\" height=\"294\" class=\"wp-image-78424\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-182.png\" \/><\/p>\n<p>As expected, only three employees fit this scenario. If you instead want to view only those employees who work for Ben, you can simplify your query even further:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT emp1.EmpID, emp1.FirstName Employee\nFROM FishEmployees emp1, WorksFor, FishEmployees emp2\nWHERE MATCH(emp1-(WorksFor)-&gt;emp2)\n  AND emp2.FirstName = 'Ben';<\/pre>\n<p>In this case, the <strong>SELECT<\/strong> statement returns the ID and name of each employee who works for Ben. To get this data, you need include only a simple <strong>MATCH<\/strong> relationship and a second <strong>WHERE<\/strong> clause condition limiting the relationships to those that terminate with Ben, giving you the following results.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"476\" height=\"286\" class=\"wp-image-78425\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-183.png\" \/><\/p>\n<p>You can do something similar to find the employees who report to Nalini:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">SELECT emp1.EmpID, emp1.FirstName Employee\nFROM FishEmployees emp1, ReportsTo, FishEmployees emp2\nWHERE MATCH(emp1-(ReportsTo)-&gt;emp2)\n  AND emp2.FirstName = 'Nalini';<\/pre>\n<p>Now the <strong>SELECT<\/strong> statement returns the results shown in the next figure.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"462\" height=\"368\" class=\"wp-image-78426\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-184.png\" \/><\/p>\n<p>The flexibility of the <strong>MATCH<\/strong> clause, along with the ability to use CTEs when necessary, allows you to return different types of data sets from a hierarchy. It might take some trial-and-error to get it right, but eventually you should be able to retrieve the results you want. In some cases, however, the <strong>MATCH<\/strong> clause\u2014with or without a CTE\u2014will not return the data you need, and you\u2019ll have to look to other strategies.<\/p>\n<h2>Bumping up against graph limitations<\/h2>\n<p>Because the graph features are so new to SQL Server, it\u2019s not surprising that they have several limitations. For example, you cannot use the <strong>MATCH<\/strong> function on derived tables, which means you cannot use the function in the recursive member of a CTE. Graph databases also don\u2019t support transitive closure\u2014the ability to search recursively through graph tables beyond the first level.<\/p>\n<p>To get around these limitations, you must use more traditional T-SQL. For example, you can retrieve a list of employees who report directly or indirectly to a specific manager by creating a recursive CTE, without using the <strong>MATCH<\/strong> function:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">WITH emp AS\n(\n  SELECT $node_id NodeID, FirstName Employee, \n    CAST(NULL AS NVARCHAR(50)) Manager\n  FROM FishEmployees\n  WHERE FirstName = 'Nalini'\n  UNION ALL\n  SELECT fe.$node_id, fe.FirstName Employee, emp.Employee Manager\n  FROM FishEmployees fe INNER JOIN ReportsTo rt\n      ON fe.$node_id = rt.$from_id \n   INNER JOIN emp\n      ON rt.$to_id = emp.NodeID\n)\nSELECT Employee, Manager FROM emp\nWHERE Manager IS NOT NULL;<\/pre>\n<p>The CTE uses the <strong>UNION<\/strong> <strong>ALL<\/strong> operator to recurse through the employees and determine who reports to Nalini. The first <strong>SELECT<\/strong> statement establishes the form of the result set, based on the Nalini record, and provides the starting point for recursing through the data. The <strong>Manager<\/strong> column acts as a placeholder for the corresponding column returned by the second <strong>SELECT<\/strong> statement, which joins the <strong>FishEmployees<\/strong> and <strong>ReportsTo<\/strong> tables to the <strong>emp<\/strong> CTE to provide the recursive element. The outer <strong>SELECT<\/strong> statement then retrieves the data from the CTE, filtering out the Nalini record, as shown in the following results.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"484\" height=\"534\" class=\"wp-image-78427\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-185.png\" \/><\/p>\n<p>As you can see, the first five employees report directly to Nalini, and the remaining four employees report directly to Chen, who is one of the five who reports to Nalini.<\/p>\n<p>You can also use a recursive CTE to return a list of employees and the levels, or tiers, in which those employees are positioned within the hierarchy:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">WITH emp AS\n(\n  SELECT $node_id NodeID, FirstName Employee, \n    CAST('N\/A' AS NVARCHAR(50)) Manager, 1 AS Tier\n  FROM FishEmployees\n  WHERE FirstName = 'Dora'\n  UNION ALL\n  SELECT fe.$node_id, fe.FirstName Employee, emp.Employee Manager, \n    (Tier + 1) AS Tier\n  FROM FishEmployees fe INNER JOIN ReportsTo rt\n      ON fe.$node_id = rt.$from_id \n    INNER JOIN emp\n      ON rt.$to_id = emp.NodeID\n)\nSELECT Employee, Tier, Manager \nFROM emp\nORDER BY Tier, Manager, Employee;<\/pre>\n<p>This time around, the first <strong>SELECT<\/strong> statement in the CTE returns a row for Dora, who sits at the top of the hierarchy. The statement also returns an additional column, <strong>Tier<\/strong>, which is assigned a value of <strong>1<\/strong> to represent the first tier. The second <strong>SELECT<\/strong> statement then adds 1 to the <strong>Tier<\/strong> value with each recursion. Most of the other T-SQL elements are the same as in the preceding example, except that now the results look much different, as shown in the following figure.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"584\" height=\"896\" class=\"wp-image-78428\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-186.png\" \/><\/p>\n<p>You can also use this statement structure to return a single tier of employees, in this case, the third tier:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">WITH emp AS\n(\n  SELECT $node_id NodeID, FirstName Employee, \n    CAST('N\/A' AS NVARCHAR(50)) Manager, 1 AS Tier\n  FROM FishEmployees\n  WHERE FirstName = 'Dora'\n  UNION ALL\n  SELECT fe.$node_id, fe.FirstName Employee, emp.Employee Manager, \n    (Tier + 1) AS Tier\n  FROM FishEmployees fe INNER JOIN ReportsTo rt\n      ON fe.$node_id = rt.$from_id \n\tINNER JOIN emp\n      ON rt.$to_id = emp.NodeID\n)\nSELECT Employee, Manager \nFROM emp\nWHERE Tier = 3\nORDER BY Tier, Manager, Employee;<\/pre>\n<p>The most important difference here, when compared to the preceding example, is that the <strong>WHERE<\/strong> clause now specifies which tier to return, giving you the results shown in the following figure.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"504\" height=\"520\" class=\"wp-image-78429\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-187.png\" \/><\/p>\n<p>As you can see, only the employees in the third tier are included in the results, along with the names of their managers, Fred and Nalini. You can, of course, come up with other ways to slice and dice the data, depending the type of information you\u2019re trying to retrieve.<\/p>\n<h2>Viewing a Hierarchy<\/h2>\n<p>There might be times when you want to get a less \u2018recursive\u2019 view of the data so you can see everyone in the management chain under a specific manager in a single view. To demonstrate how to do this, you can take your cue from Phil Factor\u2019s informative article <a href=\"https:\/\/www.red-gate.com\/simple-talk\/sql\/t-sql-programming\/sql-server-closure-tables\/\">SQL Server Closure Tables<\/a>, where he describes how to use closure tables to represent hierarchies.<\/p>\n<p>To make his approach work on the <strong>FishEmployees<\/strong> hierarchy, you should create a function that retrieves the ID and name of each employee who reports to a specific manager. The function should also assign the tier level to each employee, relative to that manager, as shown in the following example:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DROP FUNCTION IF EXISTS GetEmployees;\nGO\nCREATE FUNCTION GetEmployees (@empid int)  \nRETURNS TABLE  \nAS  \nRETURN   \n(  \nWITH emp AS\n(\n  SELECT $node_id NodeID, EmpID, FirstName Employee, 0 AS Tier\n  FROM FishEmployees\n  WHERE EmpID = @empid\n  UNION ALL\n  SELECT fe.$node_id, fe.EmpID, fe.FirstName Employee, \n      (Tier + 1) AS Tier\n  FROM FishEmployees fe INNER JOIN ReportsTo rt\n      ON fe.$node_id = rt.$from_id \n   INNER JOIN emp ON rt.$to_id = emp.NodeID\n)\nSELECT EmpID, Employee, Tier FROM emp\n);  \nGO<\/pre>\n<p>The function takes a single parameter, the manager\u2019s employee ID, and returns all the employees who report directly or indirectly to that manager. You will run the function for each employee so you have a mapping of the entire employee chain, as you\u2019ll see shortly.<\/p>\n<p>The challenge with this approach is that it does not handle employees who report to more than one manager, as is the case with the <strong>FishEmployees<\/strong> hierarchy. For this example, you can run the following two <strong>DELETE<\/strong> statements to remove the conflict, keeping in mind that ultimately you would have to include the logic necessary to handle this situation:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DELETE ReportsTo\nWHERE $from_id = (SELECT $node_id FROM FishEmployees WHERE EmpID = 6)\n  AND $to_id = (SELECT $node_id FROM FishEmployees WHERE EmpID = 11);\nDELETE ReportsTo\nWHERE $from_id = (SELECT $node_id FROM FishEmployees WHERE EmpID = 7)\n  AND $to_id = (SELECT $node_id FROM FishEmployees WHERE EmpID = 11);<\/pre>\n<p>Next, you should create a temporary table, retrieve the first employee ID from the <strong>FishEmployees<\/strong> table, and then run a <strong>WHILE<\/strong> loop that uses the function to populate the table:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk\">DROP TABLE IF EXISTS #temp;\nGO\nCREATE TABLE #temp(\n  MgrID INT,\n  MgrName VARCHAR(50),\n  EmpID INT,\n  EmpName NVARCHAR(50),\n  Tier INT);\nDECLARE  @mgrid INT = \n  (SELECT MIN(EmpID) FROM FishEmployees);\nWHILE @mgrid IS NOT NULL\nBEGIN\n  DECLARE @mgrname NVARCHAR(50) = \n    (SELECT FirstName FROM FishEmployees WHERE EmpID = @mgrid);\n  INSERT INTO #temp\n  SELECT DISTINCT @mgrid, @mgrname, EmpID, Employee, Tier \n    FROM GetEmployees(@mgrid);\n  SELECT @mgrid = MIN(EmpID) FROM FishEmployees WHERE EmpID &gt; @mgrid;\nEND;<\/pre>\n<p>The <strong>WHILE<\/strong> loop runs the <strong>GetEmployees<\/strong> function for each employee ID until there is none left, adding the returned data to the <strong>#temp<\/strong> table with each iteration.<\/p>\n<p>The <strong>#temp<\/strong> table essentially acts as the type of closer table described by Phil Factor. I\u2019ve included the employee and manager names in the results as an easy way to verify the data, but they\u2019re not necessary to the final product. One thing to note about this process is that the tier levels assigned to each employee are relative to the specific employee being called when the function runs, rather than applying universally across the entire hierarchy. This is essential to creating a proper closure table.<\/p>\n<p>Once the <strong>#temp<\/strong> table is populated with the employee data, you can run a <strong>SELECT<\/strong> statement that retrieves the management chain by using a combination of self-joins and careful sorting:<\/p>\n<pre class=\"lang:tsql theme:ssms2012-simple-talk \">SELECT STUFF((SELECT '&lt;-' + fe.FirstName\n  FROM #temp t1 INNER JOIN #temp t2 ON t2.EmpID = t1.EmpID\n    INNER JOIN FishEmployees fe ON fe.EmpID = t2.MgrID\n  WHERE t1.MgrID = t.MgrID  \n    AND t1.EmpID &lt;&gt; t1.MgrID\n    AND t1.EmpID = t.EmpID\n  ORDER BY t2.Tier DESC\n  FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') \n    AS ReportTrail\nFROM #temp t\nWHERE t.MgrID = \n  (SELECT EmpID FROM FishEmployees \n    WHERE FirstName LIKE 'Dora') \n  AND t.EmpID &lt;&gt; t.MgrID\nORDER BY ReportTrail;<\/pre>\n<p>The query creates the self-joins within a subquery, specifying several <strong>WHERE<\/strong> conditions to limit the results. The subquery uses the <strong>FOR<\/strong> <strong>XML<\/strong> output type, along with the <strong>value<\/strong> function available to the <strong>XML<\/strong> data type, to get the data in a linear format. The outer <strong>WHERE<\/strong> clause applies additional filters, along with another <strong>ORDER<\/strong> <strong>BY<\/strong> clause, providing the results shown in the following figure.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"614\" height=\"792\" class=\"wp-image-78430\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2018\/04\/word-image-188.png\" \/><\/p>\n<p>For more information about how the closure table and query work, refer to the Phil Factor article, which goes into far more detail about the different ways you can use <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/sql-server-closure-tables\/\">closure tables with hierarchical data<\/a>.<\/p>\n<p><strong>Read also: <\/strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/t-sql-window-functions-and-performance\/\">Window functions for hierarchy level calculations.<\/a><\/p>\n<h2>Working with hierarchical relationships<\/h2>\n<p>SQL Server graph databases have a number of limitations, especially when it comes to the types of advanced querying available to more mature graph technologies. As a result, you might find yourself having to revert to the sort of workarounds demonstrated in the last section. However, because the SQL Server graph database features are based on traditional relational tables, you can usually figure out some way to get the data you need, even if that data lies within a complex hierarchical structure. You might have to work a bit to return the desired results, but eventually you should be able to get at the correct information.<\/p>\n\n\n<section id=\"my-first-block-block_0bf28c9a785c167a0a13d182e8523b5c\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Fast, reliable and consistent SQL Server development&#8230;<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            &#8230;with SQL Toolbelt Essentials. 10 ingeniously simple tools for accelerating development, reducing risk, and standardizing workflows.                                    <\/div>\n            <\/div>\n                            <a href=\"https:\/\/www.red-gate.com\/products\/sql-toolbelt-essentials\/\" class=\"btn btn--secondary btn--lg\">Learn more &amp; try for free<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: Hierarchical data in SQL Server graph databases<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. How do you model a hierarchy in a SQL Server graph database?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Create a node table for entities (CREATE TABLE Employee (\u2026) AS NODE) and an edge table for relationships (CREATE TABLE ReportsTo ($from_id, $to_id) AS EDGE). Insert nodes, then insert edges connecting child to parent. Query with MATCH: SELECT Manager.Name, Employee.Name FROM Employee AS Manager, ReportsTo, Employee WHERE MATCH(Manager&lt;-(ReportsTo)-Employee) returns all direct reports.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. What are the limitations of graph databases for hierarchical data in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>The main limitation is that MATCH patterns must specify each hop explicitly. To find all reports 3 levels deep, you need three MATCH patterns chained together. For hierarchies of unknown depth, MATCH alone is insufficient. Workarounds include using closure tables alongside graph tables, or using SHORTEST_PATH (SQL Server 2019+) which can traverse variable-length paths.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. Should you use graph databases, closure tables, or HierarchyID for hierarchies?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Graph databases excel when relationships are complex (multiple types, many-to-many). HierarchyID is efficient for single, stable parent-child trees. Closure tables provide the most flexibility: variable-depth queries, temporal tracking, and standard SQL. For most SQL Server hierarchies, closure tables are the safest choice; use graph databases when you also need to model non-hierarchical relationships.<\/p>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>Model hierarchical data in SQL Server graph databases: org charts, multi-level relationships, MATCH queries, and closure table workarounds for complex hierarchy traversals.&hellip;<\/p>\n","protected":false},"author":221841,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143531],"tags":[5134],"coauthors":[6779],"class_list":["post-78417","post","type-post","status-publish","format-standard","hentry","category-t-sql-programming-sql-server","tag-sql-prompt"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/78417","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/221841"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=78417"}],"version-history":[{"count":11,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/78417\/revisions"}],"predecessor-version":[{"id":109101,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/78417\/revisions\/109101"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=78417"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=78417"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=78417"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=78417"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}