MySQL With Recursive - mysql

I need to return my categories with the item quantity in each department, the parent categories must inherit from the child categories the quantity of products in each category.
Categories
+-------------+----------------------+--------+
| id | name | parent_id |
+-------------+----------------------+--------+
| 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 |
+-------------+----------------------+--------+
Product
+-------------+----------------------+--------+
| id | product | category_id |
+-------------+----------------------+--------+
| 1 | TV LCD 32" | 4 |
| 2 | TV LCD 45" | 4 |
| 3 | TV TUBE 29" | 3 |
| 3 | IPOD | 7 |
+-------------+----------------------+--------+
Expected result
+-------------+----------------------+------------+
| id | name | level| quantity |
+-------------+----------------------+------------+
| 1 | ELECTRONICS | 1 | 4 |
| 2 | TELEVISIONS | 2 | 3 |
| 3 | TUBE | 3 | 1 |
| 4 | LCD | 3 | 2 |
| 5 | PORTABLE ELECTRONICS | 2 | 1 |
| 6 | MP3 PLAYERS | 3 | 1 |
+-------------+----------------------+------------+
I need to do using with recursive, because the speed is much higher than using nested
WITH RECURSIVE category_path (id, name, level, parent_id) AS
(
SELECT id, name, 1 level, parent_id
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, level + 1, c.parent_id
FROM category_path AS cp
JOIN categories AS c
ON cp.id = c.parent_id
)
SELECT * FROM category_path
Time: 0.020s
using nested
SELECT
parent.id,
parent.name,
parent.parent_id,
COUNT(departaments.product_id)
FROM
categories AS node
INNER JOIN
categories AS parent
ON node.lft BETWEEN parent.lft AND parent.rgt
INNER JOIN
departaments
ON node.id = departaments.categorie_id
GROUP BY
parent.id
ORDER BY
node.lft;
Time: 1.510s

First write a query to get product count per category. This is quite simple:
with products_per_category as (
select c.id, count(p.id) as pcount
from categories c
left join products p on p.category_id = c.id
group by c.id
)
select *
from products_per_category
order by id
db-fiddle
Then write a recursive CTE to generate a transitive closure:
with recursive rcte as (
select c.id, c.id as ancestor_id
from categories c
union all
select r.id, c.parent_id
from rcte r
join categories c on c.id = r.ancestor_id
)
select *
from rcte
order by id, ancestor_id
The result will be like:
| id | ancestor_id |
| --- | ----------- |
| 1 | 1 |
| 2 | 1 |
| 2 | 2 |
...
| 9 | 1 |
| 9 | 6 |
| 9 | 9 |
| 10 | 1 |
| 10 | 6 |
| 10 | 10 |
db-fiddle
It's like you get paths from root node to each category. Eg. for 9 the path is 1->6->9
If you order it by ancestor_id first, you will get:
| id | ancestor_id |
| --- | ----------- |
| 1 | 1 |
...
| 10 | 1 |
| 2 | 2 |
| 3 | 2 |
| 4 | 2 |
| 5 | 2 |
| 3 | 3 |
...
db-fiddle
Here you can see, that category 2 (ancestor_id=2) has subcategories (id) 2,3,4,5. Note that every category has itself as subcategory. This will make the next step simpler.
Now all we need is to join the two CTEs and sum up the product counts:
with recursive products_per_category as (
select c.id, count(p.id) as pcount
from categories c
left join products p on p.category_id = c.id
group by c.id
), rcte as (
select c.id, c.id as ancestor_id
from categories c
union all
select r.id, c.parent_id
from rcte r
join categories c on c.id = r.ancestor_id
where c.parent_id is not null
)
select
c.id,
c.name,
sum(p.pcount) as quantity
from rcte r
join categories c on c.id = r.ancestor_id
left join products_per_category p on p.id = r.id
group by c.id
Result:
| id | name | quantity |
| --- | -------------------- | -------- |
| 1 | ELECTRONICS | 4 |
| 2 | TELEVISIONS | 3 |
| 3 | TUBE | 1 |
| 4 | LCD | 2 |
| 5 | PLASMA | 0 |
| 6 | PORTABLE ELECTRONICS | 1 |
| 7 | MP3 PLAYERS | 1 |
| 8 | FLASH | 0 |
| 9 | CD PLAYERS | 0 |
| 10 | 2 WAY RADIOS | 0 |
db-fiddle
If you want to remove empty categories (quantity = 0), then just replace all LEFT JOINs with INNER JOINs.
Update
To get the level, you can use a subquery in the outer SELECT:
(select count(*) from rcte r2 where r2.id = c.id) as level
db-fiddle

Related

MySQL query to count occurrences from multiple tables

