Counting Descendant Nodes [from MySQL Closure Pattern] - mysql

I am using the following data structure to represent a hierarchy of data.
User Table
+----+------+
| id | name |
+----+------+
| 1 | Bob |
| 2 | Sam |
| 3 | Joe |
| 4 | Kirk |
| 5 | Greg |
+----+------+
Relationship Closure Table
+----------+------------+-------+
| ancestor | descendant | depth |
+----------+------------+-------+
| 1 | 1 | 0 |
| 1 | 2 | 1 |
| 1 | 3 | 2 |
| 1 | 4 | 2 |
| 1 | 5 | 3 |
| 2 | 2 | 0 |
| 2 | 3 | 1 |
| 2 | 4 | 1 |
| 2 | 5 | 2 |
| 3 | 3 | 0 |
| 4 | 4 | 0 |
| 4 | 5 | 1 |
| 5 | 5 | 0 |
+----------+------------+-------+
The above data represents the following (in English-ese):
Bob has one son: Sam
Sam has two sons: Joe and Kirk.
Joe has no sons.
Kirk has one son: Greg.
I am getting the sons of a given user from the following SQL:
SELECT u.*
FROM closure AS c
INNER JOIN `user` AS u ON (u.id = c.descendant)
WHERE c.ancestor = 1 AND c.depth = 1
This works fine. But I would also like to return the number of descendants all the way down the tree. The best I have been able to come up with so far is this:
SELECT
u.*,
(
SELECT COUNT(id) FROM `user` WHERE id IN (
SELECT descendant FROM closure
WHERE ancestor = c.descendant
)
) AS descendant_count
FROM closure AS c
INNER JOIN `user` AS u ON (u.id = c.descendant)
WHERE c.ancestor = 1 AND c.depth = 1
Expected output of the above query is:
+----+------+------------------+
| id | name | descendant_count |
+----+------+------------------+
| 2 | Sam | 3 |
+----+------+------------------+
Question (finally)
Is there a better way to get the total than what I have? All those sub-selects are gross.
Update
I am realizing as I look at this that I may have simplified things too much for this example. I have two sub-selects to do the count because I actually have 3 tables: category; item; category_closure. In my example data, there would obviously be no need for the double nested sub-select. In my actual data there is. Hopefully that makes sense.

You don't need subqueries. You can get the number of descendants of each child by joining to the closure table again, to find all those nodes whose ancestor is the respective child. Then use GROUP BY so you can get a count per child.
SELECT
u.*,
COUNT(*) AS descendant_count
FROM closure AS c
INNER JOIN `user` AS u ON (u.id = c.descendant)
INNER JOIN closure AS d ON (c.descendant = d.ancestor)
WHERE c.ancestor = 1 AND c.depth = 1
GROUP BY c.descendant

Related

How can I get a total count of items?

