related tables with many levels - php

Im trying make a menu with the next structure:
-category 1
--category 1.1
---category 1.1.1
----product 1
----product 2
----product 3
-category 2
--category 2.1
---category 2.1.1
----product 1
----product 2
----product 3
-category 3
--category 3.1
---category 3.1.1
----product 1
I have an scheme like wordpress:
relation_category_products table store an id from category and id from products.
The question is whats the best way to make the mysql queries for having all that structure?
My first solution it was make queries for parent category (-category), then for each every row take the id for the next --category and then until raise the product node. But with that tecnique theres a lot of queries (35 for the moment). And i dont know whats the better way to get all that relationship and take it with php for render the menu.
thanks

http://dev.mysql.com/tech-resources/articles/hierarchical-data.html
Your table structure should have a field for the parent, e.g.
Table "category"
id(int) name parent(int)
1 Category 1
2 Category 1.1 1
3 Category 1.1.1 2
4 Category 2
5 Category 2.1 4
You then use a one-to-many relationship in your "Product" table to link them to a category.
If you want to retrieve the nodes directly under a category, just SELECT by the "parent" field.
If you want to retrieve the whole tree use a join query:
SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.id
LEFT JOIN category AS t3 ON t3.parent = t2.id
This will give you back something like:
+-------------+----------------------+---------------+
| lev1 | lev2 | lev3 |
+-------------+----------------------+---------------+
| Category 1 | Category 1.1 | Category 1.1.1|
| Category 2 | Category 2.1 | NULL |
+-------------+----------------------+---------------+

You could use a leftjoin and then loop the array with a foreach make sure you croup them correctly and use the primary column alt. an indexed one for best performance there is a good guide for that here
regards

Related

Dynamic Query to Select Multiple Entries from a Relational Record Table

Let's say I have a table which stores the relation between products and their categories:
p_id | c_id
-----+-----
1 | 1
1 | 2
2 | 1
2 | 2
2 | 3
3 | 2
As you can see, a product might have multiple categories. How can I search for products that have categories 1 and 2 assigned? The closest I can think of is using JOIN:
SELECT a.p_id
FROM rel_table a
JOIN rel table b
ON a.p_id=b.p_id AND b.c_id=2
WHERE a.c_id=1
While this achieves what I want, it is not practical because my query will be dynamic. If I have to select products with 3 categories, this requires a difficult change in the query.
Is there a cleaner and more clever way to achieve this? I imagine something that selects first set, then refines with another category for the amount of levels needed.
You should use IN or Between for such things. You can dynamically create the values you put in the IN/BETWEEN
SELECT a.p_id
FROM rel_table a
WHERE a.c_id IN (1,2,3)
group by a.p_id
having count(1) = 3
order by a.p_id asc
or
SELECT a.p_id
FROM rel_table a
WHERE a.c_id between 1 and 3
group by a.p_id
having count(1) = 3
order by a.p_id asc

Best way of representing hierarchical tasks/sub-tasks (MySQL/PHP)

I am making a simple todo app (Laravel4/MySQL) and it needs the ability to make tasks and subtasks (limiting it to max. 3 levels)
I was checking out Nested-Set implementations for Laravel here and here. Is it an overkill for my requirement?
I'm guessing nested-sets saves hierarchy data globally (against say, a per-user or per-project basis) and are better for items like a multilevel menu with a limited number of items.
What is the best implementation for my case, where hundreds of users would have a multitude of projects and each having hundreds of multilevel tasks/sub-tasks? Would there be unnecessary traversals/overheads if I implement nested-sets for my case?
I recommend to read the Managing hierarchical data in mysql article.
Briefly,
CREATE TABLE category(
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
parent INT DEFAULT NULL
);
INSERT INTO category VALUES(1,'ELECTRONICS',NULL),(2,'TELEVISIONS',1),(3,'TUBE',2),
(4,'LCD',2),(5,'PLASMA',2),(6,'PORTABLE ELECTRONICS',1),(7,'MP3 PLAYERS',1),(8,'FLASH',7),
(9,'CD PLAYERS',6),(10,'2 WAY RADIOS',6);
SELECT * FROM category ORDER BY category_id;
+-------------+----------------------+--------+
| category_id | name | parent |
+-------------+----------------------+--------+
| 1 | ELECTRONICS | NULL |
| 2 | TELEVISIONS | 1 |
| 3 | TUBE | 2 |
| 4 | LCD | 2 |
| 5 | PLASMA | 2 |
| 6 | PORTABLE ELECTRONICS | 1 |
| 7 | MP3 PLAYERS | 6 |
| 8 | FLASH | 7 |
| 9 | CD PLAYERS | 6 |
| 10 | 2 WAY RADIOS | 6 |
+-------------+----------------------+--------+
The query retrieve all your data:
SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
WHERE t1.name = 'ELECTRONICS';
The retrieving only leaf names:
SELECT t1.name FROM
category AS t1 LEFT JOIN category as t2
ON t1.category_id = t2.parent
WHERE t2.category_id IS NULL;
The retrieving one path:
SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
WHERE t1.name = 'ELECTRONICS' AND t3.name = 'FLASH';
My answer would be Closure Table.
I read about a few ways to solve hierarchies in the book SQL Antipatterns (https://books.google.com/books/about/SQL_Antipatterns.html?id=Ghr4RAAACAAJ). I definitely recommend reading the book.
My favorite way to implement hierarchies is via closure tables. This is a great source that explains them in depth: http://technobytz.com/closure_table_store_hierarchical_data.html.
To summarize: make one table that keeps track of the actual items in the hierarchy (e.g. task_id, task_description, time_opened, etc.) and another table to track the relations. This second table should have things such as task_id and parent_task_id. The best trick with these tables is to keep track of every parent-child relation, not just the direct parent-child relations. So if you have Task 1 that has a child task, Task 2, and Task 2 has a child task, Task 3, keep track of the parent child relation between Task 1 and Task 2 as well as between Task 1 and Task 3.
The tradeoff with closure tables vs nested sets is that closure tables consume more memory, but have less computing needed when doing operations. This is because you store every relation between every task (this takes memory) and the simple availability of all of these relationships makes it faster for the RDBMS to get information about the relationships.
Hope this helps!

php&mysql find parent category name

I have problem about inner join, left join commands.
My category table is:
ID | parent | title
1 | 0 | First Category
2 | 1 | Other Category
I have list categorys and I want get parents category title at sql command.
I have tried:
SELECT cat.ID, cat.title, cat2.title as parentcatname, cat.parent
FROM categories cat INNER JOIN categories cat2 ON cat2.ID=cat.parent
But ıt's not working.
You have to use LEFT JOIN to be able to pull all categories no matter have they parent category or not. INNER JOIN filters out all mismatches.
SELECT c.id, c.title, c.parent, p.title parent_title
FROM categories c LEFT JOIN categories p
ON c.parent = p.id
Output:
| ID | TITLE | PARENT | PARENT_TITLE |
-------------------------------------------------
| 1 | First Category | 0 | (null) |
| 2 | Other Category | 1 | First Category |
Here is SQLFiddle demo
If you want to get all parent categories then try query
SELECT cat_id FROM categories WHERE parent=0;
If you want get parent category of a category
SELECT C.cat_id, P.title FROM categories C LEFT JOIN categories P ON P.parent=C.cat_id;
I haven't tested above code but it should work fine.
You can always debug your SQL by entering it into a validator (loads online), phpMyAdmin's SQL tab or a editor with SQL validation. It looks to me like you have a small typo near your categories table selection.
Always dumb it down if your SQL isn't working. Note that JOINs (inner, left, right, ect) are meant to join TWO or MORE tables.
SELECT
one.ID, one.title, one.parent, one.title, one.parent, one.title
FROM
categories one
LEFT JOIN
categories two
ON
one.parent = two.ID

ECommerce, products in category and category browsing

I am building an EC website for a customer and the project manager came with some strange ideas and I am struggling to actually implement what he sold to the client.
Here comes my main issue and a quick summary how the system is setup: product are inside categories, categories could be children of an another category. So the category is presented as a tree on the left sidebar of the website.
The user can browse any category, even non "leaf" category, if the user click on non leaf category a listing like that should be presented for exemple on a level 1 category (same apply to level 2 categories):
big category 1
category level ( 3 or 2 )
product 1
product 2
product 3
category level ( 3 or 2 )
The things should also have some paging and present on 5 product on each page. Plus the category should be ordered in same fashion they appear in the menu on left side ... my DB scheme is like this:
+-------------+ +-------------+
+ category + + product +
+-------------+ +-------------+
+ category_id + + product_id +
+ parent_id + + category_id +
+-------------+ +-------------+
I cannot really figure out how I should code the SQL to make sure the product appear in order they should(like ordering product and categories has menu).
Also I am concerned about the performance of the whole setup, if the user select a non "leaf" category I would have to search all the child category and make a big category IN ( id1, id2, id3 ) and I know by experience long IN statement don't perform well.
If someone have encountered same design/issue and have some advice how to make it I would be grateful.
You could use the Materialized Path design. A directory path is an example of materialized path. That is, a series of ancestor values, concatenated together, with some character ("/" or "," are common) separating them.
So you might have categories:
+---------------------------------------------+
| cat_id | Name | cat_path | depth |
+---------------------------------------------+
| 1 | Electronics | 1/ | 1 |
| 2 | Digital cameras | 1/2/ | 2 |
| 3 | SLR cameras | 1/2/3/ | 3 |
| 4 | Audio | 1/4/ | 2 |
| 5 | Speakers | 1/4/5/ | 3 |
| 6 | Wall Satellites | 1/4/5/6/ | 4 |
| 7 | Computers | 1/7/ | 2 |
+---------------------------------------------+
Now if you want all products that are under Audio, you can do a query like:
SELECT p.*, pc.*
FROM Products p JOIN Categories pc ON (p.cat_id = pc.cat_id)
JOIN Categories c ON (pc.cat_path LIKE c.cat_path||'%')
WHERE c.name = 'Audio';
For example, '1/4/5/6' LIKE '1/4/%' is true, therefore Wall Satellites are included. And same for any other subcategory of Audio.
Re your question about menu rendering: I assume you'd want the menu to render:
- All ancestors of the chosen category
- All siblings of the ancestors of the chosen category
So if you choose 'Speakers', you'd see:
Electronics
Audio
Speakers
Computers
Digital Cameras
But you don't want descendants of Computers or Digital Cameras (i.e. "cousins" of Speakers).
SELECT uncle.name, uncle.depth
FROM Categories chosen
JOIN Categories ancestor ON (chosen.cat_path LIKE ancestor.cat_path||'%')
JOIN Categories uncle ON (ancestor.depth = uncle.depth
AND SUBSTRING(REVERSE(ancestor.cat_path), 3, 100) = SUBSTRING(REVERSE(uncle.cat_path), 3, 100))
WHERE chosen.name = 'Speakers'
ORDER BY uncle.depth, uncle.name;
I'm using a trick to detect uncles: compare the paths, after stripping the last element. To do this, reverse the string and then strip the first element. This should work at least in MySQL and MS SQL Server, but REVERSE() isn't standard and might not be portable to other brands of RDBMS.
Note that you should probably allow for more than one digit for each element in the cat_path, in which case the substring offset should also increase.
From a performance perspective this is a bad design. If a customer accidentally clicks on the toppermost category you would execute a query of your entire inventory. This will probably take an unacceptable amount of time. In web terms this translates to the customer losing patience, clicking over to your rival's site and never visiting your site again.
Of course, premature optimization is the root of all evil and all that, but it is a good idea to avoid doing completely dumb things.
I would also take issue with the whole idea of tree navigation as an approach. It smacks a bit too much of asking your customers to play a game of "Guess how we inventory our stock". Apart from anything else, in many spheres a product can belong to more than one category, so fitting them in a hierarchy is an arbitrary process. At the very least you probably ought to have a data model which supports assigning a product to multiple leaf categories. (This may depend on the nature of what you're selling and the granularity of your categories).
If your boss insists on their way then you still have some options to improve the performance of the query. For instance you could have a table which includes all the products joined by all their parent categories...
cat1 product1
cat1 product2
cat1 product3
cat1 product4
cat1 cat1.1 product1
cat1 cat1.1 product2
cat1 cat1.2 product3
cat1 cat1.2 product4
cat1 cat1.1 cat1.1.1 product1
cat1 cat1.1 cat1.1.2 product2
cat1 cat1.2 cat1.2.1 product3
cat1 cat1.2 cat1.2.2 product4
You would have to maintain this, through triggers or as a materialized view or through some other mechanism (depending on what your database flavour offers). But the overhead of maintaining it would be neglible compared to the performance benefits of not having to re-assemble the product hierarchy for every customer query. Besides it is unlikely you have that much volatility in your inventory.

mySQL query for selecting children

I am not sure if this is possible in mySQL. Here are my tables:-
Categories table:
id
name
parent_id (which points to Categories.id)
I use the above table to map all the categories and sub-categories.
Products table:
id
name
category_id
The category_id in the Products table points to the sub-category id in which it belongs.
e.g. If I have Toys > Educational > ABC where ABC is product, Toys is Category and Educational is sub Category, then ABC will have category_id as 2.
Now the problem is that I want to use a SQL query to display all the products (in all the sub-categories and their sub-categories.. n level) for a particular category.
e.g.:
select * from categories,products where category.name = 'Toys' and ....
The above query should display the products from Educational also and all other sub categories and their subcategories.
Is this possible using a mySQL query? If not what options do I have? I would like to avoid PHP recursion.
Update: Basically I want to display the top 10 products in the main category which I will be doing by adding a hits column to products table.
What I've done in previous projects where I've needed to do the same thing, I added two new columns.
i_depth: int value of how deep the category is
nvc_breadcrumb: complete path of the category in a breadcrumb type of format
And then I added a trigger to the table that houses the category information to do the following (all three updates are in the same trigger)...
-- Reset all branches
UPDATE t_org_branches
SET nvc_breadcrumb = NULL,
i_depth = NULL
-- Update the root branches first
UPDATE t_org_branches
SET nvc_breadcrumb = '/',
i_depth = 0
WHERE guid_branch_parent_id IS NULL
-- Update the child branches on a loop
WHILE EXISTS (SELECT * FROM t_branches WHERE i_depth IS NULL)
UPDATE tobA
SET tobA.i_depth = tobB.i_depth + 1,
tobA.nvc_breadcrumb = tobB.nvc_breadcrumb + Ltrim(tobA.guid_branch_parent_id) + '/'
FROM t_org_branches AS tobA
INNER JOIN t_org_branches AS tobB ON (tobA.guid_branch_parent_id = tobB.guid_branch_id)
WHERE tobB.i_depth >= 0
AND tobB.nvc_breadcrumb IS NOT NULL
AND tobA.i_depth IS NULL
And then just do a join with your products table on the category ID and do a "LIKE '%/[CATEGORYID]/%' ". Keep in mind that this was done in MS SQL, but it should be easy enough to translate into a MySQL version.
It might just be compatible enough for a cut and paste (after table and column name change).
Expansion of explanation...
t_categories (as it stands now)...
Cat Parent CategoryName
1 NULL MyStore
2 1 Electronics
3 1 Clothing
4 1 Books
5 2 Televisions
6 2 Stereos
7 5 Plasma
8 5 LCD
t_categories (after modification)...
Cat Parent CategoryName Depth Breadcrumb
1 NULL MyStore NULL NULL
2 1 Electronics NULL NULL
3 1 Clothing NULL NULL
4 1 Books NULL NULL
5 2 Televisions NULL NULL
6 2 Stereos NULL NULL
7 5 Plasma NULL NULL
8 5 LCD NULL NULL
t_categories (after use of the script I gave)
Cat Parent CategoryName Depth Breadcrumb
1 NULL MyStore 0 /
2 1 Electronics 1 /1/
3 1 Clothing 1 /1/
4 1 Books 1 /1/
5 2 Televisions 2 /1/2/
6 2 Stereos 2 /1/2/
7 5 LCD 3 /1/2/5/
8 7 Samsung 4 /1/2/5/7/
t_products (as you have it now, no modifications)...
ID Cat Name
1 8 Samsung LNT5271F
2 7 LCD TV mount, up to 36"
3 7 LCD TV mount, up to 52"
4 5 HDMI Cable, 6ft
Join categories and products (where categories is C, products is P)
C.Cat Parent CategoryName Depth Breadcrumb ID p.Cat Name
1 NULL MyStore 0 / NULL NULL NULL
2 1 Electronics 1 /1/ NULL NULL NULL
3 1 Clothing 1 /1/ NULL NULL NULL
4 1 Books 1 /1/ NULL NULL NULL
5 2 Televisions 2 /1/2/ 4 5 HDMI Cable, 6ft
6 2 Stereos 2 /1/2/ NULL NULL NULL
7 5 LCD 3 /1/2/5/ 2 7 LCD TV mount, up to 36"
7 5 LCD 3 /1/2/5/ 3 7 LCD TV mount, up to 52"
8 7 Samsung 4 /1/2/5/7/ 1 8 Samsung LNT5271F
Now assuming that the products table was more complete so that there is stuff in each category and no NULLs, you could do a "Breadcrumb LIKE '%/5/%'" to get the last three items of the last table I provided. Notice that it includes the direct items and children of the category (like the Samsung tv). If you want ONLY the specific category items, just do a "c.cat = 5".
I think the cleanest way to achieve this would be to use the nested set model. It's a bit complicated to implement, but powerful to use. MySQL has a tutorial named Managing Hierarchical Data in MySQL. One of the big SQL gurus Joe Celko wrote about the same thing here. If you need even more information have a look at Troel's links on storing hierarchical data.
In my case I would stay away from using a RDBMS to store this kind of data and use a graph database instead, as the data in this case actually is a directed graph.
Add a column to the Categories table that will contain the complete comma-delimited tree for each group. Using your example, sub-category Educational would have this as the tree '1,2', where 1 = Toys, 2 = Educational (it includes itself). The next nested level of categories would keep adding to the tree.
To get all products in a group, you use MySQL's FIND_IN_SET function, like so
SELECT p.ID
FROM Products p INNER JOIN Categories c ON p.category_ID = c.ID
WHERE FIND_IN_SET(your_category_id, c.tree)
I wouldn't use this method for big tables, as I don't think this query can use an index.
One way is to maintain a table that contains the ancestor to descendant relationships. You can query this particular table and get the list of all dependents.
Assuming MySQL, it'll be difficult to avoid recursion in PHP.
Your question is, essentially, how to mimic Oracle's CONNECT BY PRIOR syntax in MySQL. People ask this question repeatedly but it's a feature that's never made it in to MySQL and implementing is via stored procedures probably won't work because (now) stored functions cannot be recursive.
Beware of the database kludges offered so far.
The best information so far are the three links from nawroth:
Managing Hierarchical Data in MySQL, including the Nested Set Model.
Trees in SQL - nested set model by Joel Celko
Troels' links: Relational database systems: Hierarchical data in RDBMSs
How big is the table Categories? You may need to cache this on the application level and construct the appropriate query: ... where id in (2, 3, 6, 7)
Also, it's best if you fetch categories by id which is their unique ID, indexed and fast as opposed to finding by name.
Bear with me, because I have never done something like this.
BEGIN
SET cat = "5";
SET temp = "";
WHILE STRCMP(temp, cat) != 0 DO
SET temp = cat;
SET cat = SELECT CONCAT_WS(GROUP_CONCAT(id), cat) FROM Categories GROUP BY (parent_id) HAVING FIND_IN_SET(parent_id, cat);
END LOOP;
END;
SELECT * FROM products WHERE FIND_IN_SET(category_id, cat)
I can almost guarantee the above won't work, but you can see what I'm trying to do. I got this far and I just decided to not finish the end of the query (select the top N from each category), sorry. :P

Categories