MySQL recursive tree search - php

I have a database with a tree of names that can go down a total of 9 levels deep and I need to be able to search down a signal branch of the tree from any point on the branch.
Database:
+----------------------+
| id | name | parent |
+----------------------+
| 1 | tom | 0 |
| 2 | bob | 0 |
| 3 | fred | 1 |
| 4 | tim | 2 |
| 5 | leo | 4 |
| 6 | sam | 4 |
| 7 | joe | 6 |
| 8 | jay | 3 |
| 9 | jim | 5 |
+----------------------+
Tree:
tom
fred
jay
bob
tim
sam
joe
leo
jim
For example:
If I search "j" from the user "bob" I should get only "joe" and "jim". If I search "j" form "leo" I should only get "jim".
I can't think of any easy way do to this so any help is appreciated.

You should really consider using the Modified Preorder Tree Traversal which makes such queries much easier. Here's your table expressed with MPTT. I have left the parent field, as it makes some queries easier.
+----------------------+-----+------+
| id | name | parent | lft | rght |
+----------------------+-----+------+
| 1 | tom | 0 | 1 | 6 |
| 2 | bob | 0 | 7 | 18 |
| 3 | fred | 1 | 2 | 5 |
| 4 | tim | 2 | 8 | 17 |
| 5 | leo | 4 | 12 | 15 |
| 6 | sam | 4 | 9 | 16 |
| 7 | joe | 6 | 10 | 11 |
| 8 | jay | 3 | 3 | 4 |
| 9 | jim | 5 | 13 | 14 |
+----------------------+-----+------+
To search j from user bob you'd use the lft and rght values for bob:
SELECT * FROM table WHERE name LIKE 'j%' AND lft > 7 AND rght < 18
Implementing the logic to update lft and rght for adding, removing and reordering nodes can be a challenge (hint: use an existing library if you can) but querying will be a breeze.

There isn't a nice/easy way of doing this; databases don't support tree-style data structures well.
You will need to work on a level-by-level basis to prune results from child-to-parent, or create a view that gives all 9 generations from a given node, and match using an OR on the descendants.

Have you thought about using a recursive loop? i use a loop for a cms i built on top of codeigniter that allows me to start anywhere in the site tree and will then subsequently filter trhough all the children> grand children > great grand children etc. Plus it keeps the sql down to short rapid queries opposed to lots of complicated joins. It may need some modifying in your case but i think it could work.
/**
* build_site_tree
*
* #return void
* #author Mike Waites
**/
public function build_site_tree($parent_id)
{
return $this->find_children($parent_id);
}
/** end build_site_tree **/
// -----------------------------------------------------------------------
/**
* find_children
* Recursive loop to find parent=>child relationships
*
* #return array $children
* #author Mike Waites
**/
public function find_children($parent_id)
{
$this->benchmark->mark('find_children_start');
if(!class_exists('Account_model'))
$this->load->model('Account_model');
$children = $this->Account_model->get_children($parent_id);
/** Recursively Loop over the results to build the site tree **/
foreach($children as $key => $child)
{
$childs = $this->find_children($child['id']);
if (count($childs) > 0)
$children[$key]['children'] = $childs;
}
return $children;
$this->benchmark->mark('find_children_end');
}
/** end find_children **/
As you can see this is a pretty simplfied version and bear in mind this has been built into codeigniter so you will need to modyfy it to suite but basically we have a loop that calls itself adding to an array each time as it goes. This will allow you to get the whole tree, or even start from a point in the tree as long as you have the parent_id avaialble first!
Hope this helps

The new "recursive with" construct will do the job, but I don't know id MySQL supports it (yet).
with recursive bobs(id) as (
select id from t where name = 'bob'
union all
select t.id from t, bobs where t.parent_id = bobs.id
)
select t.name from t, bobs where t.id = bobs.id
and name like 'j%'

There is no single SQL query that will return the data in tree format - you need processing to traverse it in the right order.
One way is to query MySQL to return MPTT:
SELECT * FROM table ORDER BY parent asc;
root of the tree will be the first item of the table, its children will be next, etc., the tree being listed "breadth first" (in layers of increasing depth)
Then use PHP to process the data, turning it into an object that holds the data structure.
Alternatively, you could implement MySQL search functions that given a node, recursively search and return a table of all its descendants, or a table of all its ancestors. As these procedures tend to be slow (being recursive, returning too much data that is then filtered by other criteria), you want to only do this if you know you're not querying for that kind of data again and again, or if you know that the data set remains small (9 levels deep and how wide?)

You can do this with a stored procedure as follows:
Example calls
mysql> call names_hier(1, 'a');
+----+----------+--------+-------------+-------+
| id | emp_name | parent | parent_name | depth |
+----+----------+--------+-------------+-------+
| 2 | ali | 1 | f00 | 1 |
| 8 | anna | 6 | keira | 4 |
+----+----------+--------+-------------+-------+
2 rows in set (0.00 sec)
mysql> call names_hier(3, 'k');
+----+----------+--------+-------------+-------+
| id | emp_name | parent | parent_name | depth |
+----+----------+--------+-------------+-------+
| 6 | keira | 5 | eva | 2 |
+----+----------+--------+-------------+-------+
1 row in set (0.00 sec)
$sqlCmd = sprintf("call names_hier(%d,'%s')", $id, $name); // dont forget to escape $name
$result = $db->query($sqlCmd);
Full script
drop table if exists names;
create table names
(
id smallint unsigned not null auto_increment primary key,
name varchar(255) not null,
parent smallint unsigned null,
key (parent)
)
engine = innodb;
insert into names (name, parent) values
('f00',null),
('ali',1),
('megan',1),
('jessica',3),
('eva',3),
('keira',5),
('mandy',6),
('anna',6);
drop procedure if exists names_hier;
delimiter #
create procedure names_hier
(
in p_id smallint unsigned,
in p_name varchar(255)
)
begin
declare v_done tinyint unsigned default(0);
declare v_dpth smallint unsigned default(0);
set p_name = trim(replace(p_name,'%',''));
create temporary table hier(
parent smallint unsigned,
id smallint unsigned,
depth smallint unsigned
)engine = memory;
insert into hier select parent, id, v_dpth from names where id = p_id;
/* http://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html */
create temporary table tmp engine=memory select * from hier;
while not v_done do
if exists( select 1 from names n inner join tmp on n.parent = tmp.id and tmp.depth = v_dpth) then
insert into hier select n.parent, n.id, v_dpth + 1
from names n inner join tmp on n.parent = tmp.id and tmp.depth = v_dpth;
set v_dpth = v_dpth + 1;
truncate table tmp;
insert into tmp select * from hier where depth = v_dpth;
else
set v_done = 1;
end if;
end while;
select
n.id,
n.name as emp_name,
p.id as parent,
p.name as parent_name,
hier.depth
from
hier
inner join names n on hier.id = n.id
left outer join names p on hier.parent = p.id
where
n.name like concat(p_name, '%');
drop temporary table if exists hier;
drop temporary table if exists tmp;
end #
delimiter ;
-- call this sproc from your php
call names_hier(1, 'a');
call names_hier(3, 'k');

Related

How SELECT separate | column mysql php (1|0|2|1|0|0|1)

