multi-table mysql query - mysql

I am trying to make a multi-table query that I am not quite sure how to do properly. I have User, Message, Thread, and Project.
A User is associated with Message/Thread/Project as either the Creator or as it being 'shared' with them.
A Message is contained within a Thread (associated by message.thread_id and thread.id), and a Thread is contained within a Project (associated by thread.project_id and project_id).
I would like to create a query where given a User.id value, it will return all messages that the user has access to, as well as the Thread and Project name that that message is under, both as Creator or 'Shared'. I use a table to handle the 'shares'. The rough diagram is: http://min.us/mvpqbAU
There are more columns in each, but I left them out for simplicity.

I've made some assumptions on column names for message, project_name and thread_name as they are not included in the diagram.
/* Get messages where user is creator */
select u.name, m.message, p.project_name, t.thread_name
from user u
inner join message m
on u.id = m.owner_user_id
inner join thread t
on m.group_id = t.id
inner join project p
on t.project_id = p.id
where u.id = #YourUserID
union
/* Get messages where user has shared access */
select u.name, m.message, p.project_name, t.thread_name
from user u
inner join message_share ms
on u.id = ms.user_id
inner join message m
on ms.message_id = m.id
and m.owner_user_id <> #YourUserID
inner join thread t
on m.group_id = t.id
inner join project p
on t.project_id = p.id
where u.id = #YourUserID

I would urge you not to proceed with this design.
You have too many permissions levels spread over individual areas. I would suggest that you alter it so members have groups, which then in turn can be parts of projects, and threads. At a message level, you are looking at an administration nightmare having that level of permissions structure on individual messages.

Related

SQL query to find users with apps with no releases

In my database, I have users, apps, and releases. A user can have 0..n apps through a permissions table and an app can have 0..n releases.
I'm trying to get a list of users who have at least 1 app, but none of that user's apps have any releases.
The schema is roughly
users permissions apps releases
----- ----------- ---- --------
id user_id id id
email app_id app_id
I think I've got something working with this, but it appears inefficient to me because I mention the permissions table twice and I'm using nested exists clauses. Is there a more efficient way to write this query?
select u.email from users u
join permissions p on p.user_id = u.id
where not exists (
select a.id from apps a
join permissions p on p.app_id = a.id
where p.user_id = u.id and exists (
select r.id from releases r
where r.app_id = a.id
)
);
You just need to use a LEFT JOIN on releases, and then look for the case where the number of released apps (r.app_id is non-NULL) is 0. If all you want is a list of users, I don't think you need to JOIN the apps table at all, as JOINing on permissions will ensure that only users that have permission for 1 or more apps are included.
SELECT u.email
FROM users u
JOIN permissions p ON p.user_id = u.id
LEFT JOIN releases r ON r.app_id = p.app_id
GROUP BY u.email
HAVING COUNT(r.app_id) = 0
The first Join seems to be correct between users and permissions table. You just need to check whether the app_id from joined result-set exists in releases table or not. You can try this query -
select u.email from users u
join permissions p on p.user_id = u.id
where not exists ( Select 1 from releases r where r.App_id = p.app_id)
I will do something like this, hope this helps:
SELECT
u.id, u.email
FROM
users AS u
INNER JOIN
permissions AS p ON p.user_id = u.id
LEFT JOIN
releases AS r ON r.app_id = p.app_id
GROUP BY
u.id, u.email
HAVING
SUM(CASE WHEN r.id IS NOT NULL THEN 1 ELSE 0 END) = 0
Another thing you could try is a combination of left and inner joins like this:
Select
email
From users u
Inner Join (
Select
p.user_id
, p.app_id
From permissions p
Left Join releases r
on p.app_id = r.app_id
Where r.app_id is null) a
on u.user_id = a.user_id
Group by email
It's hard to tell which is faster between this and the previous posted solution without knowing the size of the different tables (and hence how many rows SQL will be trying to join).
One thing that is clear - without the 'Group by email' line at the end, you might see users' email repeated multiple times in your list. Generally, literature on SQL states that using a "group by" statement at the end of your query is a faster way to get a distinct set than a "select distinct" statement at the beginning of your query.

MySQL LEFT JOIN working but slow, INNER works when the db is populated

