Selecting with modified preorder tree traversal when there are multiple trees - php

I have implemented a Modified Preorder Tree Traversal as explained here for optimalisation reasons. My table is something like this:
+----+-----------+------+-------+
| id | parent_id | left | right |
+----+-----------+------+-------+
| 1 | NULL | 1 | 4 |
| 2 | 1 | 2 | 3 |
| 3 | NULL | 1 | 4 |
| 4 | 3 | 2 | 3 |
+----+-----------+------+-------+
I.e. there exist an arbitrary number of trees in the table. If a node's parent id is null, then that automatically means the node is the very base of its tree.
The article points out that one can select all a node's descendants very simply:
SELECT *
FROM table
WHERE
left > ?
AND
right < ?
Now my problem is that the selection is obviously going to include nodes of other trees. After all, I haven't even specified a tree to begin with. Is it possible to just select the nodes from one specified tree with this model?

Don't start new tree with left = 1. Use MAX(right) + 1 for left value. This will not affects other node manipulations, and trees will be separated by left-right range.

Related

Can SELECT, SELECT COUNT and cross reference tables be handled by just one query?

I have a page that displays a list of projects. With each project is displayed the following data retrieved from a mysqli database:
Title
Subtitle
Description
Part number (1 of x)
The total number of photos associated with that project
A randomly selected photo from the project
A list of tags
Projects are displayed 6 per page using a pagination system
As this is based on an old project of mine, it was originally done with sloppy code (I was just learning and did not know any better) using many queries. Three, in fact, just for items 5-7, and those were contained within a while loop that worked with the pagination system. I'm now quite aware that this is not even close to being the right way to do business.
I am familiar with INNER JOIN and the use of subqueries, but I'm concerned that I may not be able to get all of this data using just one select query for the following reasons:
Items 1-4 are easy enough with a basic SELECT query, BUT...
Item 5 needs a SELECT COUNT AND...
Item 6 needs a basic SELECT query with an ORDER by RAND LIMIT 1 to
select one random photo out of all those associated with each project
(using FilesystemIterator is out of the question, because the photos
table has a column indicating 0 if a photo is inactive and 1 if it is
active)
Item 7 is selected from a cross reference table for the tags and
projects and a table containing the tag ID and names
Given that, I'm not certain if all this can (r even should for that matter) be done with just one query or if it will need more than one query. I have read repeatedly how it is worth a swat on the nose with a newspaper to nest one or more queries inside a while loop. I've even read that multiple queries is, in general, a bad idea.
So I'm stuck. I realize this is likely to sound too general, but I don't have any code that works, just the old code that uses 4 queries to do the job, 3 of which are nested in a while loop.
Database structure below.
Projects table:
+-------------+---------+----------+---------------+------+
| project_id | title | subtitle | description | part |
|---------------------------------------------------------|
| 1 | Chevy | Engine | Modify | 1 |
| 2 | Ford | Trans | Rebuild | 1 |
| 3 | Mopar | Diff | Swap | 1 |
+-------------+---------+----------+---------------+------+
Photos table:
+----------+------------+--------+
| photo_id | project_id | active |
|--------------------------------|
| 1 | 1 | 1 |
| 2 | 1 | 1 |
| 3 | 1 | 1 |
| 4 | 2 | 1 |
| 5 | 2 | 1 |
| 6 | 2 | 1 |
| 7 | 3 | 1 |
| 8 | 3 | 1 |
| 9 | 3 | 1 |
+----------+------------+--------+
Tags table:
+--------+------------------+
| tag_id | tag |
|---------------------------|
| 1 | classic |
| 2 | new car |
| 3 | truck |
| 4 | performance |
| 5 | easy |
| 6 | difficult |
| 7 | hard |
| 8 | oem |
| 9 | aftermarket |
+--------+------------------+
Tag/Project cross-reference table:
+------------+-----------+
| project_id | tag_id |
|------------------------|
| 1 | 1 |
| 1 | 3 |
| 1 | 4 |
| 2 | 2 |
| 2 | 5 |
| 3 | 6 |
| 3 | 9 |
+------------+-----------+
I'm not asking for the code to be written for me, but if what I'm asking makes sense, I'd sincerely appreciate a shove in the right direction. Often times I struggle with both the PHP and MySQLi manuals online, so if there's any way to break this down, then fantastic.
Thank you all so much.
You're able to do subqueries inside your SELECT clause, like this:
SELECT
p.title, p.subtitle, p.description, p.part,
(SELECT COUNT(photo_id) FROM Photos where project_id = p.project_id) as total_photos,
(SELECT photo_id FROM Photos where project_id = p.project_id ORDER BY RAND LIMIT 1) as random_photo
FROM projects as p
Now, for the list of tags, as it returns more than one row, you can't do a subquery and you should do one query for every project. Well, in fact you can if you return all the tags in some kind of concatenation, like a comma separated list: tag1,tag2,tag3... but I don't recommend this one time that you will need to explode the column value. Do it only if you have many many projects and the performance to retrieve the list of tags for each individual project is fairly low. If you really want, you can:
SELECT
p.title, p.subtitle, p.description, p.part,
(SELECT COUNT(photo_id) FROM Photos where project_id = p.project_id) as total_photos,
(SELECT photo_id FROM Photos where project_id = p.project_id ORDER BY RAND LIMIT 1) as random_photo,
(SELECT GROUP_CONCAT(tag SEPARATOR ', ') FROM tags WHERE tag_id in (SELECT tag_id FROM tagproject WHERE project_id = p.project_id)) as tags
FROM projects as p
As you said from item 1 to 4 you already have the solution.
Add to the same query a SQL_CALC_FOUND_ROWS instead of a SELECT COUNT to solve the item 5.
For the item 6 you can use a subquery or maybe a LEFT JOIN limiting to one result.
For the latest item you can also use a subquery joining all the tags in a single result (separated by comma for instance).

