Getting specific values from many-to-many relationships - mysql

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))

Related

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;

Combine two tables into one table without using join and without the result creating cartesian product

How do I make a table of results of two different ratings without creating a cartesian product. diffrating and hostrating are two different types of user ratings. The users are rating the spots which are are labeled with spotId. userdiffrating and userhostrating are linking the users ratings to the correct spots. I am trying to get all the ratings for a specific spot in a table. Sample data and the expected output are bellow.
Table contents:
userdiffrating:
-------------------------------
| RatingId | userId | spotId |
-------------------------------
| 1 | 1 | 1 |
-------------------------------
| 2 | 2 | 1 |
-------------------------------
| 3 | 1 | 2 |
-------------------------------
diffrating:
----------------------
| RatingId | userId |
----------------------
| 1 | 5 |
----------------------
| 2 | 2 |
----------------------
| 3 | 4 |
----------------------
userhostrating:
-------------------------------
| RatingId | userId | spotId |
-------------------------------
| 1 | 1 | 1 |
-------------------------------
| 2 | 2 | 1 |
-------------------------------
| 3 | 1 | 1 |
-------------------------------
hostrating:
----------------------
| RatingId | userId |
----------------------
| 1 | 1 |
----------------------
| 2 | 3 |
----------------------
| 3 | 4 |
----------------------
This is what I originally tried but this creates a cartesian product:
SELECT D.rating diffrating, H.rating hostrating FROM diffrating D
JOIN userdiffrating UD ON D.ratingId = UD.ratingId
JOIN userhostrating UH ON UD.spotId = UH.spotId
JOIN hostrating H ON UH.ratingId = H.ratingId WHERE UD.spotId = 1
Result from first query (cartesian product):
-------------------------
| diffrating| hostrating|
-------------------------
| 5 | 1 |
-------------------------
| 5 | 3 |
-------------------------
| 5 | 4 |
-------------------------
| 2 | 1 |
-------------------------
| 2 | 3 |
-------------------------
| 2 | 4 |
-------------------------
I tried this next query but I cant use a select statement that has more than one row as a subquery:
SELECT D.rating AS diffrating, H.rating AS hostrating
FROM diffrating D, hostrating H
WHERE D.ratingId = (SELECT ratingId FROM userdiffrating UD WHERE UD.spotId = 1)
AND H.ratingId = (SELECT ratingId FROM userhostrating UH WHERE UH.spotId = 1)
This is the expected result (all diff and host ratings for spotId = 1):
-------------------------
| diffrating| hostrating|
-------------------------
| 5 | 1 |
-------------------------
| 2 | 3 |
-------------------------
| NULL | 4 |
-------------------------
This is the database:
Is this possible and how would this be done?
Thanks
I think what you are trying to code is this
SELECT dr.rating diffratingval, hr.rating hostratingval
FROM diffrating dr
JOIN userdiffrating udr
ON dr.ratingid = udr.ratingid
JOIN userhostrating uhr
ON udr.spotid = uhr.spotid
JOIN hostrating hr
ON uhr.ratingid = hr.ratingid
AND dr.ratingid = hr.ratingid
WHERE udr.spotid = 60;

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 |

Querying across 6 tables, is there a better way of doing this?

