MySQL Select parents and childs in proper order with single query - mysql

I have a MySQL table with following data:
ID Name ParentID
1 Foo null
2 Bar null
3 Foo SubA 1
4 Bar SubA 2
5 Foo SubC 1
6 Foo SubB 1
I would like to retreive all data with following order:
1 Foo null
3 Foo SubA 1
6 Foo SubB 1
5 Foo SubC 1
2 Bar null
4 Bar SubA 2
Is it possible with MySQL and single query?

If this is a two-level hierarchie, i.e. no grandparents and grandchildren, it's a mere ORDER BY clause:
select id, name, parentid
from mytable
order by coalesce(parentid, id), parentid is not null, name;
This makes use of MySQL's true = 1, false = 0. parentid is not null is 0 for the parent and 1 for the children.

You could use recursive CTE (MySQL 8.0+):
-- 2 level hierarchy (parent-child)
WITH RECURSIVE cte AS
(
SELECT tx.*, 1 AS lvl, ID AS grp FROM tx WHERE ParentID IS NULL
UNION ALL
SELECT tx.*, lvl+1, cte.ID FROM tx JOIN cte WHERE tx.ParentId = cte.Id
)
SELECT ID, Name, ParentId
FROM cte
ORDER BY grp, lvl, Name;
DBFiddle Demo

Related

How to ORDER BY in each GROUP BY

i have data like this:
Name
Class
Pos
Nam
A
5
Hung
B
1
Tran
A
6
Hoang
A
1
Bao
B
4
so now i want to make query on this table to group all students by class, in each class, studen will be sorted descending by index.
My expected table be like:
Name
Class
Pos
Tran
A
6
Nam
A
5
Hoang
A
1
Bao
B
4
Hung
B
1
Here is my query
SELECT * FROM
(
SELECT *
from
(
SELECT 'Nam' as name, "A" as class, 5 as pos
UNION
SELECT 'Hung' as name, "B" as class, 1 as pos
UNION
SELECT 'Tran' as name, "A" as class, 6 as pos
UNION
SELECT 'Hoang' as name, "A" as class, 1 as pos
UNION
SELECT 'Bao' as name, "B" as class, 4 as pos
)t0
ORDER BY pos DESC
)t1
GROUP BY name, class
But the output look so different:
Name
Class
Pos
Nam
A
5
Hung
B
1
Tran
A
6
Hoang
A
1
Bao
B
4
if i filter only class A, it look like
Name
Class
Pos
Hoang
A
1
Tran
A
6
Nam
A
5
only correct for class B:
Name
Class
Pos
Bao
B
4
Hung
B
1
Look like the order in sub-query can not be kept after group by.
My finally query will look like:
SELECT class as Class, COLLECT_LIST(name) as Name FROM
(
SELECT *
from
(
SELECT 'Nam' as name, "A" as class, 5 as pos
UNION
SELECT 'Hung' as name, "B" as class, 1 as pos
UNION
SELECT 'Tran' as name, "A" as class, 6 as pos
UNION
SELECT 'Hoang' as name, "A" as class, 1 as pos
UNION
SELECT 'Bao' as name, "B" as class, 4 as pos
)t0
ORDER BY pos DESC
)t1
GROUP BY class
If GROUP BY can keep order base on pos in sub-query then COLLECT_LIST will produce expected result like this:
Class
Name
A
[Tran, Nam,Hoang]
B
[Bao,Hung]
How to group and order internal in each group?
Thanks
The ORDER BY, in the middle of your query is useless. It should be put after the GROUP BY.
SELECT * FROM
(
SELECT *
from
(
SELECT 'Nam' as name, 'A' as class, 5 as pos
UNION
SELECT 'Hung' as name, 'B' as class, 1 as pos
UNION
SELECT 'Tran' as name, 'A' as class, 6 as pos
UNION
SELECT 'Hoang' as name, 'A' as class, 1 as pos
UNION
SELECT 'Bao' as name, 'B' as class, 4 as pos
)t0
)t1
WHERE class='A'
GROUP BY name, class
ORDER BY pos DESC;
output:
+-------+-------+-----+
| name | class | pos |
+-------+-------+-----+
| Tran | A | 6 |
| Nam | A | 5 |
| Hoang | A | 1 |
+-------+-------+-----+
An explanation about ORDER BY can be found here: https://mariadb.com/kb/en/why-is-order-by-in-a-from-subquery-ignored/
A "table" (and subquery in the FROM clause too) is - according to the
SQL standard - an unordered set of rows. Rows in a table (or in a
subquery in the FROM clause) do not come in any specific order.
EDIT: If you have ONLY_FULL_GROUP_BY enabled (which is the default in MySQL8.0+), you will get an error like this:
ERROR 1055 (42000): Expression #3 of SELECT list is not in GROUP BY
clause and contains nonaggregated column 't1.pos' which is not
functionally dependent on columns in GROUP BY clause; this is
incompatible with sql_mode=only_full_group_by
This is because grouping is done on name and class so SQL does not know which value to use for pos in that group. Should it that the highest value? of the lowest value? (or some other value...).
I did not notice (not enough coffee...). A re-write with this problems solved looks like this:
SELECT
name,
class,
min(pos) as pos
FROM
(
SELECT 'Nam' as name, "A" as class, 5 as pos
UNION
SELECT 'Hung' as name, "B" as class, 1 as pos
UNION
SELECT 'Tran' as name, "A" as class, 6 as pos
UNION
SELECT 'Hoang' as name, "A" as class, 1 as pos
UNION
SELECT 'Bao' as name, "B" as class, 4 as pos
)t1
WHERE class="A"
GROUP BY name, class
ORDER BY pos DESC;
If I understand your requirement correctly, you just need a two level sort first by class, then by index:
SELECT Class, GROUP_CONCAT(Name ORDER BY Pos DESC) AS Name
FROM yourTable
GROUP BY Class
ORDER BY Class;