php mysql get all parents

i'm trying to get all parents of children. I have a simple query
select from sections where sectionID = 6
The database structure is very simple
sectionID parent
1 0
2 1
3 1
4 2
5 4
6 5
Now I'm trying to get all parents from sectionID 6, the result should be a string 0/2/4/5/6.
children might not have a lot of parents, it might have only one so the result should be different. i.e
5/6.. I hope you understand what I'm trying to do. I have tried couple recursive functions which i've found on the internet, but I really suck at this and was wondering if anyone could help me to get on the right track. P.S I'm using php and mysql
Well, as far as I can see you have two choices, both well known.
1) You make a recursive function, just the ones you have been trying. There are tons of them around and i won't put him in here.
2) By far my favourite, this is the Database pattern most modern ORM use, it's called nested set model.
Basically you create a few more columns on the table, it should look like this one:
CREATE TABLE nested_category (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL
);
INSERT INTO nested_category VALUES(1,'ELECTRONICS',1,20),(2,'TELEVISIONS',2,9),(3,'TUBE',3,4),
(4,'LCD',5,6),(5,'PLASMA',7,8),(6,'PORTABLE ELECTRONICS',10,19),(7,'MP3 PLAYERS',11,14),(8,'FLASH',12,13),
(9,'CD PLAYERS',15,16),(10,'2 WAY RADIOS',17,18);
SELECT * FROM nested_category ORDER BY category_id;
+-------------+----------------------+-----+-----+
| category_id | name | lft | rgt |
+-------------+----------------------+-----+-----+
| 1 | ELECTRONICS | 1 | 20 |
| 2 | TELEVISIONS | 2 | 9 |
| 3 | TUBE | 3 | 4 |
| 4 | LCD | 5 | 6 |
| 5 | PLASMA | 7 | 8 |
| 6 | PORTABLE ELECTRONICS | 10 | 19 |
| 7 | MP3 PLAYERS | 11 | 14 |
| 8 | FLASH | 12 | 13 |
| 9 | CD PLAYERS | 15 | 16 |
| 10 | 2 WAY RADIOS | 17 | 18 |
+-------------+----------------------+-----+-----+
If you notice there is no parent_id column. To be able to search for it's childrens for, let's say row 5 the query will be like:
Select * from nested_category where left > 7 and left < 8 order by left asc, which will bring no results.
For row number 1 the result however will bring the entire tree.
I have no php script for autocreating those columns on this computer, but there are plenty around. Im afraid they are also recursive.
You my find lots of info around searching for "nested set model", like this or theorical exaplanations of the model like this one
AND THIS IS A WAY DUPLICATED QUESTION (i can not put it as duplicated)
Some other answers:
This guys do it all in mysql
Some more answers here
You should reread the forum rules before posting, look for already asked questions.
Hope it helps.
I'm not going to write all the code, but this idea should work if I understand your database shema correctly.
section = 6
result = ""
while section != ""
select parent where sectionID = section
result += parent
section = parent
return result

