Skip to content

Nested set model

2009 May 22
by Richard Knop

In the previous post I explained the adjacency list model and how to implement it in PHP. There is an alternative – the nested set model (also called modified preorder tree traversal). It is more complicated than the adjacency model but there is a reason why it might be better in certain situations:

  • unlimited hierarchy levels

On the other hand, there are reasons why I wouldn’t recommend using it unless you have to:

  • very complicated hierarchy modifications compared to the adjacency model (you often have to update almost the whole table)
  • a small mistake in PHP code for modifying the hierarchy can break up the whole table (you must test your application carefully before deploying it to the production environment)

Look at the picture to get a better idea about what the nested set model looks like:

Nested set model

Pay special attention to the blue numbers – based on these numbers you can calculate the position of an item in the hierarchy. The schema (‘lft’ and ‘rgt’ are used because ‘left’ and ‘right’ are MySQL reserved words):

CREATE TABLE categories (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL,
PRIMARY KEY (id)
) ENGINE = INNODB;

Let’s populate the table with some content we can work with:

INSERT INTO categories (id, name, lft, rgt) VALUES
(NULL, 'Relational databases', 1, 12),
(NULL, 'SQLite', 2, 3),
(NULL, 'MySQL', 4, 9),
(NULL, 'PDO', 5, 6),
(NULL, 'Transactions', 7, 8),
(NULL, 'PostgreSQL', 10, 11);

The query to select the full tree is simple:

SELECT * FROM categories ORDER BY lft

PHP implementation

Displaying the tree in an unordered XHTML list is not the hardest part. There are two important formulas:

rgt < last rgt => increment the hierarchy level (move one level down)
lft - last lft > 2 => decrement the hierarchy level (move one level up)

Therefor you can create the unordered list by keeping a track of left and right values:

  1. $user = 'root';
  2. $pass = '';
  3. $conn = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
  4.  
  5. $sql ='SELECT * FROM categories ORDER BY lft';
  6. $stmt = $conn->query($sql);
  7.  
  8. $categories = array();
  9. $left = array();
  10. $right = array();
  11. $level = 0;
  12. while ($row = $stmt->fetchObject()) {
  13.     if (empty($right) === false) {
  14.         $lastRight = end($right);
  15.         if ($lastRight > $row->rgt) {
  16.             $level = $level + 1;
  17.         }
  18.     }
  19.     if (empty($left) === false) {
  20.         $lastLeft = end($left);
  21.         if ($row->lft - $lastLeft > 2) {
  22.             $level = $level - 1;
  23.         }
  24.     }
  25.     $categories[] = array('id' => $row->id,
  26.                           'name' => $row->name,
  27.                           'level' => $level);
  28.     $left[] = $row->lft;
  29.     $right[] = $row->rgt;
  30. }
  31.  
  32. echo '<ul>', "\n";
  33. $count = count($categories);
  34. if ($count == 1) {
  35.     echo '<li>', $categories[0]['name'], '</li>', "\n";
  36. } else {
  37.     $i = 0;
  38.     while (isset($categories[$i])) {
  39.         echo '<li>', $categories[$i]['name'];
  40.         if ($i < $count - 1) {
  41.             if ($categories[$i + 1]['level'] > $categories[$i]['level'])
  42.             {
  43.                 echo '<ul>', "\n";
  44.             }
  45.             else {
  46.                 echo '</li>', "\n";
  47.             }
  48.             if ($categories[$i + 1]['level'] < $categories[$i]['level']) {
  49.                 echo str_repeat('</ul></li>' . "\n",
  50.                                 $categories[$i]['level'] - $categories[$i + 1]['level']);
  51.             }
  52.         } else {
  53.             echo '<li>', "\n";
  54.             echo str_repeat('</ul></li>' . "\n", $categories[$i]['level']);
  55.         }
  56.     $i++;
  57.     }
  58. }
  59. echo '</ul>', "\n";

Modifying the hierarchy

This is where it gets interesting. Modifying the hierarchy created by the nested set model or in other words adding, updating or removing branches (nodes) from the tree while keeping the hierarchy integrity.

Adding nodes

There are two ways how to add a new node – from the left and from the right. Both have very similar implementation (I will show you just the latter). Before I show you how to do it here is another important formula:

(rgt - lft - 1) / 2 = number of children nodes

In theory, what you need to do is add a new node and update all nodes to the right (those that have higher ‘rgt’ value). First, here is the algorithm in the pseudo language:

get a parent node

for all nodes
    if rgt >= parent->rgt
        rgt = rgt + 2
        if lft <> 1 and lft > parent->lft
            lft = lft + 2
        endif
    endif
endfor

add a new node:
    lft = parent->lft + number of children nodes * 2 + 1
    rgt = lft + 1

Here is a function:

function addNodeFromRight($conn, $parentId, $name) {
  1.     try {
  2.         $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  3.         $conn->beginTransaction();
  4.  
  5.         $sql = 'SELECT * FROM categories WHERE id = ?';
  6.         $stmt = $conn->prepare($sql);
  7.         $stmt->execute(array($parentId));
  8.         $parent = $stmt->fetchObject();
  9.  
  10.         $sql = 'UPDATE categories SET rgt = rgt + 2 WHERE rgt >= ?';
  11.         $stmt = $conn->prepare($sql);
  12.         $stmt->execute(array($parent->rgt));
  13.  
  14.         $sql = 'UPDATE categories SET lft = lft + 2 WHERE rgt >= ? AND lft <> 1 AND lft > ?';
  15.         $stmt = $conn->prepare($sql);
  16.         $stmt->execute(array($parent->rgt, $parent->lft));
  17.  
  18.         $sql = 'INSERT INTO categories (name, lft, rgt) VALUES (?, ?, ?)';
  19.         $stmt = $conn->prepare($sql);
  20.         $numberOfChildren = ($parent->rgt - $parent->lft - 1) / 2;
  21.         $lft = $parent->lft + $numberOfChildren * 2 + 1;
  22.         $rgt = $lft + 1;
  23.         $stmt->execute(array($name, $lft, $rgt));
  24.  
  25.         $conn->commit();
  26.     } catch (PDOException $e) {
  27.         $conn->rollback();
  28.         echo $e->getMessage();
  29.     }
  30. }

