Click here to Skip to main content
Click here to Skip to main content

General trees persisted in relational databases

By , , 23 Jun 2003
 

Abstract

We present some techniques that may be used to perform computations on general trees implemented in a relational database as a self-joined table in the widely used child-parent format, and we prove the validity of this technique under very general hypothesis. Another, much better actually format.

Introduction

Relational databases are not making much progress lately in the path finding problem in a persisted tree. Vendor specific SQL extensions, such as the CONNECT BY clause in Oracle, are a further step in this area. But the casual developer still finds their performance unacceptable for most real life situations, and it's also not recommended to tight its application to a vendor specific database. It is acceptable to implement different versions of stored procedures or SQLs for different databases, but when all the application logic is based on tree facilities only provided by a specific vendor, it gets much harder to port the application to other database systems. And it's usually true that the clients have not bought the most expensive commercial database systems.

We propose some solutions to the path finding problem, that are not database specific while still giving better performance than some of the database-specific extensions supporting trees.

Changes to the child-parent format to keep the entire path for every node

This part is optional. One can skip this part to the Alternate Format section.

Database structure

The reader should note that the method presented in this section is efficient especially for read-only access to such persisted trees. The write operations complexity is roughly presented in this paper. Also, the alternate format is what you probably are looking for.

For performance reasons, one could try to add to every node the information representing the path to that node from the root of the tree. I don't actually know any applications that could benefit from this, and if I can't find such stuff in a short time, I'll remove this section.

We add a column named Path to the table containing the child to parent links. This column must not be too big, so we'll analyze it's space complexity first. The time needed to compute the Path column values is less important because we primarily discuss here read-only trees.