I am building a statistics page for users of my site to see how much they have been interacting with the various features. On the site they can do four different types of activity, get points for them, and give feedback to other users on their answers. These are all recorded in the database.
In the points tables, a row is created when a user does something for the first time, for example watches a particular video. There is a new row for each video.
In the feedback tables, a row is created for each unique piece of feedback, with user id, feedback etc.
The problem I am having is that whlst two types of join work to get the expected output, INNER JOIN and LEFT JOIN they have have radically different execution times.
LEFT JOIN executes but takes forever (and subsequently crashes my low memory computer!).
INNER JOIN executes but only if all the tables have at least one matching row for the user.
Im sure therefore I am missing some efficiency step here, or misunderstanding the usage of the joins in this context. I could use help looking at the SQL statement and looking at how it could be improved.
Fiddle Link: http://sqlfiddle.com/#!9/9dce51 (with limited data due to size!)
The problems are with the groups of eight below - the other queries execute swiftly and properly.
SELECT users.forename, users.surname, users.email,
users_peer_request.user_id, COUNT(DISTINCT users_peer_request.id) as peer_req_cnt,
COUNT(DISTINCT users_self_assessment.id) AS self_assess_cnt, AVG(users_self_assessment.assessment) AS self_assess_avg,
GROUP_CONCAT(DISTINCT classes.name, '-', classes.id SEPARATOR ',') AS classes,
COUNT(DISTINCT a_experiment_points.id) AS tried_exp,
COUNT(DISTINCT a_phetlabs_points.id) AS tried_phet,
COUNT(DISTINCT a_videonotes_points.id) AS tried_video,
COUNT(DISTINCT a_working_points.id) AS tried_work,
COUNT(DISTINCT a_experiment_feedback.id) AS fb_exp,
COUNT(DISTINCT a_phetlab_feedback.id) AS fb_phet,
COUNT(DISTINCT a_videonotes_feedback.id) AS fb_video,
COUNT(DISTINCT a_working_feedback.id) AS fb_work
FROM users_peer_request
LEFT JOIN classes ON classes.id IN (SELECT classes_students.class_id FROM classes_students WHERE classes_students.student_id = users_peer_request.user_id)
LEFT JOIN users ON users.gid = users_peer_request.user_id
LEFT JOIN users_self_assessment ON users_self_assessment.user_id = users_peer_request.user_id
INNER JOIN a_experiment_points ON a_experiment_points.user_id = users_peer_request.user_id
INNER JOIN a_phetlabs_points ON a_phetlabs_points.user_id = users_peer_request.user_id
INNER JOIN a_videonotes_points ON a_videonotes_points.user_id = users_peer_request.user_id
INNER JOIN a_working_points ON a_working_points.user_id = users_peer_request.user_id
INNER JOIN a_experiment_feedback ON a_experiment_feedback.user_fb_id = users_peer_request.user_id
INNER JOIN a_phetlab_feedback ON a_phetlab_feedback.user_fb_id = users_peer_request.user_id
INNER JOIN a_videonotes_feedback ON a_videonotes_feedback.user_id_fb = users_peer_request.user_id
INNER JOIN a_working_feedback ON a_working_feedback.user_fb_id = users_peer_request.user_id
WHERE users_peer_request.user_id = 123456789123456789123456
GROUP BY users_peer_request.user_id
This query run on the fiddle provided I would hope returns NULL or 0 where the user does not appear in the table, and a number otherwise which counts that value.
Thanks so much, I hope you have enough detail here but if not please ask :)

SELECT, 2 counts from 2nd table, RIGHT JOIN on 3rd