Removing nodes

Very similar to adding nodes but you must also take special care of children branches in case you want to remove a parent node. In the pseudo language:

get a node

if node->lft <> 1

    delete the node

    for all nodes
        if rgt > node->rgt
            rgt = rgt - 2
        endif
        if rgt > node->rgt and lft <> 1 and lft > node->lft
            lft = lft - 2
        endif
        if rgt < node->rgt and lft > node->lft
            lft = lft - 1
            rgt = rgt - 1
        endif
    endfor

endif

And here’s how it could look in PHP:

function removeNode($conn, $id) {
  1.     try {
  2.         $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  3.         $conn->beginTransaction();
  4.  
  5.         $sql = 'SELECT * FROM categories WHERE id = ?';
  6.         $stmt = $conn->prepare($sql);
  7.         $stmt->execute(array($id));
  8.         $node = $stmt->fetchObject();
  9.  
  10.         if ($node->lft <> 1) {
  11.  
  12.             $sql = 'DELETE FROM categories WHERE id = ?';
  13.             $stmt = $conn->prepare($sql);
  14.             $stmt->execute(array($id));
  15.  
  16.             $sql = 'UPDATE categories SET rgt = rgt – 2 WHERE rgt > ?';
  17.             $stmt = $conn->prepare($sql);
  18.             $stmt->execute(array($node->rgt));
  19.  
  20.             $sql = 'UPDATE categories SET lft = lft – 2 WHERE rgt > ? AND lft <> 1 AND lft > ?';
  21.             $stmt = $conn->prepare($sql);
  22.             $stmt->execute(array($node->rgt, $node->lft));
  23.  
  24.             $sql = 'UPDATE categories SET lft = lft – 1, rgt = rgt – 1 WHERE rgt < ? AND lft > ?';
  25.             $stmt = $conn->prepare($sql);
  26.             $stmt->execute(array($node->rgt, $node->lft));
  27.  
  28.         }
  29.  
  30.         $conn->commit();
  31.     } catch (PDOException $e) {
  32.         $conn->rollback();
  33.         echo $e->getMessage();
  34.     }
  35. }

Note that I made sure that lft is not equal to 1 before removing the node. You cannot remove the top node or else the whole tree hierarchy will break up.

Updating nodes

You might have already guessed it – to update a node you can combine addNodeFromRight() and removeNode() functions.

That’s not the most efficient way to do it though. I’m too tired to think about the algorithm now but I’m sure you can figure it out after reading this post.

A small trick

Most of the time you will need a hierarchy with more separate trees (or subhierarchies) which would be much more complicated to achieve. There’s a small trick you can use though. Just don’t display the top node (it could be named ‘Categories’ in this case) and decrease the hierarchy level in the script by 1.

7 Responses leave one →
  1. July 18, 2009

    Thanks a ton for this article! I really appreciate the simplicity of your examples. Taking the knowledge here and applying it to Zend_Db_Table models was very simple.

  2. December 16, 2009

    Thanks a lot too!
    Nice stuff to get the code-idea of this Algorithm :-)
    There are three notes that I want to make:

    1) Seems that this condition is not needed: AND lft > ?
    In fact, when you see it as a whole: rgt > ? AND lft > ?
    it’s clear that rgt > ? does the job alone :-)

    2) I was expecting that removeNode removes the whole branch (if has any subnodes) but your script does not. It remove the parent node, but leaves any child at its previous level. That was unexpected to me

    3) Because 2, I’m afraid that you can’t use addNodeFromRight then removeNode to update a node :-) Just get the ID and update anything that you want to update to the node :-P

    Greetings and thanks again

  3. December 16, 2009

    removeNode() removes the node and if it has any children nodes it moves them all one level up, so the hierarchy integrity is not disturbed. That seemed the most logical to me.

    It’s easy to edit it so it removes also all children nodes though :)

  4. Jaimin permalink
    June 25, 2011

    Hi,

    First of all awesome article.. i want to use the same approach but my problem is :

    In my application multiple users are inserting nodes to the same tree. and later they can access their own tree in their myaccount panel. so how do i achieve this. I cannot update the master categories table so i have to keep the separate table but i am not getting how to manage this.

    I am thinking of following structure.

    others table:
    id PK,
    user_id FK,
    Other_value, (suppose i insert “xyz” in the other than that name will come here )
    node_id (this will be the id of the other node in the categories tree)

    So will i be able to generate the tree in their myaccount panel…
    Please comment your ideas..

    Thanks

Trackbacks and Pingbacks

  1. Adjacency list model | Richard Knop's Blog
  2. Back online | Zend Framework Blog Written By Richard Knop
  3. Merge sort PHP implementation | Zend Framework Blog

Leave a Reply

Note: You can use basic XHTML in your comments. Your email address will never be published.

Subscribe to this comment feed via RSS