I have a problem when I have to select everything from one table (persons) then count how many objects they own by counting their occurrences on other tables (pens, chairs, books)
The current data is as followed:
select * from persons;
+----+-------+
| id | name |
+----+-------+
| 1 | Alex |
| 2 | Brad |
| 3 | Cathy |
+----+-------+
select * from pens;
+----+-----------+
| id | person_id |
+----+-----------+
| 1 | 2 |
| 2 | 2 |
| 3 | 2 |
| 4 | 3 |
+----+-----------+
select * from chairs;
+----+-----------+
| id | person_id |
+----+-----------+
| 1 | 1 |
+----+-----------+
select * from books;
+----+-----------+
| id | person_id |
+----+-----------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+----+-----------+
I want the result to be something like this
+----+-------+-----------------------+-------------------------+------------------------+
| id | name | count(pens.person_id) | count(chairs.person_id) | count(books.person_id) |
+----+-------+-----------------------+-------------------------+------------------------+
| 1 | Alex | 0 | 1 | 1 |
| 2 | Brad | 3 | 0 | 1 |
| 3 | Cathy | 1 | 0 | 1 |
+----+-------+-----------------------+-------------------------+------------------------+
I have tried using inner join and left outer join, but join gave me an empty set (since no person matches all of the objects) and left outer join gave me incorrect results:
> select persons.*, count(pens.person_id),count(chairs.person_id),count(books.person_id) from persons join pens on pens.person_id=persons.id join books on books.person_id=persons.id join chairs on chairs.person_id=persons.id group by persons.id;
Empty set (0.002 sec)
> select persons.*, count(pens.person_id),count(chairs.person_id),count(books.person_id) from persons left outer join pens on pens.person_id=persons.id left outer join books on books.person_id=persons.id left outer join chairs on chairs.person_id=persons.id group by persons.id;
# +----+-------+-----------------------+-------------------------+------------------------+
id | name | count(pens.person_id) | count(chairs.person_id) | count(books.person_id) |
# +----+-------+-----------------------+-------------------------+------------------------+
1 | Alex | 0 | 1 | 1 |
2 | Brad | 3 | 0 | 3 |
3 | Cathy | 1 | 0 | 1 |
# +----+-------+-----------------------+-------------------------+------------------------+
Any suggestions will be greatly appreciated, sorry if it's obvious, I'm fairly new at this.
Using a left join approach to subqueries on each table we can try:
SELECT
p.id,
p.name,
COALESCE(ps.cnt, 0) AS cnt_pens,
COALESCE(c.cnt, 0) AS cnt_chairs,
COALESCE(b.cnt, 0) AS cnt_books
FROM persons p
LEFT JOIN
(
SELECT person_id, COUNT(*) AS cnt
FROM pens
GROUP BY person_id
) ps
ON ps.person_id = p.id
LEFT JOIN
(
SELECT person_id, COUNT(*) AS cnt
FROM chairs
GROUP BY person_id
) c
ON c.person_id = p.id
LEFT JOIN
(
SELECT person_id, COUNT(*) AS cnt
FROM books
GROUP BY person_id
) b
ON b.person_id = p.id
ORDER BY
p.id;

Selecting COUNT and MAX columns with 2 tables and a bridge table

so what I am trying to do is having 3 tables (pictures, collections, and bridge) with the following columns:
Collections Table:
| id | name |
------------------
| 1 | coll1 |
| 2 | coll2 |
------------------
Pictures Table: (timestamps are unix timestamps)
| id | name | timestamp |
-------------------------
| 5 | Pic5 | 1 |
| 6 | Pic6 | 19 |
| 7 | Pic7 | 3 |
| 8 | Pic8 | 892 |
| 9 | Pic9 | 4 |
-------------------------
Bridge Table:
| id | collection | picture |
-----------------------------
| 1 | 1 | 5 |
| 2 | 1 | 6 |
| 3 | 1 | 7 |
| 4 | 1 | 8 |
| 5 | 2 | 5 |
| 6 | 2 | 9 |
| 7 | 2 | 7 |
-----------------------------
And the result should look like this:
| collection_name | picture_count | newest_picture |
----------------------------------------------------
| coll1 | 4 | 8 |
| coll2 | 3 | 9 |
----------------------------------------------------
newest_picture should always be the picture with the heighest timestamp in that collection and I also want to sort the result by it. picture_count is obviously the count of picture in that collection.
Can this be done in a single statement with table joins and if yes:
how can I do this the best way?
A simple method uses correlated subqueries:
select c.*,
(select count(*)
from bridge b
where b.collection = c.id
) as pic_count,
(select p.id
from bridge b join
pictures p
on b.picture = b.id
where b.collection = c.id
order by p.timestamp desc
limit 1
) as most_recent_picture
from collections c;
A more common approach would use window functions:
select c.id, c.name, count(bp.collection), bp.most_recent_picture
from collections c left join
(select b.*,
first_value(p.id) over (partition by b.collection order by p.timestamp desc) as most_recent_picture
from bridge b join
pictures p
on b.picture = p.id
) bp
on bp.collection = c.id
group by c.id, c.name, bp.most_recent_picture;