I'm trying to gather "followers" for a specific user (#1 in this code).
I'm doing my primary select from followers as the column following will have user #1 and followers.userid will have the userid of the person doing the following.
Next I'm trying to get a count of records from the experiences that have the user id of the follower (how many experiences does this follower have?)
Next, the follower will have rated each experience (1-5 stars) and I want to sum those ratings (experiences.stars) to get an average rating of all experiences.
Lastly, I want to join the followers user record from the users table.
I should end up with
userid, jobs, stars, * from users
SELECT * FROM followers AS F
RIGHT JOIN
(SELECT count(id) FROM experiences AS M WHERE M.userid = F.userid) AS jobs
RIGHT JOIN
(SELECT sum(stars) FROM experiences AS S WHERE S.userid = F.userid) AS stars
RIGHT JOIN
users AS U ON U.userid = F.userid
WHERE F.following = 1 /* #1 = the user # I want the follwers of/for */
I've also tried:
SELECT * FROM followers AS F,
(SELECT count(id) FROM experiences AS M WHERE M.userid = F.userid) AS jobs,
(SELECT sum(stars) FROM experiences AS S WHERE S.userid = F.userid) AS stars
RIGHT JOIN
users AS U ON U.userid = F.userid
WHERE F.following = 1 /* #1 = the user # I want the follwers of/for */
In cPanel, I'm getting an error that I have syntax error at WHERE F.userid in both statements.
A) what am I missing and B) is there a better way to do this?
It seems to me, the query would be easier to follow like so:
SELECT *
FROM followers AS F
LEFT JOIN users AS U ON U.userid = F.userid
LEFT JOIN (SELECT count(id) FROM experiences AS M WHERE M.userid = **F.userid)** AS jobs
LEFT JOIN (SELECT sum(stars) FROM experiences AS S WHERE S.userid = F.userid) AS stars
WHERE F.following = 1 /* #1 = the user # I want the follwers of/for */
;
All those RIGHT JOINs you originally had would only give you followers that had both "types" of experiences.
Also, correlated subqueries can be expensive (and you didn't need two of them...actually, you didn't even need subqueries), so I'd also rework it like so....
SELECT F.*, U.*, count(x.id), sum(x.stars)
FROM followers AS F
LEFT JOIN users AS U ON U.userid = F.userid
LEFT JOIN experiences AS x ON F.userid = x.user_id
WHERE F.following = 1
GROUP BY [all the fields selected in F and U, or just F.userid if server settings allow]
;
Seems like there's a couple of ON clauses missing.
I know that RIGHT outer joins are supported, but why would we write it that way, and not write it as LEFT outer joins. (We typically reserve RIGHT joins to the towers of academia.)
And it's well past time to ditch the old-school comma syntax for join operations. (Yes, it's still supported for backwards compatibility with existing statements. But new development should use the newer JOIN syntax.)
The condition requiring a non-NULL value of F.following would effectively negate the "outerness" of the join, rendering it equivalent to an INNER join. For clarity, we should either write that as an inner JOIN, or if we want an outer join, we should relocate that condition to the appropriate ON clause.
Also, best practice is to qualify all column references; even when they aren't ambiguous to the optimizer, it makes it easier on the future reader (so the future reader doesn't have to confirm which table contains the id column), as well as protecting the query from throwing "ambiguous column" errors in the future if a column named id is added to another table used by the query.
Also, it's not valid to reference columns from F in the outer query inside inline view queries. We can use a correlated subquery, but not as an inline view.
The specification isn't clear. Example data and sample of expected output would go a long ways to clarifying the requirements.
If we want to use correlated subqueries that return a single row, with a single column, we can put those in the SELECT list ...
SELECT f.*
, u.*
, ( SELECT COUNT(m.id)
FROM experiences m
WHERE m.userid = f.userid
) AS jobs
, ( SELECT SUM(s.stars)
FROM experiences s
WHERE s.userid = f.userid
) AS stars
FROM followers f
LEFT
JOIN users u
ON u.userid = f.userid
WHERE f.following = 1 /* #1 = the user # I want the follwers of/for */
ORDER BY ...
We could get an equivalent result using inline views, but that would look quite different.
I would tend to do the aggregation inside the inline view, something along the lines of this:
SELECT f.*
, u.*
, IFNULL(e.jobs,0) AS jobs
, IFNULL(e.stars,0) AS stars
FROM followers f
LEFT
JOIN users u
ON u.userid = f.userid
LEFT
JOIN ( SELECT ef.userid
, COUNT(ee.id) AS jobs
, SUM(ee.stars) AS stars
FROM followers ef
JOIN experiences ee
ON ee.userid = ef.userid
WHERE ef.following = 1 /* argument */
GROUP BY ef.userid
) e
ON e.userid = f.userid
WHERE f.following = 1 /* argument */
ORDER BY ...

User permissions from user own permission and group permission

i'm developing a user permission system, here is my eer
each user can have permission if:
he is a member of a group and that group have permission
he have record in user_permission table
so to get all permission that a user have, i must get a union of group permission that the user belong to, and the permission the user have in user_permission table
that the sql statement that I found as solution, but I don't know if it is the good one, (can I have the result using only one SELECT statement)
select statement:
SELECT p.name FROM permission p
JOIN group_permission as gp ON gp.permission_id = p.id
JOIN `group` as g ON g.id = gp.group_id
JOIN `user_group` as ug ON ug.group_id = g.id
where ug.user_id = 5
UNION
SELECT p.name FROM permission p
JOIN `user_permission` as up ON up.permission_id = p.id
where up.user_id = 5
here is the mysql dump of my database:
https://pastebin.com/2Kaq8fVs
The 3rd line is not necessary. You can join gp and ug via group_id. Keep your group table for securing foreign keys, but you don't need it for this kind of query. Everything else is OK for your requirement as you described it.
Note: Reading all permissions without regularly reloading them causes changed permissions not to be recognized by your application. If this is a problem, you could additionally constrain your query with a specific permission.

Select only some entries based on what's in another table

I'm trying to grab all of the offers provided to a user that are not already queued up in their projects. So, I'm grabbing offers from the OfferSuggestionHeader table based on my user's ID but also try to make sure it doesn't grab anything that the user has already added to their projects (stored in the Projects table)
I've got the following query:
SELECT DISTINCT ofh.OfferID, ofh.OfferTitle, ofh.OfferVendor, ofh.Savings,ofh.SavingsPercent
FROM OfferSuggestionHeader ofh
LEFT JOIN OfferSuggestionDetail osd
ON ofh.OfferID = osd.OfferID
LEFT JOIN Facilities f
ON osd.FacilityID = f.id
LEFT JOIN UserFacility uf
ON f.id = uf.fid
LEFT JOIN Users u
ON uf.uid = u.uid
LEFT JOIN Projects p
ON p.uid = u.uid
WHERE p.uid = '1'
AND ofh.OfferID <> ANY (SELECT offer_id FROM Projects WHERE uid = '1')
It pulls up all of the offers. If I take away ANY then I get an error saying that the subquery returns too many results.
There are 6 offers. Three are queued up by user 1. I shouldn't see offers 1, 4, or 5.
Thanks for any pointers and help.
As per my comment explanation, why not try this? I am not sure why you need a subquery when you already have PROJECT table JOINED in your main query..
WHERE p.uid = '1'
AND ofh.OfferID NOT IN (1, 4, 5);