MySql JOIN 3 tables and get child rows with same values - mysql

I have 3 tables: order, item and render.
1 order can have multiple items and each item has one render entry with status.
I want to have the order id's and date which have ALL its items with rendered 'done'.
In the example it would be the order 1 only, which has its 2 items (1 and 2) with status done in render table
order table
id| date
1 | 2021-12-10
2 | 2021-12-11
item table
id|order_id
1 | 1
2 | 1
3 | 2
4 | 2
render table
id|item_id| status
1 |1 | done
2 |2 | done
3 |3 | done
4 |4 | running
I would like to have it with JOIN statements. I've tried this but it does not work like expected (it returns both, order 1 and 2):
SELECT o.id, o.date
FROM order AS o
JOIN item AS i ON o.id = i.order_id
JOIN render r1 ON (i.`id` = r1.item_id AND r1.status = 'done')
LEFT JOIN render r2 ON (i.`id` = r2.item_id AND r2.status <> 'done')
AND r2.item_id IS NULL;
Any ideas? thank you in advance!

SELECT t1.id
, t1.date
FROM order
t1
WHERE t1.id NOT IN
(
SELECT o.id
FROM order
o
JOIN item
i
ON i.order_id = o.id
JOIN render
r
ON r.item_id = i.id
WHERE r.status <> 'done'
)
The choice of some column names is unfortunate, making the join conditions painful for a maintainer to read: it is not immediately obvious at a glance what kind if IDs the id columns refer to.
I would rename the following columns:
TABLE COLUMN_NAME BETTER_COLUMN_NAME
------ ----------- ------------------
order id order_id
order date order_date
item id item_id
render id render_id
render status render_status
I also disagree with the o / i / r table aliases, but arguments are in room 12A, just along the corridor.

Here I post what it eventually worked best for me in case it helps anybody :)
SELECT o.id, o.date
FROM order AS o
JOIN item AS i ON o.id = i.order_id
LEFT JOIN render r ON (r.item_id = i.id)
GROUP BY o.id
HAVING GROUP_CONCAT(DISTINCT IFNULL(r.status, "NULL")) = 'done';
In the HAVING clause I concatenate the statuses, remove all duplicates and check that only “done” is there. Since I needed to have a LEFT JOIN against render I explicitly set to “NULL“ the returned null rows.

Related

How do I join with multiple SQL tables and return one row per base table record with the correct GROUP_CONCAT data?

I have a situation where I want to join multiple SQL tables and get back one row per record in the base table as well as GROUP_CONCAT the other table data together with |. Unfortunately, with the query method I'm currently using, I'm getting back undesired multiplicity in the GROUP_CONCAT data and I don't know how to solve it.
I have the following basic DB structure:
things
id | name
1 | Some Thing
2 | Some Other Thing
items
id | name
1 | Blob
2 | Starfish
3 | Wrench
4 | Stereo
users
id | name
1 | Alice
2 | Bill
3 | Charlie
4 | Daisy
things_items
thing_id | item_id
1 | 1
1 | 2
2 | 3
2 | 4
things_users
thing_id | user_id
1 | 1
1 | 2
1 | 3
2 | 4
And I would ideally like to write a query that gets back the following for the Some Thing row in the things table:
Some Thing | Blob|Starfish | Alice|Bill|Charlie
However, what I'm getting back is the following:
Some Thing | Blob|Blob|Blob|Starfish|Starfish|Starfish | Alice|Alice|Bill|Bill|Charlie|Charlie
And this is the query I'm using:
SELECT things.name,
GROUP_CONCAT(items.name SEPARATOR '|')
GROUP_CONCAT(users.name SEPARATOR '|')
FROM things
JOIN things_items ON things.id = things_items.thing_id
JOIN items ON things_items.item_id = items.id
JOIN things_users ON things.id = things_users.thing_id
JOIN users ON things_items.user_id = users.id
GROUP BY things.id;
How should I change the query to get the data back the way I'd like to and avoid the multiplying of the GROUP_CONCAT data? Thank you.
You are concatenating along two separate dimensions. The simplest solution is DISTINCT:
SELECT t.name,
GROUP_CONCAT(DISTINCT i.name SEPARATOR '|')
GROUP_CONCAT(DISTINCT u.name SEPARATOR '|')
FROM things t JOIN
things_items ti
ON t.id = ti.thing_id JOIN
items i
ON ti.item_id = i.id JOIN
things_users tu
ON t.id = tu.thing_id JOIN
users u
ON tu.user_id = u.id
GROUP BY t.id;
Note the above filters out things that have either no items or no users.
The above will work fine if there are a handful of items and users for each thing. As the numbers grow, the performance gets worse because it generates a Cartesian product for each thing.
That can be solved by aggregating before joining:
SELECT t.name, i.items, u.users
FROM things t JOIN
(SELECT ti.thing_id, GROUP_CONCAT(i.name SEPARATOR '|') as items
FROM things_items ti JOIN
items i
ON ti.item_id = i.id
GROUP BY ti.thing_id
) i
ON t.id = ti.thing_id JOIN
(SELECT tu.user_id, GROUP_CONCAT(DISTINCT u.name SEPARATOR '|') as users
FROM things_users tu JOIN
users u
ON tu.user_id = u.id
GROUP BY tu.user_id
) tu
ON t.id = tu.thing_id ;
You can replace the outer JOINs with LEFT JOIN if you want all things, even those with no items or names.

