Click here to Skip to main content
15,881,898 members
Articles / Database Development / SQL Server / SQL Server 2008
Tip/Trick

Shortest Path in SQL Server using hierarchyid

Rate me:
Please Sign up or sign in to vote.
4.15/5 (7 votes)
29 Nov 2013CPOL2 min read 21.5K   10   8
Implementing a map as a tree, and finding the shortest path, between the streets, is what you will see.

Introduction

Sometimes, we need to store our map in a database and save some data related to the map. I've tried to do this, and I've found the path between the streets.

The benefit of my way is set base manipulation, as SQL Server optimizer has multi-thread processing for set base manipulation. and search in set base processing can have binary search efficient.

Image 1

As you can see, I have 20 or more streets and alleys in the map, for storing them to database, I design this table:

Image 2

The logic of my Table is that each node(street) can have multiple parents and for each parent, it has one record, and each node(street) can have multiple child(street):

Using the Code

First Step

At first, I needed to store my map to table, I got a screen shot of Google map and inserted it into my table using a procedure.

Take a look at my table design:

SQL
CREATE TABLE [dbo].[WayTree](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[WayName] [nvarchar](300) NULL,
	[WayHierarchyID] [hierarchyid] NULL,
	[WayLevel]  AS ([WayHierarchyID].[GetLevel]()) PERSISTED,
	[WayAncestor]  AS ([WayHierarchyID].[GetAncestor]((1))) PERSISTED,
	[Cost] [decimal](18, 0) NULL,
PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, _
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] 

Cost is the distance of child from his parent!

And procedure is:

SQL
create proc [dbo].[InsertChild] (@ParrentID int, @ChildName nvarchar(100), @cost Decimal)
 as 
 begin
 Declare @ParrentHierarchyID HierarchyID 
 if ((select count(*) from WayTree where id = @ParrentID)=0)
 begin
 insert into WayTree (WayName, WayHierarchyID, Cost) values(@ChildName,N'/', 0 )
 end
 else
 begin
 insert into WayTree(WayName,  WayHierarchyID, Cost) 
 select @ChildName,
   WayHierarchyID.GetDescendant((select max(WayHierarchyID.ToString()) 
					from WayTree w
					where WayHierarchyID.IsDescendantOf_
					((select  w2.WayHierarchyID from WayTree w2 where w2.ID = @ParrentID)) = 1
					and w.WayHierarchyID.GetLevel()=w3.WayHierarchyID.GetLevel()+1), null).ToString(),
   @cost from WayTree w3 where ID = @ParrentID

 end
 end 

As you can see for inserting WayHierarchyID, I add some code, the reason is that we need to find last direct child of parent(@ParentID), you can find the direct child with function GetLevel(), because always this term [child.GetLevel() = Parent.GetLevel()+1] is true, for example the input of procedure is:

SQL
@ParentID = 11 

and the hierarchyid of record with id = 11 is N'/1/2'

and it has three direct children : N'/1/2/1', N'/1/2/2', N'/1/2/3'

so next child has this hierarchyid : N'/1/2/4'

which we find this hierarchyid with function .GetDescendant()

Please note the logical errors in procedure are not completely handled as it depends on each business logic.

The last thing I need to do for inserting a map is calling my procedure, in fact I don't insert all the node , just some of them, you can find ChildName from map, if you ask why I use the ID of parent instead of its name in my procedure, I want you to go to the table design and check its logic, each street can have multiple records as it can have multiple parents, so its name is not useful, we need its ID, but you can solve this problem in UI:

