<?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>T-SQL &#8211; SQLpowered.com</title>
	<atom:link href="https://sqlpowered.com/category/t-sql/feed/" rel="self" type="application/rss+xml" />
	<link>https://sqlpowered.com</link>
	<description>SQL Server + BI</description>
	<lastBuildDate>Tue, 17 Feb 2026 22:59:05 +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>T-SQL &#8211; SQLpowered.com</title>
	<link>https://sqlpowered.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>JSON Data Type Is Not Comparable!</title>
		<link>https://sqlpowered.com/json-data-type-is-not-comparable/</link>
					<comments>https://sqlpowered.com/json-data-type-is-not-comparable/#respond</comments>
		
		<dc:creator><![CDATA[Jan Dvořák]]></dc:creator>
		<pubDate>Tue, 17 Feb 2026 22:47:49 +0000</pubDate>
				<category><![CDATA[T-SQL]]></category>
		<guid isPermaLink="false">https://sqlpowered.com/?p=5802</guid>

					<description><![CDATA[I was really not happy when I hit this with the new JSON data type. In SQL Server, set operators like UNION, INTERSECT, and EXCEPT have been a very reliable part of many generic scripts. We already survived older non-comparable types like text and image, and things became much cleaner...]]></description>
										<content:encoded><![CDATA[<p>I was really not happy when I hit this with the new <code>JSON</code> data type. In SQL Server, set operators like <code>UNION</code>, <code>INTERSECT</code>, and <code>EXCEPT</code> have been a very reliable part of many generic scripts. We already survived older non-comparable types like <code>text</code> and <code>image</code>, and things became much cleaner after moving away from them. But now <code>JSON</code> brings a very similar problem back. Even if both sides have the same column type, SQL Server still refuses to compare it in set operators.</p>
<p>Quick reminder: these operators need values that SQL Server can compare to detect duplicates and matches. If a data type is non-comparable, generic set-based scripts break immediately unless you cast. Most common troublemakers are legacy LOB types (<code>text</code>, <code>ntext</code>, <code>image</code>), and also <code>xml</code> in many compare/sort scenarios. And the new <code>json</code> since SQL Server 2025. So this is not only about one edge case, it is a design concern for reusable SQL tooling.</p>
<p>This is the simple repro:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">DROP TABLE IF EXISTS [dbo].[TableA]
DROP TABLE IF EXISTS [dbo].[TableB]

CREATE TABLE [dbo].[TableA] (
    [JSON_Data] JSON
)

CREATE TABLE [dbo].[TableB] (
    [JSON_Data] JSON
)

SELECT * FROM [dbo].[TableA]
UNION
SELECT * FROM [dbo].[TableB]
GO
</pre>
<p>It fails with this SQL Server error:</p>
<pre style="color: #c00000;">Msg 5335, Level 16, State 1, Line 14
The data type json cannot be used as an operand to the UNION, INTERSECT or EXCEPT operators because it is not comparable.
</pre>
<p>That means generic scripts now need special handling whenever any source column is <code>JSON</code>.</p>
<p>Current practical fix is explicit cast:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SELECT CAST([JSON_Data] AS NVARCHAR(MAX)) FROM [dbo].[TableA]
UNION
SELECT CAST([JSON_Data] AS NVARCHAR(MAX)) FROM [dbo].[TableB]
GO
</pre>
<p>This works, but it also means more branching and more complexity in reusable SQL utilities. In practice, any metadata-driven script should detect non-comparable source types and auto-cast them to a comparable string/binary representation before applying set operators.</p>
<p><strong>Note</strong>: Microsoft already added one <code>json</code> data type column in a new system view <code>sys.external_models</code> in SQL Server 2025. Check this query:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SELECT [o].[name], [c].[name], [tp].[name]
FROM [sys].[system_objects] [o]
	INNER JOIN [sys].[system_columns] [c] ON [c].[object_id] = [o].[object_id]
	INNER JOIN [sys]. [tp] ON [tp].[system_type_id] = [c].[system_type_id] AND [tp].[user_type_id] = [c].[user_type_id]
WHERE [tp].[name] = 'json'
</pre>
<p><img fetchpriority="high" decoding="async" class="alignnone  wp-image-5804" src="https://sqlpowered.com/wp-content/uploads/2026/02/sys_external_models_json.png" alt="" width="355" height="309" srcset="https://sqlpowered.com/wp-content/uploads/2026/02/sys_external_models_json.png 553w, https://sqlpowered.com/wp-content/uploads/2026/02/sys_external_models_json-300x261.png 300w, https://sqlpowered.com/wp-content/uploads/2026/02/sys_external_models_json-115x100.png 115w, https://sqlpowered.com/wp-content/uploads/2026/02/sys_external_models_json-360x313.png 360w" sizes="(max-width: 355px) 100vw, 355px" /></p>
]]></content:encoded>
					
					<wfw:commentRss>https://sqlpowered.com/json-data-type-is-not-comparable/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SET vs SELECT Variable Assignment When No Rows Match</title>
		<link>https://sqlpowered.com/set-vs-select-variable-assignment-when-no-rows-match/</link>
					<comments>https://sqlpowered.com/set-vs-select-variable-assignment-when-no-rows-match/#respond</comments>
		
		<dc:creator><![CDATA[Jan Dvořák]]></dc:creator>
		<pubDate>Sun, 15 Feb 2026 21:15:34 +0000</pubDate>
				<category><![CDATA[T-SQL]]></category>
		<guid isPermaLink="false">https://sqlpowered.com/?p=5476</guid>

					<description><![CDATA[This one can be easy to miss, but it can change your logic in a silent way. SET and SELECT look similar when assigning a variable, but they do not behave the same in all cases. The difference appears when the subquery returns no rows. With SELECT @var = ...the...]]></description>
										<content:encoded><![CDATA[<p>This one can be easy to miss, but it can change your logic in a silent way. <code>SET</code> and <code>SELECT</code> look similar when assigning a variable, but they do not behave the same in all cases. The difference appears when the subquery returns no rows. With <code>SELECT @var = ...</code>the variable keeps its previous value. With <code>SET @var = (SELECT ...)</code>the variable becomes <code>NULL</code>.</p>
<p>I tested it with a very small demo below. The query searching for <code>Id = 4</code> returns no row, because the table has only <code>1,2,3</code>. After that, <code>SELECT</code> leaves <code>@Id</code> as <code>10</code>, while <code>SET</code> changes it to <code>NULL</code>.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">DROP TABLE IF EXISTS #tmp
CREATE TABLE #tmp
(
    Id INT NOT NULL PRIMARY KEY
)

INSERT INTO #tmp (Id)
SELECT *
FROM (VALUES (1), (2), (3)) a(n)

DECLARE @Id INT

SET @Id = 10

-- SELECT
SELECT @Id = Id FROM #tmp WHERE Id = 4

SELECT @Id AS [SELECT]

-- SET
SET @Id = (SELECT Id FROM #tmp WHERE Id = 4)

SELECT @Id AS [SET]
</pre>
<p><img decoding="async" class="alignnone wp-image-5522" src="https://sqlpowered.com/wp-content/uploads/2023/02/SET_vs_SELECT_When_Assignin_Variable.png" alt="" width="96" height="88" srcset="https://sqlpowered.com/wp-content/uploads/2023/02/SET_vs_SELECT_When_Assignin_Variable.png 177w, https://sqlpowered.com/wp-content/uploads/2023/02/SET_vs_SELECT_When_Assignin_Variable-109x100.png 109w" sizes="(max-width: 96px) 100vw, 96px" /></p>
<p>Result in this test: <code>SELECT</code> gives <code>10</code>, <code>SET</code> gives <code>NULL</code>. So if &#8220;not found&#8221; should keep the old value, <code>SELECT</code> assignment can be useful. If &#8220;not found&#8221; should explicitly reset the variable, <code>SET</code> it gives you that behavior directly.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sqlpowered.com/set-vs-select-variable-assignment-when-no-rows-match/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server Allows Empty Stored Procedures</title>
		<link>https://sqlpowered.com/sql-server-allows-empty-stored-procedures/</link>
					<comments>https://sqlpowered.com/sql-server-allows-empty-stored-procedures/#respond</comments>
		
		<dc:creator><![CDATA[Jan Dvořák]]></dc:creator>
		<pubDate>Sun, 15 Feb 2026 21:06:52 +0000</pubDate>
				<category><![CDATA[T-SQL]]></category>
		<guid isPermaLink="false">https://sqlpowered.com/?p=5725</guid>

					<description><![CDATA[I found one small SQL Server behavior that surprised me more than I expected. You can create a stored procedure with an empty body, and SQL Server will accept it. No SELECT, no INSERT, no PRINT, nothing inside. Even more surprisingly, the procedure can be executed without any error. I...]]></description>
										<content:encoded><![CDATA[<p>I found one small SQL Server behavior that surprised me more than I expected. You can create a stored procedure with an empty body, and SQL Server will accept it. No SELECT, no INSERT, no PRINT, nothing inside. Even more surprisingly, the procedure can be executed without any error. I always assumed at least one statement would be required, but apparently not.</p>
<p>This is not something you will usually use in real production logic, but it is a useful edge case to know. It helps when you test how SQL Server validates object definitions and execution flow.</p>
<p>Simple demo:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE PROCEDURE MyEmptyProcedure AS
GO

EXEC MyEmptyProcedure
GO</pre>
<p>So yes, it really works. The procedure body can be empty, and SQL Server still allows create + execute.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sqlpowered.com/sql-server-allows-empty-stored-procedures/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SUBSTRING() in SQL Server 2022 vs 2025: Optional LENGTH Is Finally Here</title>
		<link>https://sqlpowered.com/substring-in-sql-server-2022-vs-2025-optional-length-is-finally-here/</link>
					<comments>https://sqlpowered.com/substring-in-sql-server-2022-vs-2025-optional-length-is-finally-here/#respond</comments>
		
		<dc:creator><![CDATA[Jan Dvořák]]></dc:creator>
		<pubDate>Fri, 16 Jan 2026 14:02:48 +0000</pubDate>
				<category><![CDATA[T-SQL]]></category>
		<guid isPermaLink="false">https://sqlpowered.com/?p=5776</guid>

					<description><![CDATA[SUBSTRING() is one of those T-SQL functions we all use constantly: parsing identifiers, trimming prefixes, extracting parts of file paths, URLs, error messages, or just cleaning up data for reporting. Until now, SQL Server forced us to always provide the third argument (length). With SQL Server 2025, Microsoft added a...]]></description>
										<content:encoded><![CDATA[<p><code>SUBSTRING()</code> is one of those T-SQL functions we all use constantly: parsing identifiers, trimming prefixes, extracting parts of file paths, URLs, error messages, or just cleaning up data for reporting. Until now, SQL Server forced us to always provide the third argument (<code>length</code>).</p>
<p>With <strong>SQL Server 2025</strong>, Microsoft added a small but very practical improvement: <strong>the <code>length</code> parameter is now optional</strong>. If you omit it, SQL Server returns the substring from the start position to the end of the string.</p>
<p>See official doc: <a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/substring-transact-sql?view=sql-server-ver17#e-use-substring-with-optional-length-argument" target="_blank" rel="noopener">SUBSTRING (Transact-SQL) – optional length argument</a>.</p>
<h2>Syntax comparison</h2>
<h3>SQL Server 2022 (and earlier)</h3>
<p>Length is required:</p>
<p><code class="EnlighterJSRAW" data-enlighter-language="sql">SUBSTRING(expression, start, length)</code></p>
<h3>SQL Server 2025</h3>
<p>Length can be omitted:</p>
<p><code class="EnlighterJSRAW" data-enlighter-language="sql">SUBSTRING(expression, start[, length])</code></p>
<h2>Practical T-SQL: 2022 vs 2025</h2>
<h3>Example 1: Extract everything after a prefix</h3>
<p>Let’s say you have a standard prefix and you want everything after it.</p>
<h4>SQL Server 2022</h4>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">DECLARE @s varchar(100) = 'ENV:prod-west-eu'; 

-- Get everything after 'ENV:'
SELECT SUBSTRING(@s, 5, LEN(@s) - 4) AS tail_2022;</pre>
<h4>SQL Server 2025</h4>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">DECLARE @s varchar(100) = 'ENV:prod-west-eu'; 

-- Get everything after 'ENV:' (length omitted) 
SELECT SUBSTRING(@s, 5) AS tail_2025;</pre>
<h3>Example 2: Split at a delimiter (common pattern)</h3>
<p>Extract everything after the first colon (:).</p>
<h4>SQL Server 2022</h4>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">DECLARE @s varchar(100) = 'ServerName:SQL2022-01'; 

SELECT SUBSTRING(@s, CHARINDEX(':', @s) + 1, LEN(@s)) AS after_colon_2022;</pre>
<h4>SQL Server 2025</h4>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">DECLARE @s varchar(100) = 'ServerName:SQL2025-01'; 

SELECT SUBSTRING(@s, CHARINDEX(':', @s) + 1) AS after_colon_2025;</pre>
<h2>One important detail: omitted vs NULL length</h2>
<p>The new behavior is triggered only when the third parameter is <strong>omitted</strong>. If you explicitly pass <code>NULL</code> as length, the result is still <code>NULL</code> (same as older versions).</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Same behavior (2022 and 2025): returns NULL because length is explicitly NULL
SELECT SUBSTRING('abc123', 4, NULL) AS returns_null;</pre>
<h2>Conclusion</h2>
<p>This change is small, but in daily T-SQL work it’s one of those “finally!” improvements. Anywhere you currently compute remaining length or use overshoot constants, SQL Server 2025 lets you write cleaner and more intention-revealing code.</p>
<p>If you’re maintaining scripts that must run on both SQL Server 2022 and 2025, <strong>keep the old 3-argument form for compatibility</strong>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sqlpowered.com/substring-in-sql-server-2022-vs-2025-optional-length-is-finally-here/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Managing String Content in View Definitions</title>
		<link>https://sqlpowered.com/store-any-kind-of-a-string-in-your-view-definition/</link>
					<comments>https://sqlpowered.com/store-any-kind-of-a-string-in-your-view-definition/#respond</comments>
		
		<dc:creator><![CDATA[Jan Dvořák]]></dc:creator>
		<pubDate>Wed, 26 Jun 2024 06:01:43 +0000</pubDate>
				<category><![CDATA[T-SQL]]></category>
		<guid isPermaLink="false">https://sqlpowered.com/?p=5510</guid>

					<description><![CDATA[The hidden text (this_is_hidden_text_in_the_view) in the view definition is an intriguing addition. Anything after the SELECT * FROM sys.tables query that is not a valid SQL statement (like a comment or plain text) will not be executed or parsed by SQL Server. It essentially becomes invisible to the engine and...]]></description>
										<content:encoded><![CDATA[<p>The hidden text (<code>this_is_hidden_text_in_the_view</code>) in the view definition is an intriguing addition. Anything after the <code>SELECT * FROM sys.tables</code> query that is not a valid SQL statement (like a comment or plain text) will not be executed or parsed by SQL Server. It essentially becomes invisible to the engine and is purely for informational or decorative purposes within the script.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-highlight="5">CREATE OR ALTER VIEW dbo.SampleView
AS
	SELECT	* FROM sys.tables

	this_is_hidden_text_in_the_view

GO</pre>
<p>In the medata you will see:</p>
<p><code class="EnlighterJSRAW" data-enlighter-language="sql">SELECT * FROM sys.[sql_modules] WHERE [definition] LIKE '%hidden_tex%'</code></p>
<p><img decoding="async" class="alignnone wp-image-5711" src="https://sqlpowered.com/wp-content/uploads/2024/06/text-hidden-in-view.png" alt="" width="592" height="28" srcset="https://sqlpowered.com/wp-content/uploads/2024/06/text-hidden-in-view.png 1596w, https://sqlpowered.com/wp-content/uploads/2024/06/text-hidden-in-view-300x14.png 300w, https://sqlpowered.com/wp-content/uploads/2024/06/text-hidden-in-view-1024x48.png 1024w, https://sqlpowered.com/wp-content/uploads/2024/06/text-hidden-in-view-150x7.png 150w, https://sqlpowered.com/wp-content/uploads/2024/06/text-hidden-in-view-768x36.png 768w, https://sqlpowered.com/wp-content/uploads/2024/06/text-hidden-in-view-1536x72.png 1536w, https://sqlpowered.com/wp-content/uploads/2024/06/text-hidden-in-view-360x17.png 360w" sizes="(max-width: 592px) 100vw, 592px" /></p>
<p>Storing free text that isn&#8217;t a valid T-SQL statement within a view definition can indeed break code consistency rules and potentially lead to confusions in database management and development. Here are several key points to explain this concern:</p>
<ol>
<li><strong>Code Consistency</strong>: Views in SQL Server are designed to encapsulate logical queries that retrieve and present data in a structured manner. They should adhere to standard SQL syntax and maintain a clear purpose. Introducing free text that isn&#8217;t part of a valid SQL statement contradicts this principle of consistency. It blurs the line between executable code and documentation, potentially making it harder for developers to understand and maintain the view over time.</li>
<li><strong>Parsing and Execution</strong>: SQL Server parses and executes the view definition to retrieve data when the view is queried. Any non-SQL text included in the view definition, such as comments or free text, is ignored during this process. While this doesn&#8217;t affect the functionality of the view directly, it can mislead developers who might expect the free text to behave as part of the SQL logic.</li>
<li><strong>Maintenance Challenges</strong>: Over time, as views are updated or modified, the non-SQL text within their definitions may become outdated or irrelevant. This can confuse developers who rely on accurate documentation embedded within the code. It also creates maintenance challenges, as developers might overlook or misinterpret the purpose of the non-SQL text during updates.</li>
<li><strong>Communication and Collaboration</strong>: In collaborative environments, adhering to consistent coding practices ensures that all team members can easily understand and work with database objects. Non-standard practices like embedding free text in view definitions can hinder effective communication about the view&#8217;s purpose, leading to misunderstandings or errors in development and maintenance tasks.</li>
<li><strong>Documentation Separation</strong>: Best practices advocate for separating documentation from executable code. While comments within SQL code (using <code>/* */</code> or <code>--</code>) are valid and beneficial for documenting SQL logic, embedding free text directly within view definitions blurs this distinction. It introduces content that isn&#8217;t intended for execution but might be mistakenly interpreted as such.</li>
<li><strong>Risk of Misinterpretation</strong>: Developers unfamiliar with the database might misinterpret non-SQL text within view definitions as part of the SQL logic. This misunderstanding can lead to incorrect assumptions about the functionality or design intent of the view, potentially causing errors or inefficiencies in application development.</li>
</ol>
<h3>Conclusion</h3>
<p>In conclusion, while adding non-SQL free text to view definitions might seem convenient for documentation purposes, it poses risks to code consistency and clarity in SQL Server development. It&#8217;s advisable to adhere to established coding standards and separate documentation from executable code to maintain transparency, facilitate maintenance, and mitigate confusion among developers working with database objects. Clear communication and consistent practices are key to ensuring the reliability and manageability of database systems over time.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sqlpowered.com/store-any-kind-of-a-string-in-your-view-definition/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Numbered Stored Procedures</title>
		<link>https://sqlpowered.com/numbered-stored-procedures/</link>
					<comments>https://sqlpowered.com/numbered-stored-procedures/#respond</comments>
		
		<dc:creator><![CDATA[Jan Dvořák]]></dc:creator>
		<pubDate>Wed, 12 Jun 2024 11:16:05 +0000</pubDate>
				<category><![CDATA[T-SQL]]></category>
		<category><![CDATA[procedures]]></category>
		<guid isPermaLink="false">https://sqlpowered.com/?p=5682</guid>

					<description><![CDATA[Numbered stored procedures are a lesser-known feature that allows multiple procedures to share the same name but are distinguished by a numeric suffix. This feature can be useful in certain scenarios, although it is not commonly used in modern database development. Numbered stored procedures allow you to create a series...]]></description>
										<content:encoded><![CDATA[<p>Numbered stored procedures are a lesser-known feature that allows multiple procedures to share the same name but are distinguished by a numeric suffix. This feature can be useful in certain scenarios, although it is not commonly used in modern database development.</p>
<p>Numbered stored procedures allow you to create a series of procedures that share the same base name, but each is distinguished by a unique integer. For example, you can have procedures named <code>MyProc;1</code>, <code>MyProc;2</code>, and so on. This can be useful for organizing related procedures or implementing certain types of versioning.</p>
<h2>Syntax and Usage</h2>
<p>The basic syntax for creating numbered stored procedures is:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE PROCEDURE procedure_name;number
AS
BEGIN
    -- SQL statements
END
</pre>
<p>To execute a numbered stored procedure, you use the following syntax:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">EXEC procedure_name;number
</pre>
<h2>Example</h2>
<p>Let&#8217;s create a series of numbered stored procedures that demonstrate a simple use case.</p>
<h3>Create the first procedure in the series:</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE PROCEDURE MyProc;1
AS
BEGIN
    SELECT 'This is procedure 1' AS Message;
END
</pre>
<h3>Create the second procedure in the series:</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE PROCEDURE MyProc;2
AS
BEGIN
    SELECT 'This is procedure 2' AS Message;
END
</pre>
<h3>Create the third procedure in the series:</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE PROCEDURE MyProc;3
AS
BEGIN
    SELECT 'This is procedure 3' AS Message;
END
</pre>
<h2>Executing Numbered Stored Procedures</h2>
<p>To execute these procedures, use the following commands:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">EXEC MyProc;1
EXEC MyProc;2
EXEC MyProc;3
</pre>
<p>Each execution will return the message defined in the respective procedure.</p>
<h2>Managing Numbered Stored Procedures</h2>
<p>To drop a specific numbered stored procedure, use the following syntax:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">DROP PROCEDURE procedure_name;number
</pre>
<p>For example, to drop <code>MyProc;2</code>, you would use:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">DROP PROCEDURE MyProc;2
</pre>
<h2>Advantages and Disadvantages</h2>
<h3>Advantages:</h3>
<ul>
<li><strong>Organization:</strong> Numbered stored procedures can help organize related procedures under a common base name.</li>
<li><strong>Versioning:</strong> They can be used to manage different versions of a procedure without renaming the base procedure.</li>
</ul>
<h3>Disadvantages:</h3>
<ul>
<li><strong>Complexity:</strong> Numbered stored procedures can add complexity to database management and maintenance.</li>
<li><strong>Obsolescence:</strong> This feature is not widely used or supported in modern SQL Server practices, making it less familiar to many developers and DBAs.</li>
</ul>
<h2>Conclusion</h2>
<p>While numbered stored procedures offer a way to organize and manage related SQL procedures, they are not commonly used in contemporary SQL Server development. Understanding this feature can be useful for legacy systems or specific scenarios where such organization is beneficial. However, for most purposes, it is recommended to use more modern techniques for managing stored procedures, such as naming conventions and version control systems.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sqlpowered.com/numbered-stored-procedures/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>IDENTITY Cheat Sheet</title>
		<link>https://sqlpowered.com/identity-cheat-sheet/</link>
					<comments>https://sqlpowered.com/identity-cheat-sheet/#respond</comments>
		
		<dc:creator><![CDATA[Jan Dvořák]]></dc:creator>
		<pubDate>Mon, 13 May 2024 21:59:54 +0000</pubDate>
				<category><![CDATA[T-SQL]]></category>
		<guid isPermaLink="false">https://sqlpowered.com/?p=5678</guid>

					<description><![CDATA[Today we will look at a brief overview of all important functions, global variables, and DBCC commands for working with IDENTITY values in SQL Server. Programmers often make mistakes regarding when to use each of them, particularly due to not understanding how the individual functions behave within the context in...]]></description>
										<content:encoded><![CDATA[<p>Today we will look at a brief overview of all important functions, global variables, and DBCC commands for working with IDENTITY values in SQL Server. Programmers often make mistakes regarding when to use each of them, particularly due to not understanding how the individual functions behave within the context in which they are called (stored procedure bodies, triggers, and their interrelationships). Hopefully, the following summary will clarify.</p>
<p><span id="more-5678"></span></p>
<p><strong>@@IDENTITY</strong></p>
<ul>
<li>returns data type <strong>numeric (38,0)</strong></li>
<li>after completing INSERT, SELECT INTO, or BULKCOPY, returns the last identity column value in the table</li>
<li>if there is no identity column in the table, returns NULL</li>
<li>if multiple rows are inserted and multiple identity values are generated, the last value is returned</li>
<li>if a trigger is fired that increases the identity value, @@IDENTITY returns this trigger-increased value</li>
<li>@@IDENTITY does not revert to the previous value if a rollback or insert failure occurs – for example, if a failure occurs on IGNORE_DUP_KEY, the identity for the given table is still incremented</li>
<li>@@IDENTITY, SCOPE_IDENTITY, and IDENT_CURRENT are similar functions, all returning the last inserted identity column value in the table</li>
<li>@@IDENTITY and SCOPE_IDENTITY return the last generated identity value in any table of the current session. SCOPE_IDENTITY returns the value only within the scope, @@IDENTITY regardless of the scope.</li>
<li>IDENT_CURRENT is not limited by session or scope but only by the specific table, for which it independently returns the last identity</li>
<li>@@IDENTITY applies only to the current session on the local server. It cannot be called for a linked or remote server =&gt; it is necessary to create a stored procedure on the remote server, use @@IDENTITY in it, and call it in the context of the remote server</li>
<li>it is not suitable for use in replications – replication triggers can affect the identity value because the identity of the last row of the table is not returned, but the identity of the row inserted by the trigger into replication tables =&gt; existing procedures must be rewritten to SCOPE_IDENTITY()</li>
</ul>
<p><strong>SCOPE_IDENTITY()</strong></p>
<ul>
<li>returns the last identity value within the scope =&gt; scope is defined as a module: stored procedure, trigger, function, or batch</li>
<li>returns numeric</li>
<li>difference between @@IDENTITY and SCOPE_IDENTITY: (BOL example)
<ul>
<li>For example, there are two tables, <strong>T1</strong> and <strong>T2</strong>, and an INSERT trigger is defined on <strong>T1</strong>. When a row is inserted to <strong>T1</strong>, the trigger fires and inserts a row in <strong>T2</strong>. This scenario illustrates two scopes: the insert on <strong>T1</strong>, and the insert on <strong>T2</strong> by the trigger.</li>
<li>Assuming that both <strong>T1</strong> and <strong>T2</strong> have identity columns, @@IDENTITY and SCOPE_IDENTITY will return different values at the end of an INSERT statement on <strong>T1</strong>. @@IDENTITY will return the last identity column value inserted across any scope in the current session. This is the value inserted in <strong>T2</strong>. SCOPE_IDENTITY() will return the IDENTITY value inserted in <strong>T1</strong>. This was the last insert that occurred in the same scope. The SCOPE_IDENTITY() function will return the null value if the function is invoked before any INSERT statements into an identity column occur in the scope.</li>
</ul>
</li>
</ul>
<p><strong>IDENT_CURRENT()</strong></p>
<ul>
<li>returns the identity of the last operation for a specific table regardless of session and scope</li>
<li>usage: <strong>IDENT_CURRENT( ‘table_name’ )</strong></li>
<li>returns numeric(38,0)</li>
<li>if the caller does not have permissions on the table, returns NULL or an error</li>
<li>if the table does not exist, returns NULL</li>
<li>if the table is empty, returns the first identity seed</li>
<li>beware of the difference when deleting using DELETE (does not reset identity) and TRUNCATE (resets identity)</li>
<li>rolling back a transaction or an error never returns the identity value =&gt; the number remains incremented as if the transaction was committed</li>
</ul>
<p><strong>IDENT_INCR()</strong></p>
<ul>
<li>returns the increment size (e.g., 1)</li>
<li>usage: <strong>IDENT_INCR ( ‘table_or_view’ )</strong></li>
</ul>
<p><strong>IDENT_SEED()</strong></p>
<ul>
<li>returns the initial value from which the increment starts</li>
<li>usage: <strong>IDENT_SEED ( ‘table_or_view’ )</strong></li>
</ul>
<p><strong>DBCC CHECKIDENT</strong></p>
<ul>
<li>returns the initial value from which the increment starts</li>
<li>usage:
<ul>
<li><strong>DBCC CHECKIDENT ( table_name [ , { NORESEED | { RESEED [ , new_reseed_value ] } } ] )<br />
[ WITH NO_INFOMSGS ]</strong></li>
</ul>
</li>
<li>checks the current identity value in a specific table and possibly changes it – allows setting a new next identity value for the table</li>
<li>the table must contain an identity column</li>
<li>this cannot change the initial seed value when creating the column or reseeding existing data =&gt; for this, the identity column must be recreated</li>
<li>example: <strong>DBCC CHECKIDENT (“HumanResources.Employee”, RESEED, 30);</strong></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://sqlpowered.com/identity-cheat-sheet/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Generating Empty GUIDs</title>
		<link>https://sqlpowered.com/generating-empty-guids/</link>
					<comments>https://sqlpowered.com/generating-empty-guids/#respond</comments>
		
		<dc:creator><![CDATA[Jan Dvořák]]></dc:creator>
		<pubDate>Fri, 12 Apr 2024 20:17:06 +0000</pubDate>
				<category><![CDATA[T-SQL]]></category>
		<guid isPermaLink="false">https://sqlpowered.com/?p=5675</guid>

					<description><![CDATA[In SQL Server, a Globally Unique Identifier (GUID) is a 16-byte binary data type that is commonly used to uniquely identify records in a database. The UNIQUEIDENTIFIER data type in SQL Server is equivalent to a GUID, and there are occasions where you might need to generate an empty GUID,...]]></description>
										<content:encoded><![CDATA[<p>In SQL Server, a Globally Unique Identifier (GUID) is a 16-byte binary data type that is commonly used to uniquely identify records in a database. The UNIQUEIDENTIFIER data type in SQL Server is equivalent to a GUID, and there are occasions where you might need to generate an empty GUID, which is a GUID with all bits set to zero. This can be particularly useful for initializing variables or columns, or when you need a default GUID value. SQL Server provides a couple of straightforward methods to generate an empty GUID, also known as a null GUID, which is represented as <code>00000000-0000-0000-0000-000000000000</code>.</p>
<h3>Method 1: Using BINARY Casting</h3>
<p>One way to generate an empty GUID in SQL Server is by first casting the integer <code>0</code> to a binary value, and then casting that binary value to a UNIQUEIDENTIFIER. Here’s how it&#8217;s done:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SELECT CAST(CAST(0 AS BINARY) AS UNIQUEIDENTIFIER) AS EmptyGUID;</pre>
<p>In this method, the integer <code>0</code> is first converted into a binary representation. Since the binary representation does not fill the entire 16 bytes required by a GUID, SQL Server automatically pads the remaining bytes with zeros.The final cast to UNIQUEIDENTIFIER results in an empty GUID.</p>
<h3>Method 2: Using Hexadecimal Representation</h3>
<p>Another approach to generate an empty GUID is by directly casting a hexadecimal value that represents zero to a UNIQUEIDENTIFIER. This method is more concise and looks like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SELECT CAST(0x0 AS UNIQUEIDENTIFIER) AS EmptyGUID;</pre>
<p>Here, <code>0x0</code> represents a hexadecimal constant for zero. When this is cast to a UNIQUEIDENTIFIER, SQL Server interprets this as an instruction to generate a GUID consisting entirely of zeros, resulting in an empty GUID.</p>
<h3>Method 3: Using the <code>NEWID()</code> Function with Replacement</h3>
<p>Although not as straightforward or efficient for simply generating an empty GUID, you could technically use the <code>NEWID()</code> function to generate a new GUID and then replace it with an empty GUID using string manipulation functions. This method is more of a curiosity and is not recommended for practical use due to its unnecessary complexity and performance implications:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SELECT REPLACE(CAST(NEWID() AS VARCHAR(36)), CAST(NEWID() AS VARCHAR(36)), '00000000-0000-0000-0000-000000000000')
</pre>
<p>This essentially generates a new GUID and then replaces it with the string representation of an empty GUID. Again, this is not a practical approach but is mentioned here for completeness.</p>
<h3>Method 4: Direct Assignment in Variables or Table Defaults</h3>
<p>Another straightforward method, especially when working with variables or setting default values for table columns, is to directly assign the empty GUID value:</p>
<p><strong>Variables:</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">DECLARE @EmptyGUID UNIQUEIDENTIFIER = '00000000-0000-0000-0000-000000000000'</pre>
<p><strong>Table Defaults:</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE TABLE MyTable (
    ID UNIQUEIDENTIFIER DEFAULT '00000000-0000-0000-0000-000000000000' NOT NULL,
    Name NVARCHAR(50)
);</pre>
<h3>Conclusion</h3>
<p>While the direct casting from a zero binary or hexadecimal value represents the most straightforward and efficient method to generate an empty GUID in SQL Server, understanding the various ways to accomplish this task provides flexibility for SQL developers. The method of choice might depend on the specific scenario, such as initializing variables, setting default values in table definitions, or more complex logic that might, for some reason, necessitate a different approach.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sqlpowered.com/generating-empty-guids/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Exploring the Power of the JSON_OBJECT() Function</title>
		<link>https://sqlpowered.com/exploring-the-power-of-the-json_object-function/</link>
					<comments>https://sqlpowered.com/exploring-the-power-of-the-json_object-function/#respond</comments>
		
		<dc:creator><![CDATA[Jan Dvořák]]></dc:creator>
		<pubDate>Sun, 31 Mar 2024 10:54:24 +0000</pubDate>
				<category><![CDATA[T-SQL]]></category>
		<guid isPermaLink="false">https://sqlpowered.com/?p=5670</guid>

					<description><![CDATA[SQL Server 2022 enhances its JSON capabilities with the JSON_OBJECT() function, providing a streamlined approach to transforming SQL query results into JSON format directly within SQL queries. This functionality is invaluable for developers working in environments where SQL Server interacts with web services, applications, or systems that consume or produce...]]></description>
										<content:encoded><![CDATA[<p>SQL Server 2022 enhances its JSON capabilities with the <code>JSON_OBJECT()</code> function, providing a streamlined approach to transforming SQL query results into JSON format directly within SQL queries. This functionality is invaluable for developers working in environments where SQL Server interacts with web services, applications, or systems that consume or produce JSON data. To illustrate the practical application of the <code>JSON_OBJECT()</code> function, we&#8217;ll use the <code>dbo.Employee</code> table as a reference throughout this article. Let&#8217;s begin by preparing our dataset.</p>
<h2>Setting Up the dbo.Employee Table</h2>
<p>First, we&#8217;ll create the <code>dbo.Employee</code> table and populate it with sample data. This setup will serve as the foundation for our examples.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Creation of dbo.Employee table
CREATE TABLE dbo.Employee (
    EmployeeID INT PRIMARY KEY,
    FirstName NVARCHAR(50),
    LastName NVARCHAR(50),
    Position NVARCHAR(50),
    Department NVARCHAR(50),
    Salary INT
);

-- Inserting sample data into dbo.Employee
INSERT INTO dbo.Employee (EmployeeID, FirstName, LastName, Position, Department, Salary)
VALUES
    (1, 'John', 'Doe', 'Software Engineer', 'Engineering', 70000),
    (2, 'Jane', 'Smith', 'Project Manager', 'Project Management', 85000),
    (3, 'William', 'Johnson', 'Data Analyst', 'Data Science', 75000),
    (4, 'Emma', 'Brown', 'UI/UX Designer', 'Design', 68000),
    (5, 'Michael', 'Davis', 'DevOps Engineer', 'Engineering', 90000);</pre>
<div class="dark bg-gray-950 rounded-md"></div>
<p>With our table ready, let&#8217;s explore how to use the <code>JSON_OBJECT()</code> function with the <code>dbo.Employee</code> table through a series of examples.</p>
<h2>Example 1: Creating a Simple JSON Object</h2>
<p>In this first example, we create a JSON object for each employee that includes their ID, full name, and position.</p>
<p><strong>SQL Query:</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SELECT 
    JSON_OBJECT(
        'EmployeeID':CAST(EmployeeID AS NVARCHAR(10)),
        'FullName':CONCAT(FirstName, ' ', LastName),
        'Position':Position
    ) AS EmployeeJSON
FROM dbo.Employee;</pre>
<p><strong>JSON Output:</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="json">{"EmployeeID":"1","FullName":"John Doe","Position":"Software Engineer"}
{"EmployeeID":"2","FullName":"Jane Smith","Position":"Project Manager"}
{"EmployeeID":"3","FullName":"William Johnson","Position":"Data Analyst"}
{"EmployeeID":"4","FullName":"Emma Brown","Position":"UI\/UX Designer"}
{"EmployeeID":"5","FullName":"Michael Davis","Position":"DevOps Engineer"}</pre>
<h2>Example 2: Nesting JSON Objects</h2>
<p>We explore the creation of nested JSON objects. Here, we structure the JSON to include a nested object for the department, showcasing a more complex data hierarchy.</p>
<p><strong>SQL Query:</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SELECT 
    JSON_OBJECT(
        'EmployeeID': CAST(EmployeeID AS NVARCHAR(10)),
        'FullName': CONCAT(FirstName, ' ', LastName),
        'Position': Position,
        'Department': JSON_OBJECT('Name': Department, 'BudgetCode': 'XYZ123')
    ) AS EmployeeJSON
FROM dbo.Employee;</pre>
<div class="dark bg-gray-950 rounded-md"></div>
<p><strong>JSON Output:</strong></p>
<p>For an employee in the Engineering department, the JSON output would appear as follows:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="json">{
    "EmployeeID": "1",
    "FullName": "John Doe",
    "Position": "Software Engineer",
    "Department": {
        "Name": "Engineering",
        "BudgetCode": "XYZ123"
    }
}</pre>
<p>Through these examples, it&#8217;s clear how the <code>JSON_OBJECT()</code> function can be utilized to produce structured JSON data directly from SQL queries, simplifying the integration between SQL Server and applications that operate with JSON. Whether you&#8217;re creating simple or complex JSON objects, <code>JSON_OBJECT()</code> offers the flexibility and power to meet your data serialization needs.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://sqlpowered.com/exploring-the-power-of-the-json_object-function/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<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 loading="lazy" 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="auto, (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 loading="lazy" 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="auto, (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 loading="lazy" 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="auto, (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>
	</channel>
</rss>