The Path column should be some kind of bit sequence. Some examples are RAW in Oracle or Binary in SQL Server. We define the Path value recursively (trees are to be processed recursively, isn't it ?):

Path(RootNode) = Null

For nodes other than the root,

Path(Node) = Path(Parent(Node)) + Index(Node, Parent(Node))

where + means concatenation of bit sequences, and Index is the child ordinal in the domain of the parent nodes children list.

Note that the Path will uniquely identify every node in the tree. An unique index on this column might be ok to use, if needed in case a single tree is stored in the table.

All the nodes at a given level will have the Index represented at the same bit size. Let the node count in a tree be denoted by N, let the levels be 0,1,..., L and the max(Index(i)) be Ci, where 0 <= i <= L – 1. If we want we can also define max(Index(CL)) = 0, because the nodes on the highest level have no children. It becomes clear that the bit size of the Index for nodes on level i is equal to ceil(log2(Ci)), where ceil is an integer value equal to the smallest integer greater than or equal to its numeric argument, and log2 is the base 2 logarithm of a specified number.

Having this Path format, we can easily select an entire subtree when given the subroot, searching for nodes where Path(Node).startsWith(Path(SubRoot)) and we also have at each node the information needed to identify the path to the root. But how long must be the Path column to contain this plenty of information?

If the tree is really static (read-only), that the Path size is calculated specifically for it. One can use this approach if he has strong guarantees that there is no need to change the tree structure, or that modifying a column length is an acceptable operation (especially at run-time). Another approach would be to first compute the maximum Path size for a given tree node count. Even if the first approach does not need knowledge about the maximum path size for trees of N nodes, it is still needed to mathematically prove that both approaches are appropriate for real life usage.

Path size bounds

For any given node count N, we find out the worst case for the required Path size, and also compute that maximum Path size value. If we allocate that maximum size for the Path column, then we are guaranteed that no matter how “strange” a tree with N nodes or less looks, it “fits” into our table.

The path size for any given tree TN with N nodes and L levels is equal to

PathSize(T<SUB>N</SUB>) = ceil(log<SUB>2</SUB>(C<SUB>0</SUB>)) + ... + ceil(log<SUB>2</SUB>(C<SUB>L - 1</SUB>))

Note that C0 + ... + CL - 1 <= N – 1, the equality holding when every node in the tree has at most one child with children on his own. We find an upper bound for PathSize(TN) in the domain denoted by the previous inequality.

PathSize(T<SUB>N</SUB>) <= L + log(C<SUB>0</SUB> * ... * C<SUB>L - 1</SUB>)

The arithmetic-geometric mean inequality tells us that

(C<SUB>0</SUB> * ... * C<SUB>L - 1</SUB>)<SUP>1 / L</SUP> <= (C<SUB>0</SUB> + ... + C<SUB>L - 1</SUB>) / L

We deduce that

PathSize(T<SUB>N</SUB>) <= L + L * log<SUB>2</SUB>((C<SUB>0</SUB> + ... + C<SUB>L - 1</SUB>) / L) 
             <= L + L * log<SUB>2</SUB>((N - 1) / L).

Let the function F : 1..N-1 ---> N be the right part of the upper inequality. We'll find the global maximum for this function on its definition domain. What we need is the derivative:

F'(L) = 1 + 1 * log<SUB>2</SUB>((N - 1) / L) + 
     L * (1 – N) / L<SUP>2</SUP> * 1 / ((N – 1) / L * ln(2)),

where ln means the natural (base E) logarithm of a specified number.

F'(L) = log<SUB>2</SUB>(2 * (N – 1) / L) – 1 / ln(2) 
      = (ln(2 * (N – 1) / L) – 1) / ln(2).

F' is a decreasing function, and it has a single root equal to L0 = 2(N – 1) / E, where E is Euler's number, approximately 2.71 (for those interested in high precision arithmetic, see link).

So the maximum value for F is

F(L<SUB>0</SUB>) = F(2(N – 1) / E) 
      = 2(N – 1) / E + 2(N – 1) / E * log<SUB>2</SUB>((N - 1) * E / 2 (N – 1)) 
      = 2(N – 1) / E * (1 + log<SUB>2</SUB>(E / 2)) = 2(N – 1) / (E * ln(2)).

Update behavior

In order to optimize update behavior, a Path size greater than the actual needed value can be allocated, but it should always be less than or equal to the maximum Path size computed for the maximum number of nodes accepted by the application to reduce wasted space. The format of the Path field need not be exactly the one described previously. Space can be inserted between fields as necessary in order to allow growth of some levels. For example, if inserts are more likely to appear at some levels of the tree, than more free space should be allocated to those parts of the Path field. If a node is inserted into a level and the part of the parent's Path field cannot hold the new children count, than all the Path values must be changed to accommodate a new bit corresponding to the parent's level.

The most important thing to take care of is avoiding node inserts that cause some Paths not to fit anymore into the Path field. As proved before, this can be avoided computing the maximum path size according to the formula 2(N – 1) / (E * ln(2)). As long as the node count is less than or equal to N, this path size will accommodate any updates of the tree, be them inserts or removes.

If a node is removed or inserted, all the Path values of its subtree must be updated accordingly. So leaves can always be removed in O(1) time, and leaves can be added in O(1) time only if the Path part of the parent's level can accommodate an incremented value. Otherwise, we have an O(n) time complexity.

Numeric values

If N = 10,000 than the maximum Path size is less than 1.3 Ko, while for N = 1,000,000 we get the 130 Ko bound.

Oracle provides support for bit sequences in the UTL_RAW package, while SQL Server seems to have more limited functionality related to them.

In order to ask queries, for instance, of the form WHERE Path starts With '11011', one could cast the Path to a hex string, and check if the Path is lexicographically between D8 and DF, in case the database in use does not provide enough support for bit sequences manipulation.

Further study

First of all, some averages must be computed, especially for the Path size.

A few transformations from a general tree T to a binary tree B with the property that, whenever x is a child of y in T, x remains a descendent of y in B are evident. These are based on adding new, fake nodes, into the tree to make it binary in case a node has more than 2 children. Some more sophisticated transformations could try to keep the fake nodes grouped into complete binary subtrees.

One method would be to associate the 0 bit to left edges, and the 1 bit to right edges.

Another idea would be to overlay the binary tree on top of the Stern-Brocot tree, and use the fraction m/n as the Path column (actually, there could be 2 integer columns). If the binary tree has L levels, than m and n could get equal to FL, where F is the Fibonacci sequence. One advantage of this structure is that if one wants to find a rational number between other two (reduced) rational numbers, it only has to look “between” them.

If one wants to limit the values of m and n, the Farey series could be used instead. This one is actually a part of the Stern-Brocot tree. It could also be interesting to study how changes of the initial tree are reflected in the Stern-Brocot tree.

Alternate format

An alternate way to represent a tree in a relational database is to actually keep an integer positive index for each node. Let's call this column NodeIndex. If N is the node count of the tree, let's call the nodes 0, 1, ..., N – 1. The whole idea sustaining the format presented in this section is that the sequence 0, 1, ..., N – 1 must be the Depth-First Traversal of the tree. Well, this information is not sufficient to uniquely identify the rooted labeled tree amongst all the nn - 2 labeled trees of N nodes. To solve this inconvenience, we add a second column to the tree table, named RightChildIndex that is actually the index of the rightmost child of the current node. For the leaves, RightChildIndex is equal to NodeIndex.

Let's give an example:

Depth-First Traversal

NodeIndex 0 1 2 3 4 5 6
RightChildIndex 6 1 5 4 4 5 6

Given a NodeIndex i one can easily perform the following operations described in Oracle SQL. All of these have been tested with the well-known Employee table.

  • Select it's subtree:
    SELECT t1.*
    FROM employee t1, employee t2
    WHERE i <= t1.nodeindex
    AND t1.nodeindex <= t2.rightchildindex
    AND t2.nodeindex = i;

For instance, given the node 2, the select asks for all the nodes with indexes between 2 and 5, inclusive.

If the root itself doesn't need to be returned, the inequality i <= T1.NodeIndex must be changed to strict inequality.

  • Get the path from the root to the given node:
    SELECT t1.*
    FROM employee t1, employee t2
    WHERE t1.nodeindex <= i
    AND t1.rightchildindex >= t2.rightchildindex
    AND t2.nodeindex = i;

    For instance, given the node 3, where condition searches for nodes with NodeIndex <= 3 and RightChildIndex >= 4

  • Get the immediate children of a give node:
    SELECT t1.*
    FROM employee t1, employee t2
    WHERE t1.nodeindex > i
    AND t1.nodeindex <= t2.rightchildindex
    AND t2.nodeindex = i
    AND 0 = (SELECT COUNT (1)
    FROM employee q
    WHERE nodeindx < q.nodeindex
    AND q.nodeindex < t1.nodeindex
    AND t1.rightchildindex <= q.rightchildindex
    AND q.rightchildindex <= t2.rightchildindex);

    One can improve the performance of this operation by keeping the tree in both this alternate format and the widely used child-parent format simultaneously.

  • Insert a node as a child of i:
    UPDATE employee
    SET employee.nodeindex = employee.nodeindex + 1
    WHERE employee.nodeindex > parentnode;
    
    UPDATE employee
    SET employee.rightchildindex = employee.rightchildindex + 1
    WHERE employee.rightchildindex >= parentnode;
    
    INSERT INTO employee
    (nodeindex, rightchildindex, NAME, comments)
    VALUES (parentnode + 1, parentnode + 1, nodename, nodecomments);
  • Delete the node with NodeIndex i:
    DELETE employee
    WHERE employee.nodeindex = i;
    
    UPDATE employee
    SET employee.nodeindex = employee.nodeindex - 1
    WHERE employee.nodeindex > i;
    
    UPDATE employee
    SET employee.rightchildindex = employee.rightchildindex - 1
    WHERE employee.rightchildindex > i;

    The foreign key RightChildIndex referring to NodeIndex has to be a deferred one if the delete operation is to be implemented this way. That means that it has to be only enforced at the end of the transaction, and not during it.

Further study

We could also want to store the Breadth-First Traversal of the tree, but I haven't found any useful applications yet.

Tree archives or composites

The need to “archive” tree structures arises in some applications, especially those provide traceability for some tree structures. If a tree version that existed at some point in time is not frequently accessed, then some shorter encoding for the tree can be used to store in the database. I recommend the Prüfer-Like Codes. With most of these codes, the structure of a tree with N nodes is encoded in N – 2 integers in linear time. Be aware that these encodings only store the tree structure, and not the information in the nodes. That information has to be kept separately anyway.

A complete C# implementation of the algorithms descried in An encoding scheme for labeled trees is attached to the article. I've also made a Java implementation of the algorithm.

A general class TreeNode seems to be missing from the .NET framework (the one in Windows forms doesn't help me much), so I created one with the minimal functionality required.

public class TreeNode { 
public void Add(TreeNode child)... 
public TreeNode Parent... 
public int ChildCount... 
public IList Children...
}

The TreeEncoder class has to be initialized with a tree in the adjacency lists representation. The single method of interest is:

public int[] encode()...

It actually codes the tree with n nodes into an array of integers of n – 2 length.

The TreeDecoder class knows to decode a tree from it's compact representation

public IList[] decode()...
public int diameter()...
public int radius()...

and to compute the diameter and radius of a tree directly from it's code, without first reconstructing the tree into memory.

Conclusions

A broad set of operations on trees can be efficiently implemented using only basic tree traversal algorithms. It would be interesting to further study how the known tree encodings can be efficiently applied in relational databases.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Authors

Daniel Aioanei
Web Developer
Romania Romania
Member
No Biography provided

Adi Malinaru
Web Developer
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralExcelent articolmembercristim18 Jan '10 - 4:08 
Felicitari! Am cautat o modalitate optima pentru pastrarea arborilor in baza de date si a voastra e cea mai buna.
QuestionI lose dbo.GetTreeNodeKey in sql.txt. Where can i find it?memberadamsdigiark16 Oct '06 - 3:23 
I lose dbo.GetTreeNodeKey in sql.txt. Where can i find it?
Generaldeleting right most child node problem using alternate formatsussAnonymous6 Oct '05 - 6:16 
UPDATE employee
SET employee.rightchildindex = employee.rightchildindex - 1
WHERE employee.rightchildindex > i;
 
The above query does not update the rightchildindex of the parent when its right most child gets delete. The correct query seems to be:
 
UPDATE employee
SET employee.rightchildindex = employee.rightchildindex - 1
WHERE employee.rightchildindex >= i;
GeneralReparenting nodesmemberBharatN5 Aug '05 - 0:27 
Hi,
We have implemented the alternate format to create and delete the tree structure in our project. Now we have a need to reparent the existing nodes of the tree. For eg. In the example given, if I want to move node 3 from node 2 to node 6, how to implement this. We cannot delete nodes 3 and 4 and create them as children under 6 as the nodeids created earlier(for nodes 3 & 4) are used as foreign keys in another table.
 
Please help.

GeneralPopulate databasememberEmmanuelWilfart19 Aug '04 - 22:48 
Thanks for the quality of the article.
I have create a recursive fonction in c# to populate an IE webform treeview.
Do you want to say me who I can send the code perhaps to had (free) to your article.
GeneralRe: Populate databasememberDaniel Aioanei20 Aug '04 - 2:08 
If you want your code attached at this article, you can send it to me if you'd like.
QuestionError in Alternate Format SQL?membergloonie2 Aug '04 - 9:02 
I may be wrong, but isn't there an error in the alternate format (nodeindex, rightchildindex) SQL? If I define a tree where the rightmost child of the root (0) itself as a child, I think the subtree query will not work. As an example, if you ad a node 7 to your tree below 6. The then 7 will not be picked up.
AnswerRe: Error in Alternate Format SQL?memberDaniel Aioanei2 Aug '04 - 12:08 
RightChildIndex is not the proper name for this situation. I should have called it "RightmostDescendent" or even better, "LargestDescendent". In the case you describe, the RightmostDescendent(0) will be 7.
GeneralPerformancememberHaiko Burkhardt8 Jul '03 - 10:58 
Nice theory (elementary though)!
 
I have been working on a similar approach, and i have some remarks which relate to performance with updates/inserts.
 
Looking at the function it actually travels backwards to the root node collecting the 'path' from each node.
 
Looking at the trigger it updates the entire rowset both upwards and downward in respect to the context node.
 
I suggest:
1. The function needs only to merge the 'path' from the parent node with the context node, the parent node has the needed information.
2. The trigger needs only updating the nodes along its child axis.
 
This has some obvious implications, as the ID column needs to be autoincrementet and thus not updateable once inserted.
You need an recursive trigger in order to properly delete the node (and along the child axis) in my approach.
 
In a setup where you have multiple users accessing/updating the tree you will suffer an increasing performance loss as the tree grows, but with some modifications it can be reduced.
GeneralRe: PerformancesussAnonymous13 Oct '03 - 11:24 
Actaully this sql called SQL Trees. Its been done in almost every database langauge...
 
The first articles on this are referred on Joe Celko's Book , SQL for Smarties
 
And as for the recursive trigger thats not true. If you know how to do a select query.
 
The original query uses 2 integer based fields. Doing a cross join on the self containing table.
 
select P1.* from tb1 P1,tbl2 P2
where P2.LeftNode between P1.LeftNode and P2.RightNode
 
This is a up the stack look at the tree. where the top node is the node you placed the unique key on.
 
The exact opisite provides a top down .....
Using this in a delete query will provide you with the correct handling of your delete.
 
delete from tbl where ID in (select P1.* from tb1 P1,tbl2 P2
where P2.LeftNode between P1.LeftNode and P2.RightNode
and P2.ID = @ID)
 
There is you query.
 
Addressing statement 2 ...
Again this is not true. As the parent nodes get new numbers and then child nodes get deleted ... this creates gaps in the node indexing. You would be best to update the full set of nodes that are needed.
 
The preformance is true. But this can not be helped in larger tree structures. I would highly doubt that anyone would or should place more than a few thousand records in this structure. It is faster than you think but the management of this becomes a hastle ....
 
As the nodes get deeper away from the root ... the more the clean up functions have to be ran more often.
 

 

GeneralRe: PerformancememberHaiko Burkhardt13 Oct '03 - 12:09 
Now whether you think it is true or not, if you run a trace on inserts/updates you will know what i am talking about.
 
Back to your comments.
Conceptually you are in fact talking about a 'double ended linked list' in data structures terms. In that scenario you have to traverse backwards in order to update the parent nodes, yes.
 
But this is NOT the case in the solution provided. Here we have a 'single ended linked list'. The child node holds information about its parent node only, and thus you can update the tree - and only pay attention to nodes BELOW the context node. This is a fact.
 
What do you mean with it's faster than you think, actually i think the provided solution is SLOWER than i thinkSmile | :) If you calculate the complexity of the insert/update algoritm, and then suggest a FEW THOUSAND records in the provided solution - you are cracking me up.
 
Well boss, i just need to do a few thousand updates to change the name of one single node!!!
 
Let's get serious...
 
It is really a matter of perspective - tree based data strucures has been around for ages, and i've found that you have to pay special attention to performance in respect to implementing them in RDBMS, since this is NOT what RDBMS was meant for. Of course this will all be history when 'Yukon' arrives...Big Grin | :-D Big Grin | :-D
GeneralThe alternate methodmemberMauricio Ritter25 Jun '03 - 8:25 
Quite interesting alteranive method... but it requires A LOT of updates as the tree grows, specially if you insert nodes in the first level (you have to update almost the intire tree, which makes the solution unviable in a relational database model). Anyway... interesting approach... I'll try to work on it Big Grin | :-D
 
Mauricio Ritter - Brazil
Sonorking now: 100.13560 MRitter


English is not my native language so, if you find any spelling erros in my posts, please let me know.
GeneralInteresting Article, but...susssf_jeff25 Jun '03 - 6:50 
To be honest, I skipped past the original implementation and started with the Alternate implementation, as the idea of log2(x) self-joins to reach a tree leaf is unpalatable to me.
 
Anyway, it looks like there is a bug in the first query of the alternate section, as it won't return children of 5 if they existed, which doesn't match your description of the query (and doesn't match the fact that it returns 4.)
 
Sorry if I sound pessimistic. It's a good start.
 
Jeff
GeneralRe: Interesting Article, but...memberDaniel Aioanei25 Jun '03 - 7:27 
When I insert a node as a child of 5, the old 6 node becomes 7 and the new node becomes 6, and the results are:
 
NODEINDEX RIGHTCHILDINDEX
---------- ---------------
0 7
1 1
2 6
3 4
4 4
5 6
7 7
6 6
 
which seems to be correct. The query
 
SELECT t1.*
FROM EMPLOYEE t1, EMPLOYEE t2
WHERE 5 <= t1.nodeindex
AND t1.nodeindex <= t2.rightchildindex
AND t2.nodeindex = 5
 
correctly lists nodes 5 and 6. Could you please provide some further information about the bug you describe ? TIA
GeneralRe: Interesting Article, but...memberMauricio Ritter25 Jun '03 - 8:28 
The nodeindex and rightnode index changes as you insert data in the tree. Try do add a autonumber column in your model and you'll see that.

 
Mauricio Ritter - Brazil
Sonorking now: 100.13560 MRitter


English is not my native language so, if you find any spelling erros in my posts, please let me know.
GeneralNested Setssussohaullt24 Jun '03 - 22:11 
Have you also explore and compare it with "Nested Sets" theory ?
GeneralRe: Nested SetsmemberDaniel Aioanei25 Jun '03 - 3:20 
My approach is indeed very similar to the Nested Sets where the Left value is actually the NodeIndex and the Right value is the RightChildIndex. But it could also be interesting to study what extra information can a Breadth-First Traversal offer.
GeneralNeed another example !memberthaihung18 Jun '03 - 15:43 
Hi ,
It a great topic , I really love it .
Though all what I need is cotained , I have a bit more question !
I have a real proplem in my program ( it's a Human Resource Manage - project ). I implemented the organization of a company like a tree( one NODE can contain some staff and some child-NODE). See bellow :
 
NODE :
- ID
- PARENT_ID
- DESC
 
EMPLOYEE :
- ID
- NODE_ID
- DESC
 
I need implement a store-procedure to get all employee of a node (include the child-NODE's employee) but I can do that !
So could you ge me an advice !
 



GeneralRe: Need another example !memberDaniel Aioanei19 Jun '03 - 2:26 
I've substantially changed the article. Please see the Alternate Format section. If you still need help with your application, let me know.
GeneralAn example would help !memberMauricio Ritter18 Jun '03 - 4:17 
Interesting article ! Big Grin | :-D Very nice !
But you could post an example for your formula implementation... something like a tree structure and an step by step explanation of your function.
 
Mauricio Ritter - Brazil
Sonorking now: 100.13560 MRitter


English is not my native language so, if you find any spelling erros in my posts, please let me know.
GeneralRe: An example would help !memberDaniel Aioanei18 Jun '03 - 5:09 
I'll try to write some detailed instructions for a possible implementation as soon as I find some free time. I didn't bother with such an implementation from the beginning because I wanted to see if there is any interest for it. The other messages posted on this article will help me a lot Smile | :) Thank you all that are contributing your ideas and appreciation.
GeneralRe: An example would help !sussAnonymous12 Aug '05 - 2:18 
English is not my native language so, if you find any spelling ERRORS in my posts, please let me know
GeneralImplementation on SQL server 2000memberAlex Kucherenko16 Jun '03 - 21:03 
Sorry for not very clear code... I take it from real project and don't want to clear it...
 
Base idea:
- we have self joined table by fields: ProjectID && PrjParentRef
- Path field: PrjNodeKey
 
SQL code which create table and triggers which build Path automaticaly:

CREATE TABLE [Projects] (
[ProjectID] [int] IDENTITY (1, 1) NOT NULL ,
[PrjOwnerRef] [int] NULL ,
[PrjName] [dtUniName200] NOT NULL ,
[PrjCommRef] [int] NULL ,
[PrjParentRef] [int] NULL ,
[PrjTypeRef] [int] NOT NULL ,
[PrjPriorityRef] [int] NOT NULL ,
[PrjRequestRef] [int] NULL ,
[PrjManagerRef] [int] NOT NULL ,
[PrjCreationTime] [datetime] NULL ,
[PrjDisabled] [bit] NOT NULL CONSTRAINT [DF_Projects_PrjDisabled] DEFAULT (0),
[PrjNodeKey] [nvarchar] (100) COLLATE Latin1_General_CI_AI NULL ,
[PrjIsFolder] [bit] NOT NULL CONSTRAINT [DF_Projects_PrjIsFolder] DEFAULT (0),
CONSTRAINT [PK_Projects] PRIMARY KEY CLUSTERED
(
[ProjectID]
) WITH FILLFACTOR = 90 ON [PRIMARY] ,
CONSTRAINT [FK_Projects_Comments] FOREIGN KEY
(
[PrjCommRef]
) REFERENCES [Comments] (
[commID]
) ON UPDATE CASCADE ,
CONSTRAINT [FK_Projects_dicPriority] FOREIGN KEY
(
[PrjPriorityRef]
) REFERENCES [dicPriority] (
[PriorityID]
),
CONSTRAINT [FK_Projects_dicProjectType] FOREIGN KEY
(
[PrjTypeRef]
) REFERENCES [dicProjectType] (
[ProjectTypeID]
),
CONSTRAINT [FK_Projects_Persons1] FOREIGN KEY
(
[PrjOwnerRef]
) REFERENCES [Persons] (
[PersonID]
),
CONSTRAINT [FK_Projects_Projects] FOREIGN KEY
(
[PrjParentRef]
) REFERENCES [Projects] (
[ProjectID]
)
) ON [PRIMARY]

 

create TRIGGER tg_Projects
ON dbo.Projects
FOR INSERT, UPDATE
AS BEGIN
if TRIGGER_NESTLEVEL(@@PROCID) = 1
exec ispChangeEntityState @EntityName = 'Project'
END

 

create TRIGGER tg_Projects_NodeKey
ON dbo.Projects
FOR INSERT, UPDATE
AS BEGIN
if TRIGGER_NESTLEVEL(@@PROCID) = 1
if update(ProjectID) or update(PrjParentRef) or update(PrjNodeKey)
begin
update Projects set
PrjNodeKey = dbo.fnGetProjectNodeKey(ProjectID)
where isNull(PrjNodeKey,'') <> isNull(dbo.fnGetProjectNodeKey(ProjectID),'')
 
if exists(select * from Projects where PrjNodeKey is NULL) begin
raiserror('Recursion in Tasks', 16, 1)
rollback tran
return
end
end
END

 
Pathes will look like:

379 1004 ISL Bahamas 9721 NULL 915 1118 NULL 1004 2003-02-18 19:10:46.903 0 /0379 0
385 1041 ISL Webpayroll 9728 379 915 1118 NULL 1041 2003-02-18 19:39:55.777 0 /0379/0385 0
391 1041 TSM 9740 379 915 1118 NULL 1041 2003-02-18 19:56:19.510 0 /0379/0391 0

 
How we select Projects in from Table in "Tree Style" order

select * from Projects order by PrjNodeKey

 
How to select tree node from table (opimized method - faster then other one solutions):

select * from Projects Prj
inner join Projects subPrj on LEFT(subPrj.PrjNodeKey, LEN(Prj.PrjNodeKey)) = Prj.PrjNodeKey
where ... Here write filter expression ...

 
Good Luck
Alex Kucherenko
GeneralRe: Implementation on SQL server 2000memberAlex Kucherenko16 Jun '03 - 21:08 

exec ispChangeEntityState @EntityName = 'Project'

 
Is a empty stored procedure used for modifieng state of entity in database.
 
And here function which used for getting Key from table row

CREATE function dbo.fnGetProjectNodeKey(@ID int)
returns nvarchar(500)
as
begin
declare
@Key nvarchar(500),
@SubKey nvarchar(10),
@ParentID int
 
select @Key = N''
 
while( @ID is not NULL )
begin
select @ParentID = PrjParentRef from Projects where ProjectID = @ID
 
set @SubKey = N'/' + right(replicate('0',3) + cast(@ID as nvarchar(6)),4)
if( charindex(@SubKey + N'/', @Key) <> 0 )
return NULL
set @Key = @SubKey + @Key
set @ID = @ParentID
end
 
return @Key
end

 
Good Luck
Alex Kucherenko
GeneralRe: Implementation on SQL server 2000sussKris Williams17 Jun '03 - 17:52 
Nice. In the past I've seen an implementation that uses Hi and Lo numbers to represent a bird's-eye-view of the heirarchy. Very fast for treating a sub-tree as a set, instead of using cursors or whatnot to recurse them. I've modified your sample, and made it a full cut-past-run in query analyzer. I'll look for that hi-lo sample...
set nocount on
 
if exists (select * from sysobjects where id = object_id('Tree') and sysstat & 0xf = 3)
	drop table Tree
GO
 
if not exists (select * from dbo.sysobjects where id = object_id('dbo.Tree') and sysstat & 0xf = 3)
begin
create table Tree
(
	ID int identity (1, 1) not null
	,Parent int null
	,Name nvarchar (100) not null
	,CreateDate datetime null
	,Active bit not null constraint DF_Tree_Active default (1)
	,_NodeKey nvarchar (100) null
	,IsFolder bit not null constraint DF_Tree_IsFolder default (0)
	,constraint PK_Tree primary key  clustered 
	(
		ID
	)
)
end
 
go
 
if exists (select * from sysobjects where id = object_id('dbo.OnTreeNodeKey') and sysstat & 0xf = 8)
	drop trigger OnTreeNodeKey
GO
create trigger OnTreeNodeKey
on Tree
for insert, update
as 
begin
	if trigger_nestlevel(@@procid) = 1
		if update(ID) or update(Parent) or update(_NodeKey)
		begin
			update Tree set
			_NodeKey = dbo.GetTreeNodeKey(ID)
			where isNull(_NodeKey,'') <> isNull(dbo.GetTreeNodeKey(ID),'')
			
			if exists(select * from Tree where _NodeKey is null) 
			begin
				raiserror('Recursion in Nodes', 16, 1)
				rollback tran
				return
			end
		end
end
go
 
-- Fill the Table with sample data

declare @create_date datetime
set @create_date = getdate()
 
insert into Tree (Parent, Name, CreateDate)
	select null, 'root1', @create_date
 
declare @root int
set @root = @@identity
 
insert into Tree (Parent, Name, CreateDate)
	select @root, 'a', @create_date
insert into Tree (Parent, Name, CreateDate)
	select @@identity, 'b', @create_date
insert into Tree (Parent, Name, CreateDate)
	select @@identity, 'c', @create_date
insert into Tree (Parent, Name, CreateDate)
	select @root, 'd', @create_date
insert into Tree (Parent, Name, CreateDate)
	select @@identity, 'e', @create_date
insert into Tree (Parent, Name, CreateDate)
	select @@identity, 'f', @create_date
insert into Tree (Parent, Name, CreateDate)
	select @root, 'g', @create_date
insert into Tree (Parent, Name, CreateDate)
	select @@identity, 'h', @create_date
insert into Tree (Parent, Name, CreateDate)
	select null, 'root2', @create_date
 
set @root = @@identity
 
insert into Tree (Parent, Name, CreateDate)
	select @root, 'i', @create_date
insert into Tree (Parent, Name, CreateDate)
	select @@identity, 'j', @create_date
 
-- exclusive select of all sub-ordinates of root1

select s.Name, RIGHT(s._NodeKey, LEN(s._NodeKey)-6) NodePath from Tree t
inner join Tree s on LEFT(s._NodeKey, LEN(t._NodeKey)) = t._NodeKey
where t.Name = 'root1'
and s.Parent is not null
order by s._NodeKey
 
-- inclusive select of all sub-ordinates of root2

select s.Name, s._NodeKey NodePath from Tree t
inner join Tree s on LEFT(s._NodeKey, LEN(t._NodeKey)) = t._NodeKey
where t.Name = 'root2'
order by s._NodeKey
Enjoy! Cool | :cool:
 
-Kris

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

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130523.1 | Last Updated 24 Jun 2003
Article Copyright 2003 by Daniel Aioanei, Adi Malinaru
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid