Select tree path in MySQL table - mysql

I have a MySQL table like this:
| CategoryId | Name | CategoryParentId |
|------------|---------------|------------------|
| 0 | Tech Support | (null) |
| 1 | Configuration | 0 |
| 2 | Questions | 1 |
| 3 | Sales | (null) |
| 4 | Questions | 3 |
| 5 | Other | (null) |
This is the output I desire when a query the ID 2 (for example):
Tech Support/Configuration/Questions
How do I do this without having to do multiple joins?
Fiddle
EDIT: Not sure if is the best way to do this, but I solved by creating a function:
DELIMITER $$
CREATE FUNCTION get_full_tree (CategoryId int) RETURNS VARCHAR(200)
BEGIN
SET #CategoryParentId = (SELECT CategoryParentId FROM category c WHERE c.CategoryId = CategoryId);
SET #Tree = (SELECT Name FROM category c WHERE c.CategoryId = CategoryId);
WHILE (#CategoryParentId IS NOT NULL) DO
SET #ParentName = (SELECT Name FROM category c WHERE c.CategoryId = #CategoryParentId);
SET #Tree = CONCAT(#ParentName, '/', #Tree);
SET #CategoryParentId = (SELECT CategoryParentId FROM category c WHERE c.CategoryId = #CategoryParentId);
END WHILE;
RETURN #Tree;
END $$
DELIMITER ;
I can now do this query:
SELECT CategoryId, get_full_tree(CategoryId) FROM category

You could create a new table, lets name it as hierarchy (could be a better name) where we would store all the ancestry of a category.
CREATE TABLE `hierarchy` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`parent` int(11) NOT NULL,
`child` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
For example in this case for Questions i.e. ID->2 we will have the below entries:
id parent child
====================
6 0 2
7 1 2
8 2 2
For the whole example the content of the table would be:
id parent child
===========================
1 0 0
2 3 3
3 5 5
4 0 1
5 1 1
6 0 2
7 1 2
8 2 2
9 3 4
10 4 4
Now whenever you want to retrieve the whole ancestry of node execute the below query:
select name from category where id in (select parent from hierarchy where child = 2 order by id ASC)
The above query will return all the ancestry names for the Questions (ID->2) i.e.
name
==================
Tech Support
Configuration
Questions
For completeness shake below is the content for category table
id Name
============================
0 Tech Support
1 Configuration
2 Questions
3 Sales
4 Questions
5 Other
N.B. This is just an idea i am sure you can definitely build more elegant solution on top of it.

If you are using MySQL 8 or above you can use Common Table Expressions for recursive queries. Query would be the following
WITH RECURSIVE CategoryPath (CategoryId, Name, path) AS
(
SELECT CategoryId, Name, Name as path
FROM category
WHERE CategoryParentId IS NULL
UNION ALL
SELECT c.CategoryId, c.Name, CONCAT(cp.path, ' / ', c.Name)
FROM CategoryPath AS cp JOIN category AS c
ON cp.CategoryId = c.CategoryParentId
)
SELECT * FROM CategoryPath ORDER BY path;

Related

Select locations where the user doesn't have it bound yet from 3 tables

I have 3 tables
User Info
id
name
1
bob
2
jane
3
tom
Locations
id
name
1
Test1
2
Test2
3
Test3
4
Test4
User Locations
userID
locationID
1
1
1
2
2
3
Basically What I am trying to achieve is to pull the location names where the user doesn't have it bound already.
In the above list Bob has 2 locations bounded "test 1" and "test 2" but he doesn't have "test 3" or "test 4" yet. I Only want the data to return test 3 and 4 since those are the only ones Bob doesn't have.
For Jane She only has Test 3 bounded but none of the remaining 3
Originally I had tried this and it somewhat worked. However Every time another user gets an unbounded location the its removed from the list. I'm not sure how I would add the user ID in all this so it's only specific to that user.
SELECT `name` FROM `locations`
WHERE `id` NOT IN (SELECT `locationID` FROM `user_locations`)
Create a cartesain product of the user and locations table (cross join), then using an outer join allows us to find rows that are as yet unmatched in user_locations:
select
user_info.ID AS UserID
, locations.ID AS locationID
from user_info
cross join locations
left outer join user_locations on user_info.id = user_locations.userid
and locations.id = user_locations.locationid
where user_locations.userid IS NULL
and user_info.name = 'bob'
SQL Fiddle
MySQL 5.6 Schema Setup:
CREATE TABLE user_info(
id INTEGER NOT NULL PRIMARY KEY
,name VARCHAR(4) NOT NULL
);
INSERT INTO user_info(id,name) VALUES (1,'bob');
INSERT INTO user_info(id,name) VALUES (2,'jane');
INSERT INTO user_info(id,name) VALUES (3,'tom');
CREATE TABLE locations(
id INTEGER NOT NULL PRIMARY KEY
,name VARCHAR(5) NOT NULL
);
INSERT INTO locations(id,name) VALUES (1,'Test1');
INSERT INTO locations(id,name) VALUES (2,'Test2');
INSERT INTO locations(id,name) VALUES (3,'Test3');
INSERT INTO locations(id,name) VALUES (4,'Test4');
CREATE TABLE user_locations(
userID INTEGER NOT NULL
,locationID INTEGER NOT NULL
);
INSERT INTO user_locations(userID,locationID) VALUES (1,1);
INSERT INTO user_locations(userID,locationID) VALUES (1,2);
INSERT INTO user_locations(userID,locationID) VALUES (2,3);
Query 1:
select
user_info.ID AS UserID
, locations.ID AS locationID
from user_info
cross join locations
left outer join user_locations on user_info.id = user_locations.userid
and locations.id = user_locations.locationid
where user_locations.userid IS NULL
order by 1,2
Results:
| UserID | locationID |
|--------|------------|
| 1 | 3 |
| 1 | 4 |
| 2 | 1 |
| 2 | 2 |
| 2 | 4 |
| 3 | 1 |
| 3 | 2 |
| 3 | 3 |
| 3 | 4 |

Search for multiple conditions on same column?

In SQL, how to select all rows that satisfy multiple AND conditions (not OR) of the same column?
Such as: "select all users who speak these 3 languages (language_id == 1 AND language_id == 2 AND language_id ==3)" where language_id is a column
id | user_id | language_id
--------------------------
1 | 1 | 1
2 | 2 | 1
3 | 2 | 2
4 | 3 | 1
5 | 4 | 1 << which users speak this language )
6 | 4 | 2 << AND speak this language ) => expected result: user_id == 4
7 | 4 | 3 << AND speak this language )
8 | 5 | 1
9 | 6 | 1
10 | 7 | 1
First select all users having the wanted languages, count the languages and then restrict the users to have the language count equal to the count of the wanted languages.
SELECT user_id
FROM (SELECT used_id,COUNT(*)
FROM [table_name]
WHERE language_id IN (1,2,3)
GROUP BY user_id
HAVING COUNT(*)=3
) i;
You have some ways to do that.
One simple way would be to create a view returning the user_id with the count for each language, for example:
CREATE VIEW myView
AS
SELECT DISTINCT T0.user_id
,(SELECT COUNT(*) FROM [myTable] WHERE user_id=T0.user_id AND language_id=1) AS L1
,(SELECT COUNT(*) FROM [myTable] WHERE user_id=T0.user_id AND language_id=2) AS L2
,(SELECT COUNT(*) FROM [myTable] WHERE user_id=T0.user_id AND language_id=3) AS L3
FROM [myTable] AS T0
GO
You should get a result like this:
Then, you only need to query this view, getting just the user_id where L1>0 AND L2>0 AND L3>0...
SELECT * FROM myView WHERE L1>0 AND L2>0 AND L3>0
Regards,

MySQL join "connected" intervals

Lets say I have table
teach_subject(teacher_id, subject_id, min_grade_of_school, max_grade_of_school, color_in_timetable)
Example data:
teacher_id subject_id min_grade_of_school max_grade_of_school color_in_timetable
1 2 1 4 #A00
1 2 5 6 #0A0
1 2 9 11 #00A
1 3 1 7 #AA0
It is not allowed that min_grad_of_school > max_grad_of_school, but maybe they are equal.
It is also not allowed that for a given tuple (t_id_new, s_id_new, min_grade_new, max_grade_new, color_new) there exists an other tuple (t_id, s_id, min_grade, max_grade, color) in the table holding
t_id_new = t_id and s_id_new = s_id and
( min_grade <= min_grade_new <= max_grade or min_grade <= max_grade_new <= max_grade )
So for the given example a new tuple like (1,2,6,11,#FFF) or (1,2,2,7,#FFF) is not possible.
So far no problem.
Now I want to ignore the color and join the "connected" rows, i.e. if teacher 1 teaches subject 2 from the 1st grade to the 4th grade, and from the 5th grade to the 6th grade, you can also say he teaches subject 2 from 1st to 6th grade.
So I want to "join" the tuples (1,2,1,4) and (1,2,5,6) to (1,2,1,6) but i dont want to join (1,2,4,5) and (1,2,9,11), since ther is a (integer) gap between 5 and 9.
I just have no idea if there is a way to do this with MySQL. At the moment I just select all the data an edit the selected data with PHP. Is there a MySQL way to directly select what I want or should stick to PHP?
Edit
Example result (for the above example data) :
teacher_id subject_id min_grade_of_school max_grade_of_school color_in_timetable
1 2 1 6 #A00
1 2 9 11 #00A
1 3 1 7 #AA0
Edit 2
Maybe can use a stored procedure?
DROP TABLE IF EXISTS teach_subject;
CREATE TABLE teach_subject
(teacher_id INT NOT NULL
,subject_id INT NOT NULL
,min_g INT NOT NULL
,max_g INT NOT NULL
,color_in_timetable CHAR(4) NOT NULL
,PRIMARY KEY(teacher_id,subject_id,min_g)
);
INSERT INTO teach_subject VALUES
(1,2,1,4,'#A00'),
(1,2,5,6,'#0A0'),
(1,2,9,11,'#00A'),
(1,3,1, 7,'#AA0');
SELECT a.teacher_id
, a.subject_id
, a.min_g
, MIN(c.max_g) max_g
, a.color_in_timetable
FROM teach_subject a
LEFT
JOIN teach_subject b
ON b.teacher_id = a.teacher_id
AND b.subject_id = a.subject_id
AND b.min_g = a.max_g - 1
LEFT
JOIN teach_subject c
ON c.teacher_id = a.teacher_id
AND c.subject_id = a.subject_id
AND c.min_g >= a.min_g
LEFT
JOIN teach_subject d
ON d.teacher_id = a.teacher_id
AND d.subject_id = a.subject_id
AND d.min_g = c.max_g + 1
WHERE b.teacher_id IS NULL
AND c.teacher_id IS NOT NULL
AND d.teacher_id IS NULL
GROUP
BY a.teacher_id,a.subject_id,a.min_g;
+------------+------------+-------+-------+--------------------+
| teacher_id | subject_id | min_g | max_g | color_in_timetable |
+------------+------------+-------+-------+--------------------+
| 1 | 2 | 1 | 6 | #A00 |
| 1 | 2 | 9 | 11 | #00A |
| 1 | 3 | 1 | 7 | #AA0 |
+------------+------------+-------+-------+--------------------+

Passing the results of a Mysql query to a subquery on the same table

CREATE TABLE test (
id INT(12),
time VARCHAR(16),
group INT(2),
taken TINYINT(1),
RID int(11) NOT NULL auto_increment,
primary KEY (RID));
id | time | group | taken
---------------------------
1 | 13.00| 1 | 1
---------------------------
2 | 13.00| 2 | 0
---------------------------
3 | 14.00| 2 | 0
---------------------------
4 | 15.00| 2 | 0
---------------------------
5 | 12.00| 3 | 0
Having a table structure and sample data as above, I want to get the smallest "group" number which has not been "taken" (taken=0)
I have come with two queries :
SELECT * From `test`
WHERE taken=0
and
SELECT * FROM `test`
WHERE `group` = ( SELECT MIN(`group`) FROM `test` )
Can someone show me how to combine the two queries so that I can pass the results of the first query to the second query to get as below.
id | time | group | taken
---------------------------
2 | 13.00| 2 | 0
---------------------------
3 | 14.00| 2 | 0
---------------------------
4 | 15.00| 2 | 0
---------------------------
You can use the result of the first query in the second query as follows:
SELECT *
FROM TEST
WHERE `group` = (SELECT MIN(`group`)
FROM `test`
WHERE taken = 0)
Which gives you the desired result according to this SQLFiddle
Use the sub query to get the lowest group for taken of 0. Join your main table to the results of the sub query.
Something like this:-
SELECT a.*
From `test` a
INNER JOIN
(
SELECT MIN(`group`) AS min_group
FROM `test`
WHERE taken=0
) b
ON a.taken = b.taken
AND a.`group` = b.min_group
try this:
SELECT min(`group`) FROM (
SELECT * FROM test
WHERE taken = 0)
AS t;

SQL nested order by?

I'm sure that this has been asked before, but I don't know what to call it exactly to find the answer.
I have a table of categories and sub categories. They each have an id and a parent id. If it is a top level category, the parent id is 0. Sub categories have the parent id set to the
category id of it's parent.
category_id # The ID for this record
category_name # The name of the category
parent_id # The parent ID for this category
display_order # Order of categories within their grouping
1 A 0 0 # First primary category
2 a1 1 0 # Subcategory, parent is A, display_order is 0
3 a2 1 1
4 a3 1 2
5 B 0 1 # Second primary category
6 b1 5 0 # Subcategory, parent is B, display_order is 0
7 b2 5 1
8 b3 5 2
I'm trying to write an SQL query that will give me all of the categories in this order:
A, a1, a2, a3, B, b1, b2, b3
SELECT * FROM categories ORDER BY display_order
Is this possible in SQL, or will I need to use multiple queries
Thanks,
Brad
Something like this might maybe work:
SELECT *
FROM categories
ORDER BY IF(parent_id, parent_id, category_id), parent_id, display_order
but since it can't use an index, it'll be slow. (Didn't test though, might be wrong)
The first ORDER BY condition sorts parents and children together; then the second one ensures the parent precedes its children; the third sorts the children among themselves.
Also, it will obviously work only in the case you directly described, where you have a two-level hierarchy.
an answer has already been accepted, but i thought i would share my thoughts on this anyways. i tried to sort the main categories after their display_order column as well. here's my table
mysql> select * from categories;
+-------------+---------------+-----------+---------------+
| category_id | category_name | parent_id | display_order |
+-------------+---------------+-----------+---------------+
| 1 | B | 0 | 2 |
| 2 | C | 0 | 3 |
| 3 | b2 | 1 | 2 |
| 4 | b1 | 1 | 1 |
| 5 | c3 | 2 | 3 |
| 6 | A | 0 | 1 |
| 7 | c2 | 2 | 2 |
| 8 | b3 | 1 | 3 |
| 9 | a2 | 6 | 2 |
| 10 | a1 | 6 | 1 |
| 11 | c1 | 2 | 1 |
| 12 | a3 | 6 | 3 |
+-------------+---------------+-----------+---------------+
12 rows in set (0.00 sec)
as you see, i have taken great care to add the categories in a none linear order :)
my query:
SELECT
sub_id AS category_id,
sub_name AS category_name,
sub_parent_id AS parent_id,
main_order + sub_order AS display_order
FROM (
SELECT
c1.display_order + c1.display_order * (
SELECT
inner_c.display_order
FROM
categories AS inner_c
WHERE
inner_c.parent_id <> 0
ORDER BY
inner_c.display_order DESC
LIMIT 1) AS main_order,
c2.display_order AS sub_order,
c2.category_name AS sub_name,
c2.category_id AS sub_id,
c2.parent_id AS sub_parent_id
FROM
categories AS c1
JOIN
categories AS c2
ON
c1.category_id = c2.parent_id
WHERE
c1.parent_id = 0
) AS renumbered
UNION ALL
SELECT
category_id,
category_name,
parent_id,
display_order + display_order * (
SELECT
inner_c.display_order
FROM
categories AS inner_c
WHERE
inner_c.parent_id <> 0
ORDER BY
inner_c.display_order DESC
LIMIT 1) AS display_order
FROM
categories
WHERE
parent_id = 0
ORDER BY
display_order;
Sounds almost identical to another I've answered with similar parent/child hierarchy while retaining child elements at same grouped level as its corresponding parent...Check this thread
Whenever possible, I build SQL incrementally, not least because it gives me the option of testing as I go.
The first thing we need to be able to do is identify the top-level categories:
SELECT category_id AS tl_cat_id,
category_name AS tl_cat_name,
display_order AS tl_disp_order
FROM Categories
WHERE parent_id = 0;
Now we need to join that with the categories and subcategories to get the result:
SELECT t.tl_cat_id, t.cat_name, t.tl_disp_order, c.category_id, c.category_name,
CASE WHEN c.parent_id = 0 THEN 0 ELSE c.display_order END AS disp_order
FROM Categories AS c
JOIN (SELECT category_id AS tl_cat_id,
category_name AS tl_cat_name,
display_order AS tl_disp_order
FROM Categories
WHERE parent_id = 0) AS t
ON c.tl_cat_id = t.parent_id OR (c.parent_id = 0 AND t.tl_cat_id = c.category_id)
ORDER BY tl_disp_order, disp_order;
The join condition is unusual but should work; it collects rows where the parent ID is the same as the current category ID, or rows where the parent ID is 0 but the category ID is the same. The ordering is then almost trivial - except that when you are dealing with the sub-category ordering, you want the parent item at the front of the list. The CASE expression handles that mapping.