How to join from 3 table with condition

I have 3 table inside my database, my first table is person table its store all person name, my second table is hobby table its store all hobby of all person,the third table is referensi table store all references person and hobby.
Tabel person : Tabel hobby : Tabel referensi :
----------------- ------------------ -------------------------------------
| id | name | | id | hobby | | id | ref_person | ref_hobby |
----------------- ------------------ -------------------------------------
| 1 | Rose | | 1 | Makan | | 1 | 1 | 1 |
| 2 | Lisa | | 2 | Renang | | 2 | 1 | 3 |
| 3 | Jisoo | | 3 | Nyanyi | | 3 | 1 | 4 |
| 4 | Jennie| | 4 | Youtube| | 4 | 3 | 5 |
----------------- | 5 | Masak | -------------------------------------
------------------
I want to count all hobby by that person
Example I want select Rose : Or I want select Jisoo :
--------------------------- ---------------------------
| id | hobby | count | | id | hobby | count |
--------------------------- ---------------------------
| 1 | Makan | 1 | | 1 | Makan | 0 |
| 2 | Renang | 0 | | 2 | Renang | 0 |
| 3 | Nyanyi | 1 | | 3 | Nyanyi | 0 |
| 4 | Youtube| 1 | | 4 | Youtube| 0 |
| 5 | Masak | 0 | | 5 | Masak | 1 |
--------------------------- ---------------------------
And so forth, how do I solve this problem?
This is my query that I write but doesn't seem to work, because only data with count greater than 0 is shown.
SELECT
hobby.id,
hobby.name,
count( referensi.id ) AS count
FROM
referensi
LEFT OUTER JOIN hobby ON hobby.id = referensi.ref_hobby
JOIN person ON referensi.ref_person = person.id
WHERE person.id = 1
GROUP BY
hobby.id
Thanks in advance.
To solve this you need to JOIN referensi to person, selecting only entries in referensi corresponding to the person of interest, and then RIGHT JOIN to hobby. If there is no matching entry, the output is 0, otherwise 1. For example, for person 1:
SELECT h.id,
h.hobby,
CASE WHEN r.id IS NULL THEN 0 ELSE 1 END AS count
FROM referensi r
JOIN person p ON p.id = r.ref_person AND p.id = 1
RIGHT JOIN hobby h ON h.id = r.ref_hobby
ORDER BY h.id
This can also be implemented with a correlated subquery:
SELECT h.id,
h.hobby,
EXISTS (SELECT * FROM referensi r WHERE r.ref_hobby = h.id AND r.ref_person = 1) AS count
FROM hobby h
If a person/hobby tuple can appear in the referensi table more than once, you do need to do a COUNT:
SELECT h.id,
h.hobby,
COUNT(r.id) AS count
FROM referensi r
JOIN person p ON p.id = r.ref_person AND p.id = 1
RIGHT JOIN hobby h ON h.id = r.ref_hobby
GROUP BY h.id
Output (for all three queries on your sample data):
id hobby count
1 Makan 1
2 Renang 0
3 Nyanyi 1
4 Youtube 1
5 Masak 0
Demo on SQLFiddle
You can try to use condition aggravate function with OUTER JOIN
setting condition in CASE WHEN
Query 1:
SELECT
hobby.id,
hobby.name,
COUNT(CASE WHEN person.id = 3 THEN 1 END) AS count
FROM
hobby
LEFT JOIN referensi ON hobby.id = referensi.ref_hobby
LEFT JOIN person ON referensi.ref_person = person.id
GROUP BY
hobby.id,
hobby.name
Results:
| id | name | count |
|----|---------|-------|
| 1 | Makan | 0 |
| 2 | Renang | 0 |
| 3 | Nyanyi | 0 |
| 4 | Youtube | 0 |
| 5 | Masak | 1 |
You want to start your joining from hobby table, and use LEFT JOINs to optionnaly bring up the matching records in other tables.
SELECT
h.id,
h.hobby,
count( p.id ) AS count
FROM
hobby h
LEFT JOIN referensi r ON h.id = r.ref_hobby
LEFT JOIN person p ON r.ref_person = p.id AND p.id = 1
WHERE p.name is NULL OR p.name = 'Rose'
GROUP BY h.id, h.hobby
It is also a good practice to use table aliases, I added them to your query.
Demo on DB Fiddle for user Rose :
| id | hobby | count |
| --- | ------- | ----- |
| 1 | Makan | 1 |
| 2 | Renang | 0 |
| 3 | Nyanyi | 1 |
| 4 | Youtube | 1 |
| 5 | Masak | 0 |

