In all the possible models of hierarchical data representation, the Adjacency List Model is perhaps the most favoured one among the software developers - for its sheer straight-forwardness and easy implementation. The name Adjacency List is borrowed from the Graph Theory. In its simplest form, each piece of data holds enough information about its parent. Imagine a child's passport wherein her father's name is written. Similarly, in the father's passport, his father's (i.e. the child's grandfather's) name is written. By looking at the girl's passport, we can't determine the grandfather's name without also referring to her father's passport. Please also note, in this case, that this journey is always way up - you can't look at the father's passport and determine his daughter's name.
Let us now play with some real database stuff. The example that we are going to present is that of the list of categories of products sold by a given Departmental Store. The following diagram shows the hierarchy in a tree form:
The top-most node, Product Categories is the root node - it doesn't have any parent. While this example shows one root node, in reality it is possible to have more than one root node, though it is best avoided when designing your data model.
The root node has three children, Consumer Electronics, Computers and Peripherals and Cameras. Each of these children in return, has more than one child, some of which has children one level down the hierarchy. I chose these specifically to show that a hierarchical list need not be a balanced - some branch of the tree can go n-level deep and some may be fewer than that.
This hierarchical information can be very well represented in Xml, as one of the strengths of Xml is to be able to represent hierarchical information at ease. Let's have a look:
<Category CategoryName="Product Categories">
<Category CategoryName="Consumer Electronics">
<Category CategoryName="Electronic Gadgets"/>
<Category CategoryName="Home Appliances"/>
<Category CategoryName="Office Electronics"/>
</Category>
<Category CategoryName="Computers & Peripherals"/>
<Category CategoryName="Cameras">
<Category CategoryName="Digital Compact Cameras"/>
<Category CategoryName="Digital SLR Cameras" />
</Category>
</Category>
I am just showing the first three levels of the hierarchy in the above xml. Looking at either the diagram or the xml above, you shouldn't have any problem in figuring out what is the grandparent of Digital SLR Cameras. This xml can of course be extended to include all the levels.
So far so good. The problems start pouring in when we want to represent the same data in a tabular form in SQL Server and most other commonly used RDBMs. The idea borrowed from Adjacency List model comes into rescue at this point. Consider the following diagram:
We have assigned unique numbers to each and every category node to uniquely identify them. Let's call these numbers CategoryId and these would be our primary keys. Now for each node, we have one more attribute called ParentCategoryId and the above diagram shows how we can derive the values for this attribute for each of the nodes. Essentially, we are looking at the tree diagram above, identifying the parent of a given node, and using the CategoryId of the parent node as the ParentCategoryId of the current node. Note that the left-most table doesn't have any ParentCategoryId as that contains the root node and root nodes do not have any parents. A bit of thinking leads us to normalize the above tables into one single table as shown below:
Each row in this table contains three pieces of information: name of the category, the unique id of the category, and the category id of the parent of the current category. With this clarity of understanding, it's time to cook up some SQL scripts to create our table in SQL Server and fill in the values.
--create table first
CREATE TABLE Category(
[CategoryId] [int] NOT NULL PRIMARY KEY,
[CategoryName] [nvarchar](100) NOT NULL,
[ParentCategoryId] [int] NULL )
ON [PRIMARY]
GO
-- Insert data for the root level
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 0, 'Product Categories', NULL)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 1, 'Consumer Electronics', 0)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 2, 'Computers & Peripherals', 0)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 3, 'Cameras', 0)
-- Insert data for the intermediate (Level 1)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 4, 'Electronic Gadgets', 1)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 5, 'Home Appliances', 1)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 6, 'Office Electronics', 1)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 11, 'Digital Compact Cameras', 3)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 12, 'Digital SLR Cameras', 3)
-- Insert data for the leaf level
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 7, 'Desktop PCs', 2)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 8, 'Laptops', 2)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 9, 'Printers', 2)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 10, 'Scanners', 2)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 13, '5-6 Megapixels', 11)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 14, '6-7 Megapixels', 11)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 15, '7-8 Megapixels', 11)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 16, 'Brand XXX SLR Cameras', 11)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 17, 'Brand YYY SLR Cameras', 11)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 18, 'GPS and Navigation', 4)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 19, 'Voice Recorders', 4)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 20, 'Electronic Shavers', 4)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 21, 'DVD Players', 4)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 22, 'MP3 Players', 4)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 23, 'Air Conditioners', 5)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 24, 'Food Processors', 5)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 25, 'Microwaves', 5)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 26, 'Vacuum Cleaners', 5)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 27, 'Calculators', 6)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 28, 'Currency Machines', 6)
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 29, 'Paper Shredders', 6)
Run a SELECT query to see how the entire table looks now.
Data thus stored is no good unless we can extract meaningful information out of them. The query to find out all the root nodes (in our case there is only one):
SELECT * FROM Category WHERE ParentCategoryId IS NULL
The query to find out all leaf nodes - the nodes that don't have any children:
SELECT C1.CategoryId,
C1.CategoryName
FROM Category C1
LEFT JOIN Category C2
ON C1.CategoryId = C2.ParentCategoryId WHERE C2.CategoryId IS NULL
We get all the categories which do not have any children:
As you can see from the query, we have achieved our desired result through what is called as an "Self-Join": the query outer-joins the ParentCategoryId to the CategoryId of the same table and ensures that no ParentCategoryId (through IS NULL check) exists which points to the current CategoryId. Now let's see how we can retrieve a single path for any chosen leaf:
SELECT C1.CategoryName AS Level1,
C2.CategoryName as Level2,
C3.CategoryName as Level3,
C4.CategoryName as Level4
FROM Category C1
LEFT JOIN Category C2 ON C2.ParentCategoryId = C1.CategoryId
LEFT JOIN Category C3 ON C3.ParentCategoryId = C2.CategoryId
LEFT JOIN Category C4 ON C4.ParentCategoryId = C3.CategoryId
WHERE C4.CategoryName = 'MP3 Players'
The complexity starts here. To reconstruct each single path, we need to know the depth of the node and our query is hard-coded for that level of depth. This indeed is annoying. At the time of design and coding, we may not know the actual hierarchy of product categories that are in use in a given departmental store. Hard-coding SQL in such way actually limits the flexibility of the application. The solution lies in a technique recursion. Back in SQL Server 2000, we would be writing recursive functions and stored procedures to achieve the feat. SQL Server 2005 provides new feature called Common Table Expressions which is just the tool we needed at this juncture. Without going into further details how Common Table Expressions work, as you may always refer to myriads of web-articles on that, let's straight plunge into SQL:
WITH CTE AS (
SELECT CAST('/' + C1.CategoryName AS NVARCHAR(2000)) AS FullPath,
CategoryId
FROM Category C1
WHERE C1.ParentCategoryId IS NULL
UNION ALL
SELECT CAST(CTE.FullPath + '/' + C1.CategoryName AS NVARCHAR(2000)),
C1.CategoryId
FROM Category C1
JOIN CTE ON C1.ParentCategoryId = CTE.CategoryId
)
SELECT CTE.FullPath + '/' + C1.CategoryName
FROM CTE JOIN Category C1 ON CTE.CategoryId = C1.ParentCategoryId
ORDER BY 1
The result from this query a proper bread-crumb path for each and every node, not just the child nodes:
To retrieve the data in the above xml format, a bit of work is needed. We will first write a recursive scalar function, and then we fire the function from a SELECT query:
--recurive function to walk through the hierarchy
CREATE FUNCTION dbo.fnCategoryTree(@CategoryId int)
RETURNS XML
BEGIN
RETURN
(SELECT CategoryId AS "@CategoryId",
CategoryName AS "@CategoryName",
ParentCategoryId AS "@ParentCategoryId",
CASE
WHEN ParentCategoryId = @CategoryId THEN dbo.fnCategoryTree(CategoryId)
END
FROM Category
WHERE ParentCategoryId = @CategoryId
FOR XML PATH('Category'), TYPE )
END
--retrieve xml data
SELECT CategoryId AS "@CategoryId",
CategoryName AS "@CategoryName",
CASE
WHEN CategoryId = 0 THEN dbo.fnCategoryTree(CategoryId)
END
FROM Category
WHERE CategoryId = 0
FOR XML PATH('Category'), TYPE
DML operations in Adjacency List Model
This model is extremely programmer-friendly when it comes to INSERT, UPDATE and DELETE operations. The model itself doesn't render any constraint over such operations. Inserting data can simply mean adding one more row at the bottom of the list, with the ParentCategoryId pointing to one existing CategoryId anywhere in the table. The statement can be as simple as:
INSERT INTO Category (CategoryId, CategoryName, ParentCategoryId) VALUES( 30, 'UPS', 2)
Of course the statement would have been a bit simpler if we had marked the CategoryId column as an IDENTITY column. Note that, just by ensuring a correct value for ParentCategoryId, you can not only create leaf nodes, but can create any node at any intermediate level. You may even convert one existing leaf node into an intermediate node by appending one lead below it.
Likewise, update operations are simple too. Assuming you are following the best practice of not modifying the primary key (CategoryId), updating a row merely involves either changing the Category Name of a given node or changing its parent. While changing the node name has no impact on the tree structure, changing the parent changes the physical structure of the tree. In fact, such is the power of the Adjacency List model that you can actually uproot a branch from one position of the tree and graft it below some other branch!
For example, let's try move the entire "Camera" branch, which is directly under the root, under the "Consumer Electronics" node. To achieve this, all we need to do is to change the parent of the "Camera" node from 0 (CategoryId of "Product Categories" node) to 1 (CategoryId of "Consumer Electronics") with this simple SQL:
UPDATE Category SET ParentCategoryId = 1 WHERE CategoryId = 3 -- 3 is the CategoryId of the Camera node
DELETE operations, however, are not as easy. You just cannot delete any intermediate node as that would make the entire children and descendants orphan! Ideally, you should move the descendants first under some other parent (as shown above) before removing a parent.
Improving Adjacency Model
The Adjacency Model we examined so far may be criticized for several drawbacks. For example:
- It is possible to have a child with a phantom parent (i.e. a ParentCategoryId which doesn't exist)
- It is possible to make a child its own parent (ParentCategoryId and the CategoryId being same)
- As mentioned above, you can have a whole branch dangling (without a parent)
- Because the ParentCategoryId column is NULLable, it allows you to have more than one root.
The above table design follows the Adjacency List model, but there is nothing that can stop us from deviating from that as long as we fulfil the requirement of creating a data model capable enough for representing hierarchical data. One immediate thought coming to my mind is to split the ParentCategoryId column to some other table (along with the necessary CategoryId information). Once we do that, we need to set up couple of constraints (Foreign Key, Check etc) to overcome the above problems. I'll leave it you as an exercise:)