What I did was, I wanted each user to have their own "unique" numbering system. Instead of auto incrementing the item number by 1, I did it so that Bob's first item would start at #1 and Alice's number would also start at #1. The same goes for rooms and categories. I achieved this by creating "mapping" tables for items, rooms and categories.
The query below works, but I know it can definitely be refactored. I have primary keys in each table (on the "ids").
SELECT unique_item_id as item_id, item_name, category_name, item_value, room_name
FROM
users_items, users_map_item, users_room, users_map_room, users_category, users_map_category
WHERE
users_items.id = users_map_item.map_item_id AND
item_location = users_map_room.unique_room_id AND
users_map_room.map_room_id = users_room.room_id AND
users_map_room.map_user_id = 1 AND
item_category = users_map_category.unique_category_id AND
users_map_category.map_category_id = users_category.category_id AND
users_category.user_id = users_map_category.map_user_id AND
users_map_category.map_user_id = 1
ORDER BY item_name
users_items
| id | item_name | item_location |item_category |
--------------------------------------------------------
| 1 | item_a | 1 | 1 |
| 2 | item_b | 2 | 1 |
| 3 | item_c | 1 | 1 |
users_map_item
| map_item_id | map_user_id | unique_item_id |
----------------------------------------------------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
users_rooms
| id | room_name |
----------------------
| 1 | basement |
| 2 | kitchen |
| 3 | attic |
users_map_room
| map_room_id | map_user_id | unique_room_id |
----------------------------------------------------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
users_category
| id | room_name |
----------------------
| 1 | antiques |
| 2 | appliance |
| 3 | sporting goods |
users_map_category
| map_room_id | map_user_id | unique_category_id |
----------------------------------------------------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
Rewriting your query with explicit JOIN conditions makes it more readable (while doing the same).
SELECT mi.unique_item_id AS item_id
, i.item_name
, c.category_name
, i.item_value
, r.room_name
FROM users_map_item mi
JOIN users_items i ON i.id = mi.map_item_id
JOIN users_map_room mr ON mr.unique_room_id = i.item_location
JOIN users_room r ON r.room_id = mr.map_room_id
JOIN users_map_category mc ON mc.unique_category_id = i.item_category
JOIN users_category c ON (c.user_id, c.category_id)
= (mc.map_user_id, mc.map_category_id)
WHERE mr.map_user_id = 1
AND mc.map_user_id = 1
ORDER BY i.item_name
The result is unchanged. Query plan should be the same. I see no way to improve the query further.
You should use LEFT [OUTER] JOIN instead of [INNER] JOIN if you want to keep rows in the result where no matching rows are found in the right hand table. You may want to move the additional WHERE clauses to the JOIN condition in this case, as it changes the outcome.

Include third table in LEFT JOIN query

I have five mysql tables. shops
+----+--------------+--------------+
| id | name | address |
+----+--------------+--------------+
| 1 | Shop1 | Street1 |
| 2 | Shop2 | Street2 |
| 3 | Shop3 | Street3 |
| 4 | Shop4 | Street4 |
+----+--------------+--------------+
fruits
+----+--------------+--------------+
| id | fruit | price |
+----+--------------+--------------+
| 1 | Bannana | 2.5 |
| 2 | Apple | 2.1 |
| 3 | Orange | 1.8 |
| 4 | Plum | 2.2 |
+----+--------------+--------------+
availability
+----+--------------+--------------+
| id | shop_id | fruit_id |
+----+--------------+--------------+
| 1 | 1 | 2 |
| 2 | 2 | 2 |
| 3 | 1 | 3 |
| 4 | 2 | 1 |
+----+--------------+--------------+
shop_activity
+----+--------------+--------------+--------------+
| id | shop_id | user_id | status |
+----+--------------+--------------+--------------+
| 1 | 2 | 1 | 1 |
| 2 | 3 | 2 | 1 |
| 3 | 1 | 2 | 2 |
| 4 | 2 | 2 | 1 |
+----+--------------+--------------+--------------+
users
+----+--------------+
| id | name |
+----+--------------+
| 1 | Peter |
| 2 | John |
+----+--------------+
I have query
SELECT
availability.shop_id,
shops.name
FROM availability
LEFT JOIN shops
ON availability.shop_id=shops.id
WHERE
fruit_id = 2
As a result I get name list of shops where fruit with id 2 (apple) is available.
What should I do so that I can include shop_activity table in query to get user's status if users.id = 1 beside proper shop. Something like this...
Shop1, NULL
Shop2, status: 1
You could try something like this:
SELECT
availability.shop_id,
shops.name,
shop_activity.status
FROM availability
LEFT JOIN shops
ON availability.shop_id=shops.id
LEFT JOIN shop_activity
ON shop_activity.shop_id = availability.shop_id
and shop_activity.user_id = 1
WHERE
fruit_id = 2
SELECT
availability.shop_id,
shops.name
FROM shops
LEFT JOIN availability ON availability.shop_id=shops.id
LEFT JOIN shop_activity ON shop_activity .shop_id=shops.id
WHERE
fruit_id = 2
and users.id=1
try making shops as the first table in left join
Try the following:
SELECT shops.name, shop_activity.status
FROM shops
INNER JOIN availability ON availability.shop_id = shops.id
AND availability.fruit_id = 2
LEFT JOIN shop_activity ON shops.shop_id = shop_activity.shop_id
AND shop_activity.user_id = 1
This should give you a row for every shop with apples, but the status will show as null for shops where the user has no activity, otherwise shows the status of that user.