I have column student_hobbys in my mysql table:
student_hobbys
1|0|1|1|0|0|1|0
now i try search all students who likes geography (school subject). In column student_hobbys i save this value in second argument (1|here|0|1|...). How to create a mysql query that will select students who like geography?
please help
Please read here why your design is a bad idea. You should store you data in a normalized way as follows:
table studens:
id | name
1 | John
2 | Jane
3 | Mike
4 | Spike
table hobbies:
id | name
1 | biology
2 | geogryphy
3 | football
4 | programming
table students_hobbies:
student_id | hobby_id
1 | 3
1 | 4
2 | 1
2 | 2
3 | 1
3 | 2
3 | 4
4 | 3
Schema definition:
CREATE TABLE students (
id INT UNSIGNED AUTO_INCREMENT,
name VARCHAR(50),
PRIMARY KEY (id),
INDEX (name)
);
CREATE TABLE hobbies (
id INT UNSIGNED AUTO_INCREMENT,
name VARCHAR(50),
PRIMARY KEY (id),
INDEX (name)
);
CREATE TABLE students_hobbies (
student_id INT UNSIGNED,
hobby_id INT UNSIGNED,
PRIMARY KEY (student_id, hobby_id),
INDEX (hobby_id, student_id)
);
And your SELECT query now would be:
SELECT s.*
FROM students s
JOIN students_hobbies sh ON sh.student_id = s.id
JOIN hobbies h ON h.id = sh.hobby_id
WHERE h.name = 'geogryphy';
Result:
| id | name |
| --- | ---- |
| 2 | Jane |
| 3 | Mike |
View on DB Fiddle
However - If you want to stick with your design, you can try something like this:
SELECT *
FROM students
WHERE student_hobbys LIKE '_|1%'
View on DB Fiddle
But it would be quite complex to generate this query programmatically. It will probably also be slower than the above solution on big dada sets, because there is no way to use an index for this kind of query.
If you want to avoid complex code in your application, you will need a quite more comlex query. One way would be to convert your string to a bitmask, and then use the bit operator & to check the bit at a specific position:
SET #hobby_position = 2;
SELECT *
FROM students
WHERE CONV(REVERSE(REPLACE(student_hobbys, '|', '')), 2, 10) & 1 << (#hobby_position - 1);
| id | name | student_hobbys |
| --- | ---- | --------------- |
| 2 | Jane | 1|1|1|1|0|0|1|0 |
| 3 | Mike | 1|1|1|1|0|0|1|0 |
View on DB Fiddle
There are other ways - But you will unlikely find a simple one, which can work with your design.

How to list all the users recorded under left or right side of a sponsor user [duplicate]

i have this case using recursive query on Mysql to find lv 2 and lv3 child on one table...
database structure i'm using:
id name parent
1 A 0
2 B 0
3 C 0
4 D 1
5 E 1
6 F 2
7 G 2
8 H 3
9 I 3
10 J 4
11 K 4
The result i was expecting, when filtering the data, where id=1, it will generate the result i'm expecting.
id name parent
4 D 1
5 E 1
10 J 4
11 K 4
or this is the illustration.
i've been looking everywhere, and reading this http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/, but i didn't find the result i was looking for..any help would be appreciated, thanks
if you want to get all level child of a particular parent then you should try this
select id,
name,
parent
from (select * from tablename
order by parent, id) tablename,
(select #pv := '1') initialisation
where find_in_set(parent, #pv) > 0
and #pv := concat(#pv, ',', id)
In my opinion the key to finding all children in a hierarchical table structure is to first find the path to the parent, and then use FIND_IN_SET to see if the requested node is in the path. It's easier and more efficient to search up than down, because the link to the parent is already there in the table.
So let's start with a hierarchy like this:
1 Pets
├─ 2 Dogs
│ ├─ 3 Katie
├─ 4 Cats
│ ├─ 5 George
│ ├─ 6 Pete
│ ├─ 7 Alice
├─ 8 Other
│ ├─ 9 Rabbits
│ │ ├─ 10 Noah
│ │ ├─ 11 Teddy
│ │ ├─ 12 Bella
│ ├─ 13 Rats
│ │ ├─ 14 Henry
And now you want to find all children under the category Other (with the category included) then the expected result would be:
8,9,10,11,12,13,14
Now let's have a look at the hierarchical path of Henry. The parent for Henry (14) is Rats (13), which has parent Other (8) and finally Pets (1). If we use the ID's to make a path for Henry it would look like this:
1,8,13,14
And this is where the MySQL function FIND_IN_SET kicks in. With FIND_IN_SET you can filter results where a variable can be found in a comma separated list. In this example we are looking for all children in the category Other (8) and we can simply use FIND_IN_SET(8, path).
To get the path for a hierarchical table I would like to refer to my answer in the post here MySql: ORDER BY parent and child. We will just change the dash to a comma, so we can use the FIND_IN_SET function.
The example above would look like this in a hierarchical table:
+----+--------+---------+
| id | parent | name |
+----+--------+---------+
| 1 | NULL | Pets |
| 2 | 1 | Dogs |
| 3 | 2 | Katie |
| 4 | 1 | Cats |
| 5 | 4 | George |
| 6 | 4 | Pete |
| 7 | 4 | Alice |
| 8 | 1 | Other |
| 9 | 8 | Rabbits |
| 10 | 9 | Noah |
| 11 | 9 | Teddy |
| 12 | 9 | Bella |
| 13 | 8 | Rats |
| 14 | 13 | Henry |
+----+--------+---------+
In my approach I will use a procedure that will recursively call itself and keep prepending the path with the parent of the requested id until it reaches the NULL parent.
DELIMITER $$
CREATE DEFINER=`root`#`localhost` PROCEDURE `PATH`(IN `input` INT, OUT `output` VARCHAR(128))
BEGIN
DECLARE _id INT;
DECLARE _parent INT;
DECLARE _path VARCHAR(128);
SET `max_sp_recursion_depth` = 50;
SELECT `id`, `parent`
INTO _id, _parent
FROM `database`.`table`
WHERE `table`.`id` = `input`;
IF _parent IS NULL THEN
SET _path = _id;
ELSE
CALL `PATH`(_parent, _path);
SELECT CONCAT(_path, ',', _id) INTO _path;
END IF;
SELECT _path INTO `output`;
END $$
DELIMITER ;
We need these results in a SELECT query so we need a FUNCTION too that wraps the results of the PROCEDURE.
DELIMITER $$
CREATE DEFINER=`root`#`localhost` FUNCTION `GETPATH`(`input` INT) RETURNS VARCHAR(128)
BEGIN
CALL `PATH`(`input`, #path);
RETURN #path;
END $$
DELIMITER ;
Now we can use the path in a query. On a table with 10000 rows it takes just over a second on my workstation.
SELECT `id`, `parent`, `name`, GETPATH(`id`) `path` FROM `database`.`table`;
Example output:
+----+--------+---------+-----------+
| id | parent | name | path |
+----+--------+---------+-----------+
| 1 | NULL | Pets | 1 |
| 2 | 1 | Dogs | 1,2 |
| 3 | 2 | Katie | 1,2,3 |
| 4 | 1 | Cats | 1,4 |
| 5 | 4 | George | 1,4,5 |
| 6 | 4 | Pete | 1,4,6 |
| 7 | 4 | Alice | 1,4,7 |
| 8 | 1 | Other | 1,8 |
| 9 | 8 | Rabbits | 1,8,9 |
| 10 | 9 | Noah | 1,8,9,10 |
| 11 | 9 | Teddy | 1,8,9,11 |
| 12 | 9 | Bella | 1,8,9,12 |
| 13 | 8 | Rats | 1,8,13 |
| 14 | 13 | Henry | 1,8,13,14 |
+----+--------+---------+-----------+
And to find all children of Other (8) with Other itself also included we can use the same query and filter with FIND_IN_SET:
SELECT `id`, `parent`, `name`, GETPATH(`id`) `path` FROM `database`.`table` WHERE FIND_IN_SET(8, GETPATH(`id`));
And finally here is the result. We put a recursion limit of 50 levels in the procedure, but besides that we have no limit in the depth.
+----+--------+---------+-----------+
| id | parent | name | path |
+----+--------+---------+-----------+
| 8 | 1 | Other | 1,8 |
| 9 | 8 | Rabbits | 1,8,9 |
| 10 | 9 | Noah | 1,8,9,10 |
| 11 | 9 | Teddy | 1,8,9,11 |
| 12 | 9 | Bella | 1,8,9,12 |
| 13 | 8 | Rats | 1,8,13 |
| 14 | 13 | Henry | 1,8,13,14 |
+----+--------+---------+-----------+
7 rows in set (0,01 sec)
If you want to have a single value instead of rows then you might want to use GROUP_CONCAT like this:
SELECT GROUP_CONCAT(`id`) `children` FROM `database`.`table` WHERE FIND_IN_SET(8, GETPATH(`id`));
Giving you the following result:
+--------------------+
| children |
+--------------------+
| 8,9,10,11,12,13,14 |
+--------------------+
1 row in set (0,00 sec)
Try this one , much simple and easy to understand.
(but supports only one hierarchy level)
SET #pv = 1;
select * from tablename
where FIND_IN_SET(parentrecordID,#pv) and !isnull(#pv:= concat(#pv, ',', id));
I tried this one
select id from (select * from roles order by parent_role, id) roles,(select #pv := '1') initialisation
where find_in_set(parent_role, #pv) > 0
and #pv := concat(#pv, ',', id)
but it works for depth 2 only, i need to make it work with more depth since i have 8 levels
Thanks #Manoj Rana
your solution is really helped me a lot.
But I want to use this solution in Hibernate createNativeQuery(); function.
Because of := operator I can't able to use. So I prepared new stored procedure using your solution and use it in my code.
You can find The stored procedure which I have created in this link
The answer you are looking for can be this;
https://github.com/ersengultepe/mysql_hierarchy_recursive_procedure/
DROP PROCEDURE IF EXISTS store_procedure_name;
CREATE PROCEDURE `store_procedure_name`(IN cat_id INT)
BEGIN
declare loopId Int;
SET max_sp_recursion_depth = 255;
-- If the value of the category that comes as a parameter is not in the table as parent_id, no further action is required
IF(select count(id) from category_table where parent_id=cat_id) > 0 THEN
-- create temporary table
CREATE TEMPORARY TABLE IF NOT EXISTS temp_category_table (
`id` smallint(5) unsigned,
`status` tinyint(3)
) ENGINE=InnoDB ;
-- First, save the corresponding value in the temporary table.
INSERT INTO temp_category_table
(id, status)
VALUES (cat_id, 0);
-- continue loop as long as the appropriate record exists in the temporary table
WHILE (select count(id) from temp_category_table where status=0) > 0 DO
-- in this section, a record with a status of 0 will be extracted from the temporary table and assigned to the variable loopId
set loopId = (select id from temp_category_table where status=0 limit 1);
INSERT INTO temp_category_table
(id, status)
(select id, 0 from category_table where parent_id=loopId);
update temp_category_table set status=1 where id=loopId;
CALL store_procedure_name((select id from temp_category_table where status=0 limit 1));
END WHILE;
(select DISTINCT(id) from temp_category_table order by id );
END IF;
END;
Try this, Much faster
SELECT *
FROM table AS T1
INNER JOIN (SELECT id FROM table WHERE parent = 1) AS T2
ON T2.id = T1.parent OR T1.parent = 1
GROUP BY T1.id
SELECT *
FROM TABLENAME
WHERE PARENT = 1
UNION
SELECT *
FROM TABLENAME
WHERE PARENT IN
(SELECT ID FROM TABLENAME WHERE PARENT = 1)

Mysql - order tree [duplicate]

Assume you have a flat table that stores an ordered tree hierarchy:
Id Name ParentId Order
1 'Node 1' 0 10
2 'Node 1.1' 1 10
3 'Node 2' 0 20
4 'Node 1.1.1' 2 10
5 'Node 2.1' 3 10
6 'Node 1.2' 1 20
Here's a diagram, where we have [id] Name. Root node 0 is fictional.
[0] ROOT
/ \
[1] Node 1 [3] Node 2
/ \ \
[2] Node 1.1 [6] Node 1.2 [5] Node 2.1
/
[4] Node 1.1.1
What minimalistic approach would you use to output that to HTML (or text, for that matter) as a correctly ordered, correctly indented tree?
Assume further you only have basic data structures (arrays and hashmaps), no fancy objects with parent/children references, no ORM, no framework, just your two hands. The table is represented as a result set, which can be accessed randomly.
Pseudo code or plain English is okay, this is purely a conceptional question.
Bonus question: Is there a fundamentally better way to store a tree structure like this in a RDBMS?
EDITS AND ADDITIONS
To answer one commenter's (Mark Bessey's) question: A root node is not necessary, because it is never going to be displayed anyway. ParentId = 0 is the convention to express "these are top level". The Order column defines how nodes with the same parent are going to be sorted.
The "result set" I spoke of can be pictured as an array of hashmaps (to stay in that terminology). For my example was meant to be already there. Some answers go the extra mile and construct it first, but thats okay.
The tree can be arbitrarily deep. Each node can have N children. I did not exactly have a "millions of entries" tree in mind, though.
Don't mistake my choice of node naming ('Node 1.1.1') for something to rely on. The nodes could equally well be called 'Frank' or 'Bob', no naming structure is implied, this was merely to make it readable.
I have posted my own solution so you guys can pull it to pieces.
Now that MySQL 8.0 supports recursive queries, we can say that all popular SQL databases support recursive queries in standard syntax.
WITH RECURSIVE MyTree AS (
SELECT * FROM MyTable WHERE ParentId IS NULL
UNION ALL
SELECT m.* FROM MyTABLE AS m JOIN MyTree AS t ON m.ParentId = t.Id
)
SELECT * FROM MyTree;
I tested recursive queries in MySQL 8.0 in my presentation Recursive Query Throwdown in 2017.
Below is my original answer from 2008:
There are several ways to store tree-structured data in a relational database. What you show in your example uses two methods:
Adjacency List (the "parent" column) and
Path Enumeration (the dotted-numbers in your name column).
Another solution is called Nested Sets, and it can be stored in the same table too. Read "Trees and Hierarchies in SQL for Smarties" by Joe Celko for a lot more information on these designs.
I usually prefer a design called Closure Table (aka "Adjacency Relation") for storing tree-structured data. It requires another table, but then querying trees is pretty easy.
I cover Closure Table in my presentation Models for Hierarchical Data with SQL and PHP and in my book SQL Antipatterns Volume 1: Avoiding the Pitfalls of Database Programming.
CREATE TABLE ClosureTable (
ancestor_id INT NOT NULL REFERENCES FlatTable(id),
descendant_id INT NOT NULL REFERENCES FlatTable(id),
PRIMARY KEY (ancestor_id, descendant_id)
);
Store all paths in the Closure Table, where there is a direct ancestry from one node to another. Include a row for each node to reference itself. For example, using the data set you showed in your question:
INSERT INTO ClosureTable (ancestor_id, descendant_id) VALUES
(1,1), (1,2), (1,4), (1,6),
(2,2), (2,4),
(3,3), (3,5),
(4,4),
(5,5),
(6,6);
Now you can get a tree starting at node 1 like this:
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;
The output (in MySQL client) looks like the following:
+----+
| id |
+----+
| 1 |
| 2 |
| 4 |
| 6 |
+----+
In other words, nodes 3 and 5 are excluded, because they're part of a separate hierarchy, not descending from node 1.
Re: comment from e-satis about immediate children (or immediate parent). You can add a "path_length" column to the ClosureTable to make it easier to query specifically for an immediate child or parent (or any other distance).
INSERT INTO ClosureTable (ancestor_id, descendant_id, path_length) VALUES
(1,1,0), (1,2,1), (1,4,2), (1,6,1),
(2,2,0), (2,4,1),
(3,3,0), (3,5,1),
(4,4,0),
(5,5,0),
(6,6,0);
Then you can add a term in your search for querying the immediate children of a given node. These are descendants whose path_length is 1.
SELECT f.*
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
AND path_length = 1;
+----+
| id |
+----+
| 2 |
| 6 |
+----+
Re comment from #ashraf: "How about sorting the whole tree [by name]?"
Here's an example query to return all nodes that are descendants of node 1, join them to the FlatTable that contains other node attributes such as name, and sort by the name.
SELECT f.name
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
ORDER BY f.name;
Re comment from #Nate:
SELECT f.name, GROUP_CONCAT(b.ancestor_id order by b.path_length desc) AS breadcrumbs
FROM FlatTable f
JOIN ClosureTable a ON (f.id = a.descendant_id)
JOIN ClosureTable b ON (b.descendant_id = a.descendant_id)
WHERE a.ancestor_id = 1
GROUP BY a.descendant_id
ORDER BY f.name
+------------+-------------+
| name | breadcrumbs |
+------------+-------------+
| Node 1 | 1 |
| Node 1.1 | 1,2 |
| Node 1.1.1 | 1,2,4 |
| Node 1.2 | 1,6 |
+------------+-------------+
A user suggested an edit today. SO moderators approved the edit, but I am reversing it.
The edit suggested that the ORDER BY in the last query above should be ORDER BY b.path_length, f.name, presumably to make sure the ordering matches the hierarchy. But this doesn't work, because it would order "Node 1.1.1" after "Node 1.2".
If you want the ordering to match the hierarchy in a sensible way, that is possible, but not simply by ordering by the path length. For example, see my answer to MySQL Closure Table hierarchical database - How to pull information out in the correct order.
If you use nested sets (sometimes referred to as Modified Pre-order Tree Traversal) you can extract the entire tree structure or any subtree within it in tree order with a single query, at the cost of inserts being more expensive, as you need to manage columns which describe an in-order path through thee tree structure.
For django-mptt, I used a structure like this:
id parent_id tree_id level lft rght
-- --------- ------- ----- --- ----
1 null 1 0 1 14
2 1 1 1 2 7
3 2 1 2 3 4
4 2 1 2 5 6
5 1 1 1 8 13
6 5 1 2 9 10
7 5 1 2 11 12
Which describes a tree which looks like this (with id representing each item):
1
+-- 2
| +-- 3
| +-- 4
|
+-- 5
+-- 6
+-- 7
Or, as a nested set diagram which makes it more obvious how the lft and rght values work:
__________________________________________________________________________
| Root 1 |
| ________________________________ ________________________________ |
| | Child 1.1 | | Child 1.2 | |
| | ___________ ___________ | | ___________ ___________ | |
| | | C 1.1.1 | | C 1.1.2 | | | | C 1.2.1 | | C 1.2.2 | | |
1 2 3___________4 5___________6 7 8 9___________10 11__________12 13 14
| |________________________________| |________________________________| |
|__________________________________________________________________________|
As you can see, to get the entire subtree for a given node, in tree order, you simply have to select all rows which have lft and rght values between its lft and rght values. It's also simple to retrieve the tree of ancestors for a given node.
The level column is a bit of denormalisation for convenience more than anything and the tree_id column allows you to restart the lft and rght numbering for each top-level node, which reduces the number of columns affected by inserts, moves and deletions, as the lft and rght columns have to be adjusted accordingly when these operations take place in order to create or close gaps. I made some development notes at the time when I was trying to wrap my head around the queries required for each operation.
In terms of actually working with this data to display a tree, I created a tree_item_iterator utility function which, for each node, should give you sufficient information to generate whatever kind of display you want.
More info about MPTT:
Trees in SQL
Storing Hierarchical Data in a Database
Managing Hierarchical Data in MySQL
It's a quite old question, but as it's got many views I think it's worth to present an alternative, and in my opinion very elegant, solution.
In order to read a tree structure you can use recursive Common Table Expressions (CTEs). It gives a possibility to fetch whole tree structure at once, have the information about the level of the node, its parent node and order within children of the parent node.
Let me show you how this would work in PostgreSQL 9.1.
Create a structure
CREATE TABLE tree (
id int NOT NULL,
name varchar(32) NOT NULL,
parent_id int NULL,
node_order int NOT NULL,
CONSTRAINT tree_pk PRIMARY KEY (id),
CONSTRAINT tree_tree_fk FOREIGN KEY (parent_id)
REFERENCES tree (id) NOT DEFERRABLE
);
insert into tree values
(0, 'ROOT', NULL, 0),
(1, 'Node 1', 0, 10),
(2, 'Node 1.1', 1, 10),
(3, 'Node 2', 0, 20),
(4, 'Node 1.1.1', 2, 10),
(5, 'Node 2.1', 3, 10),
(6, 'Node 1.2', 1, 20);
Write a query
WITH RECURSIVE
tree_search (id, name, level, parent_id, node_order) AS (
SELECT
id,
name,
0,
parent_id,
1
FROM tree
WHERE parent_id is NULL
UNION ALL
SELECT
t.id,
t.name,
ts.level + 1,
ts.id,
t.node_order
FROM tree t, tree_search ts
WHERE t.parent_id = ts.id
)
SELECT * FROM tree_search
WHERE level > 0
ORDER BY level, parent_id, node_order;
Here are the results:
id | name | level | parent_id | node_order
----+------------+-------+-----------+------------
1 | Node 1 | 1 | 0 | 10
3 | Node 2 | 1 | 0 | 20
2 | Node 1.1 | 2 | 1 | 10
6 | Node 1.2 | 2 | 1 | 20
5 | Node 2.1 | 2 | 3 | 10
4 | Node 1.1.1 | 3 | 2 | 10
(6 rows)
The tree nodes are ordered by a level of depth. In the final output we would present them in the subsequent lines.
For each level, they are ordered by parent_id and node_order within the parent. This tells us how to present them in the output - link node to the parent in this order.
Having such a structure it wouldn't be difficult to make a really nice presentation in HTML.
Recursive CTEs are available in PostgreSQL, IBM DB2, MS SQL Server, Oracle and SQLite.
If you'd like to read more on recursive SQL queries, you can either check the documentation of your favourite DBMS or read my two articles covering this topic:
Do It In SQL: Recursive Tree Traversal
Get to know the power of SQL recursive queries
As of Oracle 9i, you can use CONNECT BY.
SELECT LPAD(' ', (LEVEL - 1) * 4) || "Name" AS "Name"
FROM (SELECT * FROM TMP_NODE ORDER BY "Order")
CONNECT BY PRIOR "Id" = "ParentId"
START WITH "Id" IN (SELECT "Id" FROM TMP_NODE WHERE "ParentId" = 0)
As of SQL Server 2005, you can use a recursive common table expression (CTE).
WITH [NodeList] (
[Id]
, [ParentId]
, [Level]
, [Order]
) AS (
SELECT [Node].[Id]
, [Node].[ParentId]
, 0 AS [Level]
, CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
FROM [Node]
WHERE [Node].[ParentId] = 0
UNION ALL
SELECT [Node].[Id]
, [Node].[ParentId]
, [NodeList].[Level] + 1 AS [Level]
, [NodeList].[Order] + '|'
+ CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
FROM [Node]
INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[ParentId]
) SELECT REPLICATE(' ', [NodeList].[Level] * 4) + [Node].[Name] AS [Name]
FROM [Node]
INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[Id]
ORDER BY [NodeList].[Order]
Both will output the following results.
Name
'Node 1'
' Node 1.1'
' Node 1.1.1'
' Node 1.2'
'Node 2'
' Node 2.1'
Bill's answer is pretty gosh-darned good, this answer adds some things to it which makes me wish SO supported threaded answers.
Anyway I wanted to support the tree structure and the Order property. I included a single property in each Node called leftSibling that does the same thing Order is meant to do in the original question (maintain left-to-right order).
mysql> desc nodes ;
+-------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(255) | YES | | NULL | |
| leftSibling | int(11) | NO | | 0 | |
+-------------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
mysql> desc adjacencies;
+------------+---------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------+------+-----+---------+----------------+
| relationId | int(11) | NO | PRI | NULL | auto_increment |
| parent | int(11) | NO | | NULL | |
| child | int(11) | NO | | NULL | |
| pathLen | int(11) | NO | | NULL | |
+------------+---------+------+-----+---------+----------------+
4 rows in set (0.00 sec)
More detail and SQL code on my blog.
Thanks Bill your answer was helpful in getting started!
There are really good solutions which exploit the internal btree representation of sql indices. This is based on some great research done back around 1998.
Here is an example table (in mysql).
CREATE TABLE `node` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`tw` int(10) unsigned NOT NULL,
`pa` int(10) unsigned DEFAULT NULL,
`sz` int(10) unsigned DEFAULT NULL,
`nc` int(11) GENERATED ALWAYS AS (tw+sz) STORED,
PRIMARY KEY (`id`),
KEY `node_tw_index` (`tw`),
KEY `node_pa_index` (`pa`),
KEY `node_nc_index` (`nc`),
CONSTRAINT `node_pa_fk` FOREIGN KEY (`pa`) REFERENCES `node` (`tw`) ON DELETE CASCADE
)
The only fields necessary for the tree representation are:
tw: The Left to Right DFS Pre-order index, where root = 1.
pa: The reference (using tw) to the parent node, root has null.
sz: The size of the node's branch including itself.
nc: is used as syntactic sugar. it is tw+sz and represents the tw of the node's "next child".
Here is an example 24 node population, ordered by tw:
+-----+---------+----+------+------+------+
| id | name | tw | pa | sz | nc |
+-----+---------+----+------+------+------+
| 1 | Root | 1 | NULL | 24 | 25 |
| 2 | A | 2 | 1 | 14 | 16 |
| 3 | AA | 3 | 2 | 1 | 4 |
| 4 | AB | 4 | 2 | 7 | 11 |
| 5 | ABA | 5 | 4 | 1 | 6 |
| 6 | ABB | 6 | 4 | 3 | 9 |
| 7 | ABBA | 7 | 6 | 1 | 8 |
| 8 | ABBB | 8 | 6 | 1 | 9 |
| 9 | ABC | 9 | 4 | 2 | 11 |
| 10 | ABCD | 10 | 9 | 1 | 11 |
| 11 | AC | 11 | 2 | 4 | 15 |
| 12 | ACA | 12 | 11 | 2 | 14 |
| 13 | ACAA | 13 | 12 | 1 | 14 |
| 14 | ACB | 14 | 11 | 1 | 15 |
| 15 | AD | 15 | 2 | 1 | 16 |
| 16 | B | 16 | 1 | 1 | 17 |
| 17 | C | 17 | 1 | 6 | 23 |
| 359 | C0 | 18 | 17 | 5 | 23 |
| 360 | C1 | 19 | 18 | 4 | 23 |
| 361 | C2(res) | 20 | 19 | 3 | 23 |
| 362 | C3 | 21 | 20 | 2 | 23 |
| 363 | C4 | 22 | 21 | 1 | 23 |
| 18 | D | 23 | 1 | 1 | 24 |
| 19 | E | 24 | 1 | 1 | 25 |
+-----+---------+----+------+------+------+
Every tree result can be done non-recursively.
For instance, to get a list of ancestors of node at tw='22'
Ancestors
select anc.* from node me,node anc
where me.tw=22 and anc.nc >= me.tw and anc.tw <= me.tw
order by anc.tw;
+-----+---------+----+------+------+------+
| id | name | tw | pa | sz | nc |
+-----+---------+----+------+------+------+
| 1 | Root | 1 | NULL | 24 | 25 |
| 17 | C | 17 | 1 | 6 | 23 |
| 359 | C0 | 18 | 17 | 5 | 23 |
| 360 | C1 | 19 | 18 | 4 | 23 |
| 361 | C2(res) | 20 | 19 | 3 | 23 |
| 362 | C3 | 21 | 20 | 2 | 23 |
| 363 | C4 | 22 | 21 | 1 | 23 |
+-----+---------+----+------+------+------+
Siblings and children are trivial - just use pa field ordering by tw.
Descendants
For example the set (branch) of nodes that are rooted at tw = 17.
select des.* from node me,node des
where me.tw=17 and des.tw < me.nc and des.tw >= me.tw
order by des.tw;
+-----+---------+----+------+------+------+
| id | name | tw | pa | sz | nc |
+-----+---------+----+------+------+------+
| 17 | C | 17 | 1 | 6 | 23 |
| 359 | C0 | 18 | 17 | 5 | 23 |
| 360 | C1 | 19 | 18 | 4 | 23 |
| 361 | C2(res) | 20 | 19 | 3 | 23 |
| 362 | C3 | 21 | 20 | 2 | 23 |
| 363 | C4 | 22 | 21 | 1 | 23 |
+-----+---------+----+------+------+------+
Additional Notes
This methodology is extremely useful for when there are a far greater number of reads than there are inserts or updates.
Because the insertion, movement, or updating of a node in the tree requires the tree to be adjusted, it is necessary to lock the table before commencing with the action.
The insertion/deletion cost is high because the tw index and sz (branch size) values will need to be updated on all the nodes after the insertion point, and for all ancestors respectively.
Branch moving involves moving the tw value of the branch out of range, so it is also necessary to disable foreign key constraints when moving a branch. There are, essentially four queries required to move a branch:
Move the branch out of range.
Close the gap that it left. (the remaining tree is now normalised).
Open the gap where it will go to.
Move the branch into it's new position.
Adjust Tree Queries
The opening/closing of gaps in the tree is an important sub-function used by create/update/delete methods, so I include it here.
We need two parameters - a flag representing whether or not we are downsizing or upsizing, and the node's tw index. So, for example tw=18 (which has a branch size of 5). Let's assume that we are downsizing (removing tw) - this means that we are using '-' instead of '+' in the updates of the following example.
We first use a (slightly altered) ancestor function to update the sz value.
update node me, node anc set anc.sz = anc.sz - me.sz from
node me, node anc where me.tw=18
and ((anc.nc >= me.tw and anc.tw < me.pa) or (anc.tw=me.pa));
Then we need to adjust the tw for those whose tw is higher than the branch to be removed.
update node me, node anc set anc.tw = anc.tw - me.sz from
node me, node anc where me.tw=18 and anc.tw >= me.tw;
Then we need to adjust the parent for those whose pa's tw is higher than the branch to be removed.
update node me, node anc set anc.pa = anc.pa - me.sz from
node me, node anc where me.tw=18 and anc.pa >= me.tw;
Well given the choice, I'd be using objects. I'd create an object for each record where each object has a children collection and store them all in an assoc array (/hashtable) where the Id is the key. And blitz through the collection once, adding the children to the relevant children fields. Simple.
But because you're being no fun by restricting use of some good OOP, I'd probably iterate based on:
function PrintLine(int pID, int level)
foreach record where ParentID == pID
print level*tabs + record-data
PrintLine(record.ID, level + 1)
PrintLine(0, 0)
Edit: this is similar to a couple of other entries, but I think it's slightly cleaner. One thing I'll add: this is extremely SQL-intensive. It's nasty. If you have the choice, go the OOP route.
This was written quickly, and is neither pretty nor efficient (plus it autoboxes alot, converting between int and Integer is annoying!), but it works.
It probably breaks the rules since I'm creating my own objects but hey I'm doing this as a diversion from real work :)
This also assumes that the resultSet/table is completely read into some sort of structure before you start building Nodes, which wouldn't be the best solution if you have hundreds of thousands of rows.
public class Node {
private Node parent = null;
private List<Node> children;
private String name;
private int id = -1;
public Node(Node parent, int id, String name) {
this.parent = parent;
this.children = new ArrayList<Node>();
this.name = name;
this.id = id;
}
public int getId() {
return this.id;
}
public String getName() {
return this.name;
}
public void addChild(Node child) {
children.add(child);
}
public List<Node> getChildren() {
return children;
}
public boolean isRoot() {
return (this.parent == null);
}
#Override
public String toString() {
return "id=" + id + ", name=" + name + ", parent=" + parent;
}
}
public class NodeBuilder {
public static Node build(List<Map<String, String>> input) {
// maps id of a node to it's Node object
Map<Integer, Node> nodeMap = new HashMap<Integer, Node>();
// maps id of a node to the id of it's parent
Map<Integer, Integer> childParentMap = new HashMap<Integer, Integer>();
// create special 'root' Node with id=0
Node root = new Node(null, 0, "root");
nodeMap.put(root.getId(), root);
// iterate thru the input
for (Map<String, String> map : input) {
// expect each Map to have keys for "id", "name", "parent" ... a
// real implementation would read from a SQL object or resultset
int id = Integer.parseInt(map.get("id"));
String name = map.get("name");
int parent = Integer.parseInt(map.get("parent"));
Node node = new Node(null, id, name);
nodeMap.put(id, node);
childParentMap.put(id, parent);
}
// now that each Node is created, setup the child-parent relationships
for (Map.Entry<Integer, Integer> entry : childParentMap.entrySet()) {
int nodeId = entry.getKey();
int parentId = entry.getValue();
Node child = nodeMap.get(nodeId);
Node parent = nodeMap.get(parentId);
parent.addChild(child);
}
return root;
}
}
public class NodePrinter {
static void printRootNode(Node root) {
printNodes(root, 0);
}
static void printNodes(Node node, int indentLevel) {
printNode(node, indentLevel);
// recurse
for (Node child : node.getChildren()) {
printNodes(child, indentLevel + 1);
}
}
static void printNode(Node node, int indentLevel) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < indentLevel; i++) {
sb.append("\t");
}
sb.append(node);
System.out.println(sb.toString());
}
public static void main(String[] args) {
// setup dummy data
List<Map<String, String>> resultSet = new ArrayList<Map<String, String>>();
resultSet.add(newMap("1", "Node 1", "0"));
resultSet.add(newMap("2", "Node 1.1", "1"));
resultSet.add(newMap("3", "Node 2", "0"));
resultSet.add(newMap("4", "Node 1.1.1", "2"));
resultSet.add(newMap("5", "Node 2.1", "3"));
resultSet.add(newMap("6", "Node 1.2", "1"));
Node root = NodeBuilder.build(resultSet);
printRootNode(root);
}
//convenience method for creating our dummy data
private static Map<String, String> newMap(String id, String name, String parentId) {
Map<String, String> row = new HashMap<String, String>();
row.put("id", id);
row.put("name", name);
row.put("parent", parentId);
return row;
}
}
Assuming that you know that the root elements are zero, here's the pseudocode to output to text:
function PrintLevel (int curr, int level)
//print the indents
for (i=1; i<=level; i++)
print a tab
print curr \n;
for each child in the table with a parent of curr
PrintLevel (child, level+1)
for each elementID where the parentid is zero
PrintLevel(elementID, 0)
You can emulate any other data structure with a hashmap, so that's not a terrible limitation. Scanning from the top to the bottom, you create a hashmap for each row of the database, with an entry for each column. Add each of these hashmaps to a "master" hashmap, keyed on the id. If any node has a "parent" that you haven't seen yet, create an placeholder entry for it in the master hashmap, and fill it in when you see the actual node.
To print it out, do a simple depth-first pass through the data, keeping track of indent level along the way. You can make this easier by keeping a "children" entry for each row, and populating it as you scan the data.
As for whether there's a "better" way to store a tree in a database, that depends on how you're going to use the data. I've seen systems that had a known maximum depth that used a different table for each level in the hierarchy. That makes a lot of sense if the levels in the tree aren't quite equivalent after all (top level categories being different than the leaves).
If nested hash maps or arrays can be created, then I can simply go down the table from the beginning and add each item to the nested array. I must trace each line to the root node in order to know which level in the nested array to insert into. I can employ memoization so that I do not need to look up the same parent over and over again.
Edit: I would read the entire table into an array first, so it will not query the DB repeatedly. Of course this won't be practical if your table is very large.
After the structure is built, I must do a depth first traverse through it and print out the HTML.
There's no better fundamental way to store this information using one table (I could be wrong though ;), and would love to see a better solution ). However, if you create a scheme to employ dynamically created db tables, then you opened up a whole new world at the sacrifice of simplicity, and the risk of SQL hell ;).
To Extend Bill's SQL solution you can basically do the same using a flat array. Further more if your strings all have the same lenght and your maximum number of children are known (say in a binary tree) you can do it using a single string (character array). If you have arbitrary number of children this complicates things a bit... I would have to check my old notes to see what can be done.
Then, sacrificing a bit of memory, especially if your tree is sparse and/or unballanced, you can, with a bit of index math, access all the strings randomly by storing your tree, width first in the array like so (for a binary tree):
String[] nodeArray = [L0root, L1child1, L1child2, L2Child1, L2Child2, L2Child3, L2Child4] ...
yo know your string length, you know it
I'm at work now so cannot spend much time on it but with interest I can fetch a bit of code to do this.
We use to do it to search in binary trees made of DNA codons, a process built the tree, then we flattened it to search text patterns and when found, though index math (revers from above) we get the node back... very fast and efficient, tough our tree rarely had empty nodes, but we could searh gigabytes of data in a jiffy.
Pre-order transversal with on-the-fly path enumeration on adjacency representation
Nested sets from:
Konchog https://stackoverflow.com/a/42781302/895245
Jonny Buchanan https://stackoverflow.com/a/194031/895245
is the only efficient way I've seen of traversing, at the cost of slower updates. That's likely what most people will want for pre-order.
Closure table from https://stackoverflow.com/a/192462/895245 is interesting, but I don't see how to enforce pre-order there: MySQL Closure Table hierarchical database - How to pull information out in the correct order
Mostly for fun, here's a method that recursively calculates the 1.3.2.5. prefixes on the fly and sorts by them at the end, based only on the parent ID/child index representation.
Upsides:
updates only need to update the indexes of each sibling
Downsides:
n^2 memory usage worst case for a super deep tree. This could be quite serious, which is why I say this method is likely mostly for fun only. But maybe there is some ultra high update case where someone would want to use it? Who knows
recursive queries, so reads are going to be less efficient than nested sets
Create and populate table:
CREATE TABLE "ParentIndexTree" (
"id" INTEGER PRIMARY KEY,
"parentId" INTEGER,
"childIndex" INTEGER NOT NULL,
"value" INTEGER NOT NULL,
"name" TEXT NOT NULL,
FOREIGN KEY ("parentId") REFERENCES "ParentIndexTree"(id)
)
;
INSERT INTO "ParentIndexTree" VALUES
(0, NULL, 0, 1, 'one' ),
(1, 0, 0, 2, 'two' ),
(2, 0, 1, 3, 'three'),
(3, 1, 0, 4, 'four' ),
(4, 1, 1, 5, 'five' )
;
Represented tree:
1
/ \
2 3
/ \
4 5
Then for a DBMS with arrays like PostgreSQL](https://www.postgresql.org/docs/14/arrays.html):
WITH RECURSIVE "TreeSearch" (
"id",
"parentId",
"childIndex",
"value",
"name",
"prefix"
) AS (
SELECT
"id",
"parentId",
"childIndex",
"value",
"name",
array[0]
FROM "ParentIndexTree"
WHERE "parentId" IS NULL
UNION ALL
SELECT
"child"."id",
"child"."parentId",
"child"."childIndex",
"child"."value",
"child"."name",
array_append("parent"."prefix", "child"."childIndex")
FROM "ParentIndexTree" AS "child"
JOIN "TreeSearch" AS "parent"
ON "child"."parentId" = "parent"."id"
)
SELECT * FROM "TreeSearch"
ORDER BY "prefix"
;
This creates on the fly prefixes of form:
1 -> 0
2 -> 0, 0
3 -> 0, 1
4 -> 0, 0, 0
5 -> 0, 0, 1
and then PostgreSQL then sorts by the arrays alphabetically as:
1 -> 0
2 -> 0, 0
4 -> 0, 0, 0
5 -> 0, 0, 1
3 -> 0, 1
which is the pre-order result that we want.
For a DBMS without arrays like SQLite, you can hack by encoding the prefix with a string of fixed width integers. Binary would be ideal, but I couldn't find out how, so hex would work. This of course means you will have to select a maximum depth that will fit in the number of bytes selected, e.g. below I choose 6 allowing for a maximum of 16^6 children per node.
WITH RECURSIVE "TreeSearch" (
"id",
"parentId",
"childIndex",
"value",
"name",
"prefix"
) AS (
SELECT
"id",
"parentId",
"childIndex",
"value",
"name",
'000000'
FROM "ParentIndexTree"
WHERE "parentId" IS NULL
UNION ALL
SELECT
"child"."id",
"child"."parentId",
"child"."childIndex",
"child"."value",
"child"."name",
"parent"."prefix" || printf('%06x', "child"."childIndex")
FROM "ParentIndexTree" AS "child"
JOIN "TreeSearch" AS "parent"
ON "child"."parentId" = "parent"."id"
)
SELECT * FROM "TreeSearch"
ORDER BY "prefix"
;
Some nested set notes
Here are a few points which confused me a bit after looking at the other nested set answers.
Jonny Buchanan shows his nested set setup as:
__________________________________________________________________________
| Root 1 |
| ________________________________ ________________________________ |
| | Child 1.1 | | Child 1.2 | |
| | ___________ ___________ | | ___________ ___________ | |
| | | C 1.1.1 | | C 1.1.2 | | | | C 1.2.1 | | C 1.2.2 | | |
1 2 3___________4 5___________6 7 8 9___________10 11__________12 13 14
| |________________________________| |________________________________| |
|__________________________________________________________________________|
which made me wonder why not just use the simpler looking:
__________________________________________________________________________
| Root 1 |
| ________________________________ _______________________________ |
| | Child 1.1 | | Child 1.2 | |
| | ___________ ___________ | | ___________ ___________ | |
| | | C 1.1.1 | | C 1.1.2 | | | | C 1.2.1 | | C 1.2.2 | | |
1 2 3___________| 4___________| | 5 6___________| 7___________| | |
| |________________________________| |_______________________________| |
|_________________________________________________________________________|
which does not have an extra number for each endpoint.
But then when I actually tried to implement it, I noticed that it was hard/impossible to implement the update queries like that, unless I had parent information as used by Konchog. The problem is that it was hard/impossible to distinguish between a sibling and a parent in one case while the tree was being moved around, and I needed that to decide if I was going to reduce the right hand side or not while closing a gap.
Left/size vs left/right: you could store it either way in the database, but I think left/right can be more efficient as you can index the DB with a multicolumn index (left, right) which can then be used to speed up ancestor queries, which are of type:
left < curLeft AND right > curLeft
Tested on Ubuntu 22.04, PostgreSQL 14.5, SQLite 3.34.0.
If elements are in tree order, as shown in your example, you can use something like the following Python example:
delimiter = '.'
stack = []
for item in items:
while stack and not item.startswith(stack[-1]+delimiter):
print "</div>"
stack.pop()
print "<div>"
print item
stack.append(item)
What this does is maintain a stack representing the current position in the tree. For each element in the table, it pops stack elements (closing the matching divs) until it finds the parent of the current item. Then it outputs the start of that node and pushes it to the stack.
If you want to output the tree using indenting rather than nested elements, you can simply skip the print statements to print the divs, and print a number of spaces equal to some multiple of the size of the stack before each item. For example, in Python:
print " " * len(stack)
You could also easily use this method to construct a set of nested lists or dictionaries.
Edit: I see from your clarification that the names were not intended to be node paths. That suggests an alternate approach:
idx = {}
idx[0] = []
for node in results:
child_list = []
idx[node.Id] = child_list
idx[node.ParentId].append((node, child_list))
This constructs a tree of arrays of tuples(!). idx[0] represents the root(s) of the tree. Each element in an array is a 2-tuple consisting of the node itself and a list of all its children. Once constructed, you can hold on to idx[0] and discard idx, unless you want to access nodes by their ID.
Think about using nosql tools like neo4j for hierarchial structures.
e.g a networked application like linkedin uses couchbase (another nosql solution)
But use nosql only for data-mart level queries and not to store / maintain transactions

PHP MySQL consolidate column where other column has duplicates

I have a MySQL table that has three columns, the first is a unique key (INT(11) AUTO_INCREMENT), the next is an indexed value (VARCHAR(255)) and the third is a description (TEXT). There are duplicate values in the second column, but each row has a different description. I want to remove all rows where the second column is duplicated but append each description of the same indexed value to the first instance the value, and breaking string with a semicolon and space.
For example, my table looks like this:
cid | word | description
------------------------------
1 | cat | an animal with wiskers
2 | cat | a house pet
3 | dog | a member of the canine family
4 | cat | a cool person
I want to change the table to look like this:
cid | word | description
------------------------------
1 | cat | an animal with wiskers; a house pet; a cool person
3 | dog | a member of the canine family
I'm not adverse to using a PHP script to do this, but would prefer MySQL. The table has over 170,000 rows and would take PHP a long time to loop over it.
SQL:
select `cid`,`word`,group_concat(`description` SEPARATOR '; ') as `description` from `test_table` group by `word`;
Ok.. you can copy all the data into another table, and rename it then..
insert into `test_new` (`cid`,`word`,`desc`) (select `cid`,`word`,group_concat(`desc` SEPARATOR '; ') as `description` from `test_table` group by `word`);
mysql> describe `test_new`;
+-------+----------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+-------+
| id | int(11) | YES | | NULL | |
| word | char(10) | YES | | NULL | |
| desc | text | YES | | NULL | |
+-------+----------+------+-----+---------+-------+
3 rows in set (0.00 sec)
mysql> select * from `test_new`;
+------+------+---------------------+
| id | word | desc |
+------+------+---------------------+
| 1 | cat | desc1; desc2; desc4 |
| 3 | dog | desc3 |
+------+------+---------------------+
2 rows in set (0.00 sec)
As was mentioned before, you can create a new table and copy the info, you can also do it in two steps, but only if there´s no problem with modifying the old table:
UPDATE tableOld AS N1, tableOld AS N2
SET N1.description = concat(concat(N1.description,"; "),N2.decription))
WHERE N2.word = N1.word
insert into tableNew (cid,name,description)select * from tableOld group by word

GROUP_CONCAT With Nested Set Model

I have an application that uses a nested set model class to organise my data, however I'm trying to write a query that will group_concat my results. I know I need to put some sub select statements somewhere but I can't figure it out!
Here's my structure at the moment:
table: person
-----------+------------+-----------
|Person_ID | Name | Age |
-----------+------------+-----------
| 1 | Mark Vance | 19 |
| 2 | Michael Tsu| 22 |
| 3 | Mark Jones | 29 |
| 4 | Sara Young | 25 |
-----------+------------+-----------
table: person_to_group
----+------------+-----------
|ID | Person_ID | Group_ID |
----+------------+-----------
| 1 | 3 | 1 |
| 2 | 3 | 2 |
| 3 | 1 | 2 |
| 4 | 4 | 3 |
----+------------+-----------
table: groups
----------+--------------+--------------+-------------
|Group_ID | Group_Name | Group_Left | Group_Right |
----------+--------------+--------------+-------------
| 1 | Root | 1 | 6 |
| 2 | Node | 2 | 5 |
| 3 | Sub Node | 3 | 4 |
----------+--------------+--------------+-------------
I need to render something like this with my results:
//Grab the group_IDs for this person and put them in the class tag...
<li class="2 3">Sara Young is in the Sub Node Group</li>
Notice that although Sara is in the Sub Node group, she is still being given the id for Node aswell because she is a child of Node.
The following is the query that I am working with as a starting point.
SELECT *, GROUP_CONCAT( CAST( gg.Group_ID AS CHAR ) SEPARATOR ' ' ) Group_IDs
FROM groups gg
LEFT JOIN person_to_group AS t1 ON gg.Group_ID = t1.Group_ID
LEFT JOIN person AS t2 ON t2.Person_ID = t1.Person_ID
GROUP BY t2.per_ID
ORDER BY t2.Name ASC
Any help would be much appreciated!
Here's how I'd write the query:
SELECT p.Name,
GROUP_CONCAT( g.Group_Name ) AS Group_List,
GROUP_CONCAT( CAST( gg.Group_ID AS CHAR ) SEPARATOR ' ' ) AS Group_ID_List
FROM person AS p
INNER JOIN person_to_group AS pg ON p.Person_ID = pg.Person_ID
INNER JOIN groups AS g ON pg.Group_ID = g.Group_ID
INNER JOIN groups AS gg ON g.Group_Left BETWEEN gg.Group_Left AND gg.Group_Right
GROUP BY p.Name
ORDER BY p.Name ASC
Note that if you group by person name, you also need to GROUP_CONCAT the list of group names. According to your schema, a person could belong to multiple groups, because of the many-to-many relationship.
I also recommend against using SELECT * in general. Just specify the columns you need.
This was little bit interesting as I do programming in both MsSQL and MySql. In SQL I have used function called STUFF. In MySQL you can use a function called INSERT. I tried out the below query in MsSQL. Don't have a MySQL handy to try out my query. If I have time I will post the MySQL version of the query.
DECLARE #person TABLE (Person_ID INT, Name VARCHAR(50), Age INT)
INSERT INTO #person VALUES
(1,'Mark Vance',19),
(2,'Michael Tsu',22),
(3,'Mark Jones',29),
(4,'Sara Young',25)
DECLARE #groups TABLE (Group_ID INT, Group_Name VARCHAR(50), Group_Left INT, Group_Right INT)
INSERT INTO #groups VALUES
(1,'Root',1,6),
(2,'Node',2,5),
(3,'Sub Node',3,4)
DECLARE #person_to_group TABLE (ID INT, Person_ID INT, Group_ID INT)
INSERT INTO #person_to_group VALUES
(1,3,1),
(2,3,2),
(3,1,1),
(4,4,1),
(4,1,1)
SELECT *,STUFF((SELECT ',' + CAST(g.Group_ID AS VARCHAR) FROM #groups g
JOIN #person_to_group pg ON g.Group_ID = pg.Group_ID AND pg.Person_ID = a.Person_ID FOR XML PATH('')) , 1, 1, '' ) FROM #person a
Function: INSERT(str,pos,len,newstr)
Documentation: http://dev.mysql.com/doc/refman/5.0/en/string-functions.html#function_insert

Categories