I'm trying to figure out a MYSQL string and my noob-ness is getting in my way. I'm trying to count the total number of teams per phase.
Tables to consider:
phases
+----+------------+
| id | phase_name |
+----+------------+
| 1 | start |
| 2 | middle |
| 3 | end |
| 4 | finish |
+----+------------+
teams
+----+-----------+----------+
| id | team_name | phase_id |
+----+-----------+----------+
| 1 | team1 | 2 |
| 2 | team2 | 3 |
| 3 | team3 | 3 |
| 4 | team4 | 4 |
| 4 | team5 | 3 |
+----+-----------+----------+
Desired result
+----------+------------+-----------+
| phase_id | phase_name | tot_teams |
+----------+------------+-----------+
| 1 | start | NULL |
| 2 | middle | 1 |
| 3 | end | 3 |
| 4 | finish | 1 |
+----------+------------+-----------+
I've tried:
SELECT
T.phase_id, P.phase_name, COUNT(*) AS tot_teams
FROM
teams T
LEFT JOIN
phases P ON P.id = T.phase_id
GROUP BY
phase_id;
but that only shows the affected phase_id's...and I'm hoping to get ALL phase_id's in a table. I also tried:
SELECT
P.phase_name, T.phase_id, COUNT(*)
FROM
teams T
RIGHT JOIN
phases P on P.`id` = T.`phase_id`
GROUP BY
P.id
but that shows invalid data. (For example, phase_id has a qty of 1 but doesn't show up in the teams table.
Can you point me in the right direction?
Thanks!
The RIGHT JOIN is correct, but you need to use COUNT(T.phase_id) instead of COUNT(*). Otherwise, you're counting the row containing NULL that's generated for the phase with no teams.
Most people prefer to use LEFT JOIN, putting the master table first.
SELECT P.phase_name, P.phase_name, COUNT(T.phase_id)
FROM phase AS P
LEFT JOIN teams AS T ON P.id = T.phase_id
GROUP BY P.id

SQL NOT LIKE with JOIN

I'm using MYSQL and can't get a NOT LIKE working with multiple JOINS.
I have 3 tables which look like this:
Parents (Table1):
+------------+-------+-----+
| ParentID | Name | AGE |
+------------+-------+-----+
| 1 | Peter | 26 |
| 2 | Karl | 33 |
| 3 | Tessa | 43 |
+------------+-------+-----+
Kids (Table2):
+------------+-------+-----+
| KidID | Name | AGE |
+------------+-------+-----+
| 1 | Mike | 3 |
| 2 | Mike | 13 |
| 3 | Jenna | 4 |
| 4 | Jessi | 14 |
+------------+-------+-----+
Parents_Kids (Table3):
+-----------+-------+
| ParentID | KidID |
+-----------+-------+
| 1 | 2 |
| 1 | 4 |
| 2 | 1 |
| 3 | 3 |
+-----------+-------+
Now i want to get all parent names who don't have a kid named Mike or any form of Mike in there name.
I tried this:
SELECT p.name
FROM PARENTS p JOIN
Parents_Kids pk
ON pk.ParentID = p.ParentID JOIN
Kids k
ON k.KidID = pk.KidID
WHERE k.Name NOT LIKE '%mike%';
But the result is wrong with this query.
If i try this query with LIKE it works correctly but not with NOT LIKE.
I think you don't want to select the parent if atleast one of his/her kids have a name like mike. You can use having to filter such cases.
SELECT p.name
FROM PARENTS p
JOIN Parents_Kids pk ON pk.ParentID=p.ParentID
JOIN Kids k ON k.KidID=pk.KidID
group by p.name
having count(case when k.name like '%mike%' then 1 end) = 0
You say your query works correctly with LIKE, so a way to get the other parent names is this:
SELECT name
FROM parents
WHERE name not in (SELECT p.name
FROM PARENTS p JOIN
Parents_Kids pk
ON pk.ParentID = p.ParentID JOIN
Kids k
ON k.KidID = pk.KidID
WHERE k.Name LIKE '%mike%')

Join top 3 interest fields along with each user row

I'm trying to get the top 3 interests of each user, probably as a LEFT JOIN query.
The way the app is designed, each user has a set of interests which are no other than 'childs' (rows without parent) of the categories table.
Here are some simplified table schemas w/mock data (see SQL Fiddle demo)
-- Users table
| ID | NAME |
--------------
| 1 | John |
| 2 | Mary |
| 3 | Chris |
-- Categories table -- Interests table
| ID | NAME | PARENT | | ID | USER_ID | CATEGORY_ID |
-------------------------------------- ------------------------------
| 1 | Web Development | (null) | | 1 | 1 | 1 |
| 2 | Mobile Apps | (null) | | 2 | 1 | 1 |
| 3 | Software Development | (null) | | 3 | 1 | 1 |
| 4 | Marketing & Sales | (null) | | 4 | 2 | 1 |
| 5 | Web Apps | 1 | | 5 | 2 | 1 |
| 6 | CSS | 1 | | 6 | 3 | 1 |
| 7 | iOS | 2 | | 7 | 3 | 1 |
| 8 | Streaming Media | 3 | | 8 | 3 | 1 |
| 9 | SEO | 4 |
| 10 | SEM | 4 |
To get the top 3 interests of a given user, I've usually performed this query:
SELECT `c`.`parent` as `category_id`
FROM `interests` `i` LEFT JOIN `categories` `c` ON `c`.`id` = `i`.`category_id`
WHERE `i`.`user_id` = '2'
GROUP BY `c`.`parent`
ORDER BY count(`c`.`parent`) DESC LIMIT 3
This query returns the top 3 categories (parents) of user with id = 2
I would like to find out how I can query the users table and get their top 3 categories either in 3 different fields (preferred) or as a group_concat(..) in one field
SELECT id, name, top_categories FROM users, (...) WHERE id IN ('1', '2', '3');
Any ideas how I should go about doing this?
Thanks!
First build a groped query that lists on distinct rows, the top three skills for each user. Then pivot that into to pull the three skills for eah user out to the right. You will need to use the Max(isnull(skill,'')) expression on the skills in each skill column.
It is very crude way of doing it in MYSQL to get top 3 records for each user
SELECT u.id, c.name
FROM
users u,
categories c,
(SELECT i.id,
i.user_id,
i.category_id,
#running:=if(#previous=i.user_id,#running,0) + 1 as rId,
#previous:=i.user_id
FROM
(SELECT * FROM intersect ORDER BY user_id) i JOIN
(SELECT #running=0, #previous=0 ) r) i
WHERE
u.id = i.USER_ID AND
i.CATEGORY_ID = c.id AND
i.rId <= 3
group by u.id, c.name ;
Hope it helps
FIDDLE

Select Distinct Set Common to Subset From Join Table

Given a join table for m-2-m relationship between booth and user
+-----------+------------------+
| booth_id | user_id |
+-----------+------------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 5 |
| 1 | 9 |
| 2 | 1 |
| 2 | 2 |
| 2 | 5 |
| 2 | 10 |
| 3 | 1 |
| 3 | 2 |
| 3 | 3 |
| 3 | 4 |
| 3 | 6 |
| 3 | 11 |
+-----------+------------------+
How can I get a distinct set of booth records that are common between a subset of user ids? For example, if I am given user_id values of 1,2,3, I expect the result set to include only booth with id 3 since it is the only common booth in the join table above between all user_id's provided.
I'm hoping I'm missing a keyword in MySQL to accompish this. The furthest I've come so far is using ... user_id = all (1,2,3) but this is always returning an empty result set (I believe I understand why it is though).
The SQL query for this will be:
select booth_id from table1 where [user_id]
in (1,2,3) group by booth_id having count(booth_id) =
(select count(distinct([user_id])) from table1 where [user_id] in (1,2,3))
If this could help you creating the MySQL query.

SQL - Select all matching ID's if one of the ID's has a certain column value

I'm not sure how to word this question, but it is easier to explain.
If I have a table that looks like this:
+-------+----------+-------+
| User | Result | Level |
+-------+----------+-------+
| 1 | Victory | 1 |
| 1 | Victory | 2 |
| 1 | Victory | 3 |
| 2 | Victory | 1 |
| 2 | Defeat | 2 |
| 3 | Defeat | 1 |
| 4 | Defeat | 1 |
| 5 | Defeat | 1 |
+-------+----------+-------+
I want to be able to only select the 'User's that have a victory on level 3. So the select would return:
+-------+----------+-------+
| User | Result | Level |
+-------+----------+-------+
| 1 | Victory | 1 |
| 1 | Victory | 2 |
| 1 | Victory | 3 |
+-------+----------+-------+
The others would not be returned because they did not complete level 3.
Thanks a lot! I hope I explained it clearly.
If you just want to know which users have a victory on level 3:
SELECT User FROM my_table WHERE Result = 'Victory' AND Level = 3
However, if (as you have shown) you want all of those users' results (with thanks to #Andomar and #ConradFrix for their comments below):
SELECT DISTINCT m1.*
FROM my_table AS m1
JOIN my_table AS m2 USING (User)
WHERE m2.Result = 'Victory'
AND m2.Level = 3
You could use an exists clause, for example:
select *
from scores s
where exists
(
select *
from scores vl3
where vl3.user = s.user
and vl3.Result = 'Victory'
and vl3.Level >= 3
)