How to write a MYSQL query that will return children nested under parents?

I don't know if what I'm asking is even possible, but here's my situation. I have a table structured somewhat like this:
+--------------------------------------------------+
| id | parent_id | name | category | ....
+--------------------------------------------------+
| 0 | -1 | item0 | 1 |
| 1 | 0 | item1 | 1 |
| 2 | 0 | item2 | 1 |
| 3 | 2 | item3 | 1 |
| 4 | 2 | item4 | 1 |
| 5 | -1 | item5 | 1 |
+--------------------------------------------------+
A parent_id of -1 will mean it is a "base" item with no parent. Each item will have more columns of information. I need to somehow output all items in a category nested like the following:
item0 => item1
=> item2
=> item3
=> item4
item5
I don't know if that makes sense or not, but hopefully it does!
The only way I can think of doing this is making a query to get all of the "base" items (querying for rows with parent_id = -1) then iterate through every resulting row, querying for rows that have their parent_id equal to the current row's id, then repeating the process going deeper and deeper until there aren't any more children for a base item.
Is there a better way?
Thanks!!
It is not possible in pure SQL.
SQL is intended to work with relational data not trees (hierarchical data).
You can represent tree in an SQL schema, however you won't be a able to result in a tree as you intend to do.
The only way to do is to get an usable result by making as many join as level you're storing.
Your current schema may support multiple level, however, it will be very difficult to manage more than one or two level.
You may be interested in Nested Set Model or Managing hierarchical data in mysql
There are some implementation of the Nested Set like this one to work with Doctrine 2
This is not possible in pure SQL and it is one of the aspects of the relational model that generates most criticism.
I would recommend you to read the links on this post: SQL "tree-like" query - most parent group
And also, if your application relies too much on this, I would suggest you to take a look at some non-relational databases that can represent this kind of data way better, such as MongoDB ( www.mongodb.org )
I hope i understood well your question(it's pretty late here and i've just come from a bar), if i didnt, just correct me and i'll rewrite my answer.
From the scenario given, i guess there's another parent table, isn't there?
Lets imagine it's attributes are id and name. Children table is the given one by you (w/o unnecessary attributes).
mysql> insert into parent(name) values ('petr'),('tomas'),('richard');
mysql> insert into children(name,parent_id) values('michal',1),('tomas',1),('michal');
mysql> select parent.id,parent.name,children.name from parent left join children on parent.id = children.parent_id;
+----+---------+--------+
| id | name | name |
+----+---------+--------+
| 1 | petr | michal |
| 1 | petr | tomas |
| 2 | tomas | NULL |
| 3 | richard | michal |
+----+---------+--------+
In order to do this multiple time (parent got child who got child who got child etc..) You can accomplish that by using multiple joins.
mysql> select parent.id,parent.name as Parent,children.name as Child,children2.name as Child2 from parent left join children on parent.id = children.parent_id left join children2 on children.id = children2.parent_id;
+----+---------+--------+--------+
| id | Parent | Child | Child2 |
+----+---------+--------+--------+
| 1 | petr | michal | NULL |
| 1 | petr | tomas | dan |
| 1 | petr | tomas | pavel |
| 2 | tomas | NULL | NULL |
| 3 | richard | michal | michal |
+----+---------+--------+--------+
If i either didnt answer what you asked or you need further explanation let me know ;]
Regards,
Releis

Is there a way to get nested data out of MySQL without using recursion?