SQL
exec InsertChild 1 , N'EmamKHomeini', 0
exec InsertChild 1 , N'Basti', 2
exec InsertChild 2 , N'Aftab', 2
exec InsertChild 2 , N'Laleh1', 2
exec InsertChild 2 , N'Laleh2', 4
exec InsertChild 2 , N'Mahtab', 5
exec InsertChild 2 , N'Laleh3', 6
exec InsertChild 2 , N'Laleh4', 8
exec InsertChild 2 , N'Laleh5', 10
exec InsertChild 1 , N'Soheil', 5
exec InsertChild 1 , N'Mokhaberat', 8
exec InsertChild 11 , N'M_Alley1', 3
exec InsertChild 11 , N'M_Alley2', 5
exec InsertChild 11 , N'Laleh4', 7
exec InsertChild 11 , N'MokhaberatAlley3', 8
exec InsertChild 1 , N'Isargaran', 10
exec InsertChild 16 , N'I_Alley1', 3
exec InsertChild 16 , N'I_Alley2', 5
exec InsertChild 16 , N'I_Alley3', 8
exec InsertChild 1 , N'Farhangian', 13
exec InsertChild 20 , N'I_Alley1', 3
exec InsertChild 20 , N'I_Alley2', 5
exec InsertChild 20 , N'I_Alley3', 8
exec InsertChild 20 , N'Farhang1', 3
exec InsertChild 20 , N'Farhang2', 5
exec InsertChild 20 , N'Farhang3', 8

Second Step

Writing the query to find path, and shortest path between two locations:

The major logic of my query is retrieving all the parent/child of beginning street in a recursive way, and parent/child of result node to last, so I have all the nodes which have relation with beginning node, then I create path between nodes by adding direct parents and children together.

At last, I found the path which includes beginning and destination.

SQL
declare @Beginning nvarchar(100) = N'Laleh4'
declare @Destination nvarchar(100) = N'Farhang3'
declare @MaxRecursion int = 4 -- be note in worth case the max recursion 
--is the sum of location with two or more parents + the length of longest WayLevel
;with cte as (
select 1 as RowNumber,Begining.WayName BeginingName, _
Begining.WayHierarchyID as BeginingHierarchyID, _
Begining.WayHierarchyID.ToString() as BeginingHierarchyIDString,
	   Begining.Cost BeginingCost,
	   RelatedWay.WayName RelatedWayName,
	   RelatedWay.WayHierarchyID.ToString() as RelatedHierarchyIDString,
	   RelatedWay.WayHierarchyID as RelatedHierarchyID,
	   RelatedWay.Cost RelatedWayCost
from WayTree Begining 
inner join WayTree RelatedWay on (RelatedWay.WayHierarchyID.IsDescendantOf_
( Begining.WayHierarchyID) = 1 and Begining.WayHierarchyID != RelatedWay.WayHierarchyID 
								  and RelatedWay.WayLevel =  Begining.WayLevel + 1) or
								    (Begining.WayHierarchyID.IsDescendantOf_
								    (RelatedWay.WayHierarchyID) = 1 and _
								    Begining.WayHierarchyID != RelatedWay.WayHierarchyID
								  and Begining.WayLevel = RelatedWay.WayLevel + 1)
where Begining.WayName= @Beginning 

	 union all

select  c.RowNumber + 1 as RowNumber,  Begining.WayName BeginingName, _
Begining.WayHierarchyID as BeginingHierarchyID,
	   Begining.WayHierarchyID.ToString() as BeginingHierarchyIDString,
	   Begining.Cost BeginingCost, 
	   RelatedWay.WayName RelatedWayName,
       RelatedWay.WayHierarchyID.ToString() as RelatedHierarchyIDString,
	   RelatedWay.WayHierarchyID as RelatedHierarchyID,
	   RelatedWay.Cost RelatedWayCost
from cte c
inner join WayTree as Begining on c.RelatedWayName = Begining.WayName 
inner join WayTree RelatedWay on ((RelatedWay.WayHierarchyID.IsDescendantOf_
(Begining.WayHierarchyID) = 1 and Begining.WayHierarchyID != RelatedWay.WayHierarchyID
								   and RelatedWay.WayLevel = Begining.WayLevel + 1) or
								  (Begining.WayHierarchyID.IsDescendantOf_
								  ( RelatedWay.WayHierarchyID) = 1 and _
								  Begining.WayHierarchyID != RelatedWay.WayHierarchyID
								   and Begining.WayLevel = RelatedWay.WayLevel + 1))

where c.RowNumber<@MaxRecursion
 and RelatedWay.WayHierarchyID != c.BeginingHierarchyID
 and RelatedWay.WayHierarchyID != Begining.WayHierarchyID

 ), cte2 as (

select distinct cte.BeginingName, _
cte.BeginingHierarchyIDString,cte.BeginingHierarchyID, cte.BeginingCost,
			    cte.RelatedWayName, cte.RelatedHierarchyIDString, _
			    cte.RelatedHierarchyID, cte.RelatedWayCost
from cte
)