Find the level segments up-to 7-levels in parent-child hierarchy

Parent-child table
id
introducer_id
name
1
NULL
Riya
2
1
Ramesh
3
1
Anand
4
2
Preety
5
3
Rakesh
Query to get members list when id = 1
select id AS memberid, name
from (select *
from table_member
order by introducer_id, id) table_member_sorted,
(select #pv := '1') initialisation
where find_in_set(introducer_id, #pv)
and length(#pv := concat(#pv, ',', id))
The above query displays output
member_id
name
2
Ramesh
3
Anand
4
Preety
5
Rakesh
Now i also want to find the level of all the members
(i.e) i want the output as:
member_id
name
level
2
Ramesh
1
3
Anand
1
4
Preety
2
5
Rakesh
2
How can i update my previous query to get the above output?
Note: Query to display maximum level upto 7
Thank You.
CREATE PROCEDURE get_tree (start_from INT)
BEGIN
DROP TABLE IF EXISTS tmp;
CREATE TABLE tmp (member_id INT PRIMARY KEY,
name VARCHAR(255),
level INT);
INSERT INTO tmp SELECT id, name, 0
FROM test
WHERE id = start_from;
REPEAT
INSERT IGNORE INTO tmp SELECT test.id, test.name, tmp.level + 1
FROM test
JOIN tmp ON tmp.member_id = test.introducer_id;
UNTIL !ROW_COUNT() END REPEAT;
SELECT * FROM tmp;
END
fiddle
I have shared screenshot link of my sql version. snipboard.io/qbdB0A.jpg – AfreenB
Your SQL server version is MariaDB 10.4.14. – Akina
For your server version use
WITH RECURSIVE
cte AS ( SELECT id member_id, name, 0 level
FROM test
WHERE id = #start_from
UNION ALL
SELECT test.id, test.name, cte.level + 1
FROM test
JOIN cte ON cte.member_id = test.introducer_id
-- WHERE cte.level < 7 )
SELECT *
FROM cte;
fiddle

Multiple Aggregate with different Group By

I have table User, Company, ParentCompany and table Goal.
Each Company have ParentCompany, and each User inside one Company. Goal have number of action, and type of Goal, user who execute the goal, and time executed.
I want to calculate the number of action in a date range for each type of Goal, for each user, company, and parent_company. Number of action for each company equal to sum of action for user that reside in that company.
More or less after some join query, I able to get this table below, where column id is id of company, parent_id is id of companyparent, and num is number of goal for all user inside of id company.
id parent_id num
----------- -------------------- -----------------------
1 3 1
2 1 2
3 1 1
4 2 4
Now I want to make it like below:
id parent_id sum_id sum_parent
----------- -------------------- -------------- -------------
1 3 1 1
2 1 2 3
3 1 1 3
4 2 4 4
How can I make it works? I can get one of the value (sum_id or sum_parent) with GROUP BY,
SELECT id,SUM(num) AS sum_id FROM tableA GROUP BY id
or
SELECT parent_id,SUM(num) AS sum_parent FROM tableA GROUP BY parent_id
but is there any way to make it only in one query? tableA results from query with 5 join inside.
Try this:
SELECT a.id, a.parent_id, a.sum_id, b.sum_parent
FROM (SELECT id, parent_id, SUM(num) sum_id FROM tableA a GROUP BY id) a
INNER JOIN (SELECT parent_id, SUM(num) sum_parent FROM tableA a GROUP BY parent_id) b ON a.parent_id = b.parent_id
Try this :
SELECT
t1.id, t1.parent_id, t1.sum_id, t2.sum_parent
FROM
(SELECT id, parent_id, SUM(num) AS sum_id FROM mytable GROUP BY id) t1
INNER JOIN
(SELECT parent_id, SUM(num) AS sum_parent FROM mytable GROUP BY parent_id) t2
ON t1.parent_id = t2.parent_id
Apparently what I want can be done with WITH ROLLUP statement. (http://dev.mysql.com/doc/refman/5.0/en/group-by-modifiers.html)
With
SELECT id,sum(num) FROM tableA GROUP BY parent_id, id WITH ROLLUP;
will results in
parent_id id sum(num)
----------- -------------------- --------------
1 2 2
1 3 1
1 null 3
2 4 4
2 null 4
3 1 2
3 null 2

SQL select to eliminate duplicate value that has 2 other values in next column

I have constructed a junction table which goes like this.
Table Name: myTable
p_id | c_id
-----------
1 1
1 2
1 3
2 2
2 3
3 2
3 3
3 4
I wanted to SELECT p_id that doesn't have both c_id 3 and 4. In this case only p_id 3 has both c_id 3 and 4 so after the select statement the query should return both p_id 1 and 2.
The thing is that I try different kind of method but still it wouldn't work. I really need help.
my query
1.) SELECT DISTINCT p_id FROM myTable WHERE c_id != 3 AND course_id != 4;
Problem: It still returns 3 as one of the result since 3 has c_id of 2
Something like this:
SELECT DISTINCT p_id
FROM mytable
WHERE p_id NOT IN (SELECT p_id
FROM mytable
WHERE c_id IN ( 3, 4 )
GROUP BY p_id
HAVING Count(DISTINCT c_id) = 2)
SQLFiddle demo
Try this:
SELECT DISTINCT p_id
FROM myTable
WHERE c_id IN (3,4)
GROUP BY p_id HAVING COUNT(DISTINCT c_id)<2
The straightforward solution is to use exists:
select
distinct p_Id
from myTable t
where not (exists (select 1
from myTable
where (c_id = 3) and
(p_id = t.p_id)) and
exists (select 1
from myTable
where (c_id = 4) and
(p_id = t.p_id)))
Try this:
SELECT mytable.p_id
FROM mytable
LEFT OUTER JOIN (SELECT v1.p_id
FROM (SELECT p_id
FROM mytable
WHERE c_id = 3) v1
INNER JOIN (SELECT p_id
FROM mytable
WHERE c_id = 4) v2
ON v1.p_id = v2.p_id) v
ON mytable.p_id = v.p_id
WHERE v.p_id IS NULL
GROUP BY mytable.p_id
Try this:
select distinct mytable.p_id from mytable where c_id not in (3,4) and p_id <>3
This will give result which does not have 3 and 4

first item used by a user

I am writing a query to grab the items that a specific user_id was the first to use. Here is some sample data -
item_id used_user_id date_used
1 1 2012-08-25
1 2 2012-08-26
1 3 2012-08-27
2 2 2012-08-27
3 1 2012-08-27
4 1 2012-08-21
4 3 2012-08-24
5 3 2012-08-23
query
select item_id as inner_item_id, ( select used_user_id
from test
where test.item_id = inner_item_id
order by date_used asc
limit 1 ) as first_to_use_it
from test
where used_user_id = 1
group by item_id
It returns the correct values
inner_item_id first_to_use_it
1 1
3 1
4 1
but the query is VERY slow on a giant table. Is there a certain index that I can use or a better query that I can write?
i can't get exactly what you mean because in your inner query you have sorted it by their used_user_id and and on your outer query you have filtered it also by their userid. Why not do this directly?
SELECT DISTINCT item_id AS inner_item_id,
used_user_id AS first_to_use_it
FROM test
WHERE used_user_id = 1
UPDATE 1
SELECT b.item_id,
b.used_user_id AS first_to_use_it
FROM
(
SELECT item_ID, MIN(date_used) minDate
FROM tableName
GROUP BY item_ID
) a
INNER JOIN tableName b
ON a.item_ID = b.item_ID AND
a.minDate = b.date_used
WHERE b.used_user_id = 1