swirl
Home Software Blog Wallpapers Webtools
SQL Server Hierarchical data
Tuesday 11, October 2016   |   Post link   |   Sample code  Download related meterial

It's not everyday I need to store hierarchical data in SQL tables but when I do, the usual self-join on a table works out fine. For example, if I'm storing file system folders in a SQL database, I'll probably define a table like this:

create table Folder
(
	Id int Primary Key Identity(1,1),
	Name varchar(256) Not Null,
	Path varchar(Max) Not Null,	
	ParentId Int Null	
);

create index IX_Folder_ParentId on Folder (ParentId)
go

If I'm storing my humble music collection the output might look like this:

sample output

I can now find out which folder belong to whom. For something trickier like finding the the entire hierarchy below a particular folder, I can use a CTE like this:

-- I want to list all albums of Guns n' Roses  	
declare @folderId int = 335;
with FolderHierarchy (id, name, path, parentid, level) as 
(
	select Id, Name, Path, ParentId, 0 as level from Folder where Id = @folderId
	union all
	select f.Id, f.Name, f.Path, f.ParentId, level + 1 from Folder f
	inner join FolderHierarchy h
	on f.ParentId = h.Id
)
select * from FolderHierarchy order by level
go
output

As it turns out this isn't the only way to represent hierarchical data. SQL Server since version 2008 has specific datatype for such scenarios, its the 'hierarchyid' type. If I now introduce a new column for it, the table can be created like this:

if exists(select * from sys.tables where name = 'Folder')
drop table Folder 
go

create table Folder
(
	Id int Primary Key Identity(1,1),
	Name varchar(256) Not Null,
	Path varchar(Max) Not Null,	
	ParentId Int Null, -- No longer needed but I've kept it to avoid making two tables for illustration  
	HId HierarchyId Null
);
go

create index IX_Folder_ParentId on Folder (ParentId)
go

create unique index UQ_Folder_HId on Folder (HId)
go

The HId column is supposed to store hierarchy in a particular format. The format is:
The root item should have the value /. The first child of the root can be /1/, the second child can be /2/. There can be a child inserted between 1 and 2 as '/1.5/'. If 1 has a child, it can be /1/1/ and so on. These values can computed at the application side and inserted into the table or SQL functions can help you generate these. In my case I have decided to generate these values in the sample C# application itself.

Once the hierarchy is correctly setup we can perform things like listing all direct or indirect descendant quite easily. For example, instead of using the CTE we can now use:

select *, cast(hid as varchar(max)) from Folder where HId.IsDescendantOf('/4/40/') = 1
go

What about doing the reverse which is listing all direct and indirect parents? The code below shows both approaches.

-- Find all ancestors using CTE
with FolderHierarchy (id, name, path, parentid) as 
(
	select Id, Name, Path, ParentId from Folder where ID = 335
	union all
	select f1.Id, f1.Name, f1.Path, f1.ParentId from Folder f1
	inner join FolderHierarchy f2 on f1.id = f2.ParentId
)
select * from FolderHierarchy

-- Find all ancestors using hierarchy
select f1.*, cast(f1.hid as varchar(max)) as Path, f1.hid.GetLevel() as Level from Folder f1 
inner join Folder f2 on f2.HId.IsDescendantOf(f1.HId) = 1 
where f2.Id = 335

So which is better? Well the hierarchyid datatype allows quite a few other operations like moving entire hierarchy under a different node but this should be easy using the traditional approach too. What about performance? Here is screenshot of the plan comparison for a small number (1000 odd) of records.

simple plan comparison

It looks like using hierarchyid results in a much simpler plan and is 2 times better. However when I tried with greater number of records the difference was not too much.

plan comparison more records

For the query where we listed direct and indirect ancestors, the difference is reverse, using hierarchyid makes the performance worse.

ancestor plan comparison

The sample application which reads a file system folder and populates the Folder table is present in the ZIP file. The ZIP file also contains the database.sql file which contains all the T-SQL code discussed here.

Sample code  Download related meterial


Comments

Posts By Year

2017 (8)
2016 (6)
2015 (18)
2014 (2)
2013 (4)
2012 (2)

Posts By Category

ASP.NET MVC (4)
Android (1)
Book review (3)
Business (1)
Containers (3)
Corporate culture (1)
Database migration (1)
Desktop (1)
Entity Framework (2)
Git (2)
IIS (1)
Java (2)
Life (6)
Lucene (1)
OData (1)
Office (1)
PHP (1)
PowerShell (1)
Programming (14)
Rants (5)
Software Engineering (1)
Software development (1)
Solr (1)
Sql Server (1)
T-SQL (1)
TDD (1)
TSQL (5)
Tablet (1)
Technology (1)
Test Driven (1)
Unit Testing (1)
Utilities (1)
Wallpapers (1)
Windows (5)
XML (1)

Posts By Tags

ASP.NET(4) Adults(1) Advertising(1) Android(1) Anti-forgery(1) Backup(1) Beliefs(1) Book review(2) Books(1) Busy(1) C#(2) C++(1) CSRF(1) CTE(1) Checkbox(1) Cmdlet(1) Commons(1) Company culture(1) Consumerism(1) Containers(3) Data-time(1) Database(1) Debugging(1) Developer(2) Dockers(2) Entity framework(1) File copy(1) File history(1) Git(1) GradleApache(1) HierarchyID(1) IIS(1) Installing(1) Intelli J(1) JSON(1) JavaScript(1) Log4J(1) Lucene(1) MVC(4) Management(2) Migration history(1) Mobile Apps(1) Modern Life(1) Money(1) OData(1) Office(1) Organization(1) PHP(1) Paths(1) PowerShell(1) Programming(1) Python(1) Quality(1) SD card(1) SQL(1) SQL Code-first Migration(1) SSH(1) School(1) Self reliance(1) Solr(1) Sony VAIO(1) Spirituality(1) Sql Express(1) System Image(1) TDD(1) TSQL(3) Tablet(1) Url rewrite(1) Validation(2) Wallpapers(1) Web Development(4) Windows(1) Windows 10(1) Windows 2016(2) Windows 8.1(1) Work culture(1) XML(1) Yii(1)