Getting specific values from many-to-many relationships

My database looks like this, I have client accounts which are assigned to specific profiles, and I have profiles which are assigned to specific categories, like in this schema:
| categories | | profiles | | categories_map |
--------------- ------------- ----------------------------
| ID | name | | ID | name | | ID | profile_id | cat_id |
--------------- ------------- ----------------------------
| 1 | cat1 | | 1 | p1 | | 1 | 1 | 1 |
| 2 | cat2 | | 2 | p2 | | 2 | 2 | 1 |
| 3 | cat3 | | 3 | p3 | | 3 | 3 | 1 |
| 4 | p4 | | 4 | 1 | 2 |
| 5 | 3 | 2 |
| 6 | 4 | 3 |
| profiles_map |
-----------------------------
| ID | profile_id | acc_id |
-----------------------------
| 1 | 1 | 1 |
| 2 | 3 | 1 |
| 3 | 4 | 1 |
I need to get categories assigned to accounts - which means when I want to get categories for acc_id = 1, I should get categories with ID 2 and 3 ( category with ID 2 doesn't fit because it contains profile with ID 2 which isn't assigned to this account). I tried this query but it doesn't work
select cats.id from profiles_map map
right join categories_map catm on catm.profile_id = map.profile_id
right join categories cats on cats.id = catm.cat_id
where catm.profile_id in (select profile_id from profiles_map where acc_id = 1)
and map.acc_id = 1 group by cats.id;
Could anybody help me with this question?
Can you try this one?
SELECT DISTINCT C.ID
FROM profiles_map PM
INNER JOIN categories_map CM ON CM.profile_id = PM.profile_id
INNER JOIN categories C ON C.ID = CM.cat_id
WHERE PM.acc_id= 1
If you want to get only category id, Please try following query:
SELECT DISTINCT cm.cat_id FROM categories_map cm
WHERE cm.profile_id in
(SELECT profile_id FROM profiles_map WHERE acc_id = 1)
Or if want to get category name and id then , use following query:
SELECT cat.id,cat.name FROM categories cm
WHERE cat.id in (SELECT DISTINCT cm.cat_id FROM categories_map cm
WHERE cm.profile_id in
(SELECT pm.profile_id FROM profiles_map pm WHERE pm.acc_id = 1))

Left join select using Propel ORM

I have 3 table
major table:
+----+------------+
| id | major |
+----+------------+
| 1 | Computer |
| 2 | Architect |
| 3 | Designer |
+----+------------+
classroom table:
+----+----------+-------+
| id | major_id | name |
+----+----------+-------+
| 1 | 1 | A |
| 2 | 1 | B |
| 3 | 1 | C |
| 4 | 2 | A |
| 5 | 2 | B |
| 6 | 3 | A |
+----+----------+-------+
and finally, student_classroom table
+----+------------+--------------+----------+
| id | student | classroom_id | status |
+----+------------+--------------+----------+
| 1 | John | 1 | Inactive |
| 2 | Defou | 2 | Active |
| 3 | John | 2 | Active |
| 4 | Alexa | 1 | Active |
| 5 | Nina | 1 | Active |
+----+------------+--------------+----------+
how can I use propel to build query below
select
a.id,
a.major,
b.number_of_student,
c.number_of_classroom
from major a
left join (
select
major.major_id,
count(student_classroom.id) as number_of_student
from major
left join classroom on classroom.major_id = major.id
left join student_classroom on student_classroom.classroom_id = classroom.id
where student_classroom.`status` = 'Active'
group by major_id
) b on b.major_id = a.major_id
left join (
select
major.major_id,
count(classroom.id) as number_of_classroom
from major
left join classroom on classroom.major_id = major.id
group by major_id
) c on c.major_id = a.major_id
Because I want the final result would be something like this, I spend hours trying to figure it out without success.
+----+------------+-------------------+---------------------+
| id | major | number_of_student | number_of_classroom |
+----+------------+-------------------+---------------------+
| 1 | Computer | 4 | 3 |
| 2 | Architect | 0 | 2 |
| 3 | Designer | 0 | 1 |
+----+------------+-------------------+---------------------+
Try this
select
m.id,
m.major,
count(distinct s.id) as number_of_student ,
count(distinct c.id) as number_of_classroom
from major m
left join classroom c on
(m.id = c.major_id)
left join student_classroom s
on (s.classroom_id = c.id and c.major_id = m.id and s.status = 'active')
group by m.id
order by m.id