select same column twice having different values for single id using mysql

I have these three tables movies, category and relationship as shown below.
movies--
-----------------------
id|name|duration|
1 |x |5 mins |
2 |y |10 mins |
----------------------
category--
-----------------------
id|type |value |
1 |genre |action |
2 |language|english |
3 |genre |thriller |
4 |language|spanish |
------------------------
relationship--
id| movie_id|category_id|
1 |1 | 2 |
2 |1 | 3 |
------------------------------
i want a query that will fetch both genre and language for the movie in a single query.
below is the expected output.
name|duration|language|genre |
x |5 mins |english |thriller|
--------------------------------
in short i want to use the type column twice.
Please help
You need mysql pivot table for that. That is turn some columns into row data. The following query will produce what you want:
SELECT
m.name,
m.duration,
MAX(IF(c.type = 'language', c.value, NULL)) AS language,
MAX(IF(c.type = 'genre', c.value, NULL)) AS genre
FROM movies AS m
INNER JOIN relationship AS r ON m.id = r.movie_id
INNER JOIN category AS c ON r.category_id = c.id
WHERE m.name = 'x'
GROUP BY m.name;
That will produce:
| name | duration | language | genre |
| x | 5 mins | english | thriller |
See DEMO
Step 1: Join all the three table together. Now you get all the category infos for each movie.
Step 2: Select what you want from the big combined table.
Step 3: Use two subquery to satisfy your special needs for language and genre.
Step 4: Add a LIMIT 1 to avoid redundant records.
The final query might be something like this:
SELECT name, duration, (SELECT value FROM t WHERE type = 'language' AND name = 'x') AS language, (SELECT value FROM t WHERE type = 'genre' AND name = 'x') AS genre
FROM
(
SELECT m.name, m.duration, c.type FROM movies AS m
JOIN relationship AS r ON m.id = r.movie_id
JOIN category AS c ON r.category_id = c.id
) AS t LIMIT 1;
Note:
Replace your own query condition for WHERE clause.
This query might not be strictly syntax correct. Please fix it by yourself.
One method uses two joins, one for each type:
select m.*, cl.value as language, cg.language as genre
from movies m join
relationships r
on m.id = r.movie_id left join
categories cl
on cl.id = r.category_id and type = 'language' left join
categories cg
on cg.id = r.category_id and type = 'genre';
Note that movies typically have only one language, but they can have multiple genres. If this is the case you will get a separate row for each genre.

MySql 3 tables join, group and sum

I want to add order totals together and then group them by site owner
I have 3 tables
orders
subtotal | site
site_data
site_owner | record_id
site
record_id
the relationship between these are
sites.site_data = site_data.record_id
sites.record_id = orders.site
currently this is what I have
SELECT site_data.site_owner,
SUM('orders.subtotal')
FROM site_data
INNER JOIN site ON site.site_data = site_data.record_id
INNER JOIN orders ON site.record_id = orders.site
group by site_data.site_owner
but the output is as follows
site_owner | SUM('orders,subtotal')
Mr Foo | 0
Mr Bar | 0
all the orders totals are 0 and I am not sure why I have done a sum on this field before and not had an issue so must be to do with the grouping.
You should not use quotes inside SUM function
SELECT site_data.site_owner,
SUM(orders.subtotal)
FROM site_data
INNER JOIN site ON site.site_data = site_data.record_id
INNER JOIN orders ON site.record_id = orders.site
group by site_data.site_owner

MySQL - JOIN multiple rows to single row multiple times