,cte4 as(

select  1 AS RowNumber, BeginingHierarchyID, BeginingHierarchyIDString,BeginingName , _
CAST(cte2.BeginingName AS nvarchar(MAX)) as waypath, cast(BeginingCost as decimal) _
as cost ,cast(0 as decimal)pathcost
from cte2
union all 
select 1 as RowNumber, RelatedHierarchyID, RelatedHierarchyIDString, RelatedWayName, _
CAST(cte2.RelatedWayName AS nvarchar(MAX)) as waypath, cast(RelatedWayCost as decimal) _
as cost,cast(0 as decimal) as pathcost
from cte2

union all

select C.RowNumber + 1, c2.BeginingHierarchyID,C2.BeginingHierarchyIDString,C2.BeginingName, 
cast((c.waypath+'.'+C2.BeginingName) as nvarchar(MAX)) as waypath,
cast(case when c.BeginingHierarchyID.IsDescendantOf_
(c2.BeginingHierarchyID)=1 then c2.BeginingCost
		when c2.BeginingHierarchyID.IsDescendantOf_
		(c.BeginingHierarchyID )=1 then c2.BeginingCost end as decimal),
cast(c.pathcost + cast(case when c.BeginingHierarchyID.IsDescendantOf_
(c2.BeginingHierarchyID)=1 then c.Cost
		when c2.BeginingHierarchyID.IsDescendantOf_
		(c.BeginingHierarchyID )=1 then c2.BeginingCost end as decimal)
														  as decimal)
from cte4 c 
inner join cte2 c2 on (c.BeginingHierarchyID.IsDescendantOf_
(c2.BeginingHierarchyID)=1 and c.BeginingHierarchyID.GetLevel() = _
c2.BeginingHierarchyID.GetLevel()+1 and c.waypath not like N'%'+c2.BeginingName+N'%')
			or (c2.BeginingHierarchyID.IsDescendantOf_
			(c.BeginingHierarchyID )=1 and c.BeginingHierarchyID.GetLevel() _
			+1 = c2.BeginingHierarchyID.GetLevel() and c.waypath not like N'%'+c2.BeginingName+N'%')
    WHERE C.RowNumber<10
)
select * into #temp  from cte4 
OPTION (MAXRECURSION 10);
select distinct * from #Temp
where waypath like N'%'+@Destination+'%'
and waypath like N'%'+@Beginning+'%' 

Remember in the worst case, the max recursion is the sum of location with two or more parents + the length of longest WayLevel.

Points of Interest

I think if we want to show shortest path to users, the best way could be saving them after process, and when user wants the result, just showing the result, without process.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Idea Tolue Software
Iran (Islamic Republic of) Iran (Islamic Republic of)
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionWhen in trouble like this call on Code Project Pin
HanksComputer19-Dec-13 21:21
HanksComputer19-Dec-13 21:21 
AnswerRe: When in trouble like this call on Code Project Pin
Mehdy Moini20-Dec-13 7:51
professionalMehdy Moini20-Dec-13 7:51 
QuestionNo Images Pin
Sleety13-Dec-13 8:20
Sleety13-Dec-13 8:20 
AnswerRe: No Images Pin
Mehdy Moini13-Dec-13 8:34
professionalMehdy Moini13-Dec-13 8:34 
QuestionImages? Pin
Aglets2-Dec-13 11:11
Aglets2-Dec-13 11:11 
AnswerRe: Images? Pin
Mehdy Moini2-Dec-13 20:59
professionalMehdy Moini2-Dec-13 20:59 
Questioncongratulation Pin
sajjad hasanzadeh1-Dec-13 2:35
sajjad hasanzadeh1-Dec-13 2:35 
AnswerRe: congratulation Pin
Mehdy Moini1-Dec-13 3:27
professionalMehdy Moini1-Dec-13 3:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.