So let us say that I have a menu system with all the navigation items stored in a MySQL table like so:
Table: Menu
-------------------------------------------------------
| id | title | url | parent_id |
-------------------------------------------------------
| 1 | Home | /home | 0 |
| 2 | About | /about | 0 |
| 3 | History | /about/history | 2 |
| 4 | Location | /about/location | 2 |
| 5 | Staff | /about/staff | 2 |
| 6 | Articles | /blog | 0 |
| 7 | Archive | /blog/archive | 6 |
| 8 | Tags | /blog/tags | 6 |
| 9 | Tag Name 1 | /blog/tags/tag-name-1 | 8 |
| 10 | Tag Name 2 | /blog/tags/tag-name-2 | 8 |
-------------------------------------------------------
As you can see this table is quite simple with the only complication being the self referencing column parent_id, which defines how the menu should be nested.
So this would produce the following menu:
- Home
- About
- History
- Location
- Staff
- Articles
- Archive
- Tags
- Tag Name 1
- Tag Name 2
Is there a way to get this structure from the aforementioned table without making use of a recursive function in PHP (but it could be Python, Java or any other language) that queries the database with each iteration?
Ideally this could be handled with one MySQL query. Perhaps the table structure needs to be changed to accommodate this - if so how?
You could pull all of it out in one single pull, and then work with it recursively in PHP. That way you save some of the query time, but gain a little scripting time.
I would do something like this:
Get all data, ordered by parent id
Put row into $data[$parent_id][]
define function to build menu, takes one param which is id
get $data[$id] and work with that array, building the array.
while looping through the items, check if size of $data[current-item-id] > 0
if so, call above function with 0 as param
This way, you only query the database once, but use a little more of the servers ram.
If you're fetching the whole tree and you can't or don't want to change the table structure, take a look at https://stackoverflow.com/a/8325451/4833
This can be done in sql query, take a look at this resource which explains recursion in a query
http://www.artfulsoftware.com/mysqlbook/sampler/mysqled1ch20.html.
MySQL don't have an default function to do that.
You can make an procedure with loop to get the data result you want, or create an function and use in your sql select.
Anyway you will use loop.
Example:
DROP PROCEDURE IF EXISTS famsubtree;
DELIMITER go
CREATE PROCEDURE famsubtree( root INT )
BEGIN
DROP TABLE IF EXISTS famsubtree;
CREATE TABLE famsubtree
SELECT childID, parentID, 0 AS level
FROM familytree
WHERE parentID = root;
ALTER TABLE famsubtree ADD PRIMARY KEY(childID,parentID);
REPEAT
INSERT IGNORE INTO famsubtree
SELECT f.childID, f.parentID, s.level+1
FROM familytree AS f
JOIN famsubtree AS s ON f.parentID = s.childID;
UNTIL Row_Count() = 0 END REPEAT;
E ND ;
go
DELIMITER ;
And use to query:
call famsubtree(1); -- from the root you can see forever
SELECT Concat(Space(level),parentID) AS Parent, Group_Concat(childID ORDER BY childID) AS Child
FROM famsubtree
GROUP BY parentID;

Sub categories Hierarchy

I designed a SQL structure to represent categories and their subcategories.
I have 3 tables:
articles
articles_categories
categories
Articles table:
id,title,content
Categories table:
id, title, parent_id
articles_categories:
id,article_id,category_id
No problem with SQL, but now - lets say i'm on article id 5
article id 5 has - 3 categories, that 2 of them has parents, and the main has '0' as parent.
How do I fetch them all efficiently? (lets say for - breadcrumbs).
thanks!
Unless the depth of the category hierarchy is fixed, you cannot do this in MySQL with your current model (adjacency list). You'd have to traverse the hierarchy using several SQL statements in a loop.
If the category hierarchy is fairly static, you can "precompute" the tree by using:
Path enumeration
Nested sets
Closure table
All of the above, trades write performance for read performance.
Google or search SO for any of the above and you will find examples of how to implement it.
Quite often, I find that storing the data in a adjacency list (because of best matches the data model) and caching a copy of the tree in the application is good enough, but that depends on your requirements of course :)
This should do the job:
select * from articles_categories
left join categories on categories.id = articles_categories.category_id
where article_id=1;
+------+------------+-------------+------+--------+-----------+
| id | article_id | category_id | id | title | parent_id |
+------+------------+-------------+------+--------+-----------+
| NULL | 1 | 1 | 1 | first | 0 |
| NULL | 1 | 2 | 2 | second | 1 |
| NULL | 1 | 3 | 3 | third | 2 |
+------+------------+-------------+------+--------+-----------+
Additionally, I would remove the "id" column from associative table articles_categories.

Categories