I am trying to join multiple rows of information for single row, but it seems to multiply every time there is more rows in one of the joins.
My tables structure is as follows:
news
id | title | public
------------------------
1 | Test | 0
news_groups_map
id | news_id | members_group_id
------------------------------------
1 | 1 | 5
2 | 2 | 6
members_groups_map
id | member_id | group_id
------------------------------
1 | 750 | 5
2 | 750 | 6
The query I've got so far is:
SELECT
n.title,
n.public,
CAST(GROUP_CONCAT(ngm.members_group_id) AS CHAR(1000)) AS news_groups,
CAST(GROUP_CONCAT(member_groups.group_id) AS CHAR(1000)) AS user_groups
FROM news n
LEFT JOIN news_groups_map ngm ON n.id = ngm.news_id
JOIN (
SELECT group_id
FROM members_groups_map
WHERE member_id = 750
) member_groups
WHERE n.public = 0
GROUP BY n.id
However, the result is as follows:
title | public | news_groups | user_groups
-------------------------------------------------
Test | 0 | 5,6,5,6 | 6,6,5,5
As you can see, the news_group and user_groups are duplicating, so if a news article is in 3 groups, the user_groups will be multiplied as well and show something like 5,6,6,6,5,5.
How can I group those groups, so that they are only displayed once?
The ultimate goal here is to compare news_groups and user_groups. So if at least one group matches (meaning user has enough permissions), then there should be a boolean with true returned, and false otherwise. I don't know how to do that either, however, I thought I should sort out the grouping first, as once the number of groups gets bigger there is going to be unnecessary lots of same data selected.
Thanks!
The simplest method is to use distinct:
SELECT n.title, n.public,
GROUP_CONCAT(DISTINCT ngm.members_group_id) AS news_groups,
GROUP_CONCAT(DISTINCT mg.group_id) AS user_groups
FROM news n LEFT JOIN
news_groups_map ngm
ON n.id = ngm.news_id CROSS JOIN
(SELECT group_id
FROM members_groups_map
WHERE member_id = 750
) mg
WHERE n.public = 0
GROUP BY n.id;
This query doesn't actually make sense. First, the subquery is not needed:
SELECT n.title, n.public,
GROUP_CONCAT(DISTINCT ngm.members_group_id) AS news_groups,
GROUP_CONCAT(DISTINCTD mg.group_id) AS user_groups
FROM news n LEFT JOIN
news_groups_map ngm
ON n.id = ngm.news_id CROSS JOIN
members_groups_map mg
ON member_id = 750
WHERE n.public = 0
GROUP BY n.id;
Second, the CROSS JOIN (or equivalently, JOIN without an ON clause) doesn't make sense. Normally, I would expect a join condition to one of the other tables.
Use DISTINCT in the GROUP_CONCAT
...
CAST(GROUP_CONCAT(DISTINCT ngm.members_group_id) AS CHAR(1000)) AS news_groups,
CAST(GROUP_CONCAT(DISTINCT member_groups.group_id) AS CHAR(1000)) AS user_groups
...

MySQL: Display one random result which a user has not voted on

I need help on displaying one random result in which the current user has not voted on.
Currently my database setup and the last query I have tried can be found on http://sqlfiddle.com/#!2/2f91b/1
Basically I can isolate each individual item using this query:
SELECT a.img_url, a.item_id, a.user_id, a.img_status, b.item_question, c.user_name, c.user_fbid, d.voter_id, count(d.img_id) AS totalVotes
FROM os_photos a
LEFT JOIN os_items b ON a.item_id = b.item_id
LEFT JOIN os_users c ON a.user_id = c.user_id
LEFT JOIN os_votes d ON a.img_id = d.img_id
GROUP BY a.img_id
ORDER BY RAND()
LIMIT 1
My Problem is: With the SQL knowledge that I have, I am unable to isolate the results to show only the rows in which user #2 has not voted on. I realize the problem is when I use group by, it combines the voter_id and therefore I am unable to check if user #2 has had any input for the item.
Example:
Item # | voter_id
1 | 2
1 | 3
2 | 2
3 | 1
3 | 4
4 | 3
4 | 1
5 | 1
5 | 2
With the above sample set, the resulting item should be either item #3, #4 or any other items which have not been voted on.
Your help, advise and knowledge is greatly appreciated.
To get the items that dont exist you need a LEFT JOIN with condition that would otherwise make a positive match, and then add a WHERE clause matching one of the resulting columns to NULL:
SELECT a.img_url, a.item_id, a.user_id, a.img_status, b.item_question, c.user_name,c.user_fbid, d.voter_id, count(d.img_id) AS totalVotes
FROM os_photos a
LEFT JOIN os_items b ON a.item_id = b.item_id
LEFT JOIN os_users c ON a.user_id = c.user_id
LEFT JOIN os_votes d ON a.img_id = d.img_id
LEFT JOIN os_votes d2 ON a.img_id = d2.img_id AND d2.voter_id=2
WHERE d2.voter_id IS NULL
GROUP BY a.img_id
ORDER BY RAND()
LIMIT 1