Difficult MySQL Query - mysql

In our database tables we keep a number of counting columns to help reduce the number of complex look-up queries. For example, in our users table we have columns for the number of reviews written, photos uploaded, friends, followers, etc. To help make sure these stay in sync we have a script that runs periodically to check and update these counting columns.
I've been attempting to write an efficient query that calculates the number of friends a specific user has and haven't been able to figure out how to do it. In our friend model someone is a friend if they add you as a friend (no confirmation needed), so you have to count the number of unique of people when adding the number of friends you have added plus the number of people who have added you as a friend.
Here are two queries that each work to update the friend count for all the rows in our users table for friendship in a single direction. What I can't figure out it how to combine them so you get the total number of unique friends a user has:
Users Who Added You As A Friend
UPDATE users
JOIN (SELECT cid2, COUNT(*) as c
FROM connections
JOIN users ON connections.cid1 = users.user_id
WHERE connection_type = "MM"
AND connections.status="A"
AND users.status != "D"
GROUP BY cid2) f ON f.cid2 = users.user_id
SET users.friends = f.c
WHERE users.status != "D";
Users Who You Added As A Friend
UPDATE users u
JOIN (SELECT cid1, COUNT(*) as c
FROM connections
JOIN users ON connections.cid1 = users.user_id
WHERE connection_type = "MM"
AND connections.status = "A"
AND users.status != "D"
GROUP BY cid1) f ON f.cid1 = users.user_id
SET users.friends = f.c
WHERE users.status != "D";

Attempt 3...
UPDATE
users
INNER JOIN
(
SELECT
connections.cid1 AS user_id,
COUNT(*) AS total
FROM
(
SELECT cid1, cid2 FROM connections WHERE connection_type = 'MM' AND status = 'A'
UNION
SELECT cid2, cid1 FROM connections WHERE connection_type = 'MM' AND status = 'A'
)
AS connections
INNER JOIN
users
ON users.user_id = connections.cid2
AND users.status != 'D'
GROUP BY
connections.cid1
)
AS friends
ON friends.user_id = users.user_id
SET
users.friends = friends.total
WHERE
users.status != 'D';
(Other options deleted at OP's request. See edit history if interested.)

You could write it as a stored procedure which does both SELECT COUNT() and then adds it to a counter variable, then issue a single UPDATE

How important is the status flag? You're going to run into trouble trying to update the users table with a sub-query which references the same table.
Consider:
UPDATE connections SET status='D' WHERE cid1 IN
(SELECT user.id
FROM users u
WHERE status='D');
UPDATE connections SET status='D' WHERE cid2 IN
(SELECT user.id
FROM users u
WHERE status='D');
UPDATE users u
SET friends = (
SELECT COUNT(DISTINCT friend)
FROM (
SELECT c1.cid2 as friend
FROM connections c1
WHERE c1.connection_type = 'MM'
AND c1.connections.status='A'
AND c1.status<>'D'
AND c1.cid1=u.id
UNION
SELECT c2.cid1 AS friend
FROM connections c2
WHERE c2.connection_type = 'MM'
AND c2.connections.status='A'
AND c2.status<>'D'
AND c2.cid1=u.id
)
)

Related

How to understand mysql inner join?

I have a query as below:
users = select * from users where group_id = 1 and track_id = "xxxxxx";
user_ids = [user.id for user in users]
tickets = select * from tickets where group_id = 1 and user_id in (user_ids);
users and tickets tables all have the group_id field.
The index of users table are on group_id and track_id fields;
The index of tickets table are on group_id and user_id fields.
I want to merge two sql to one.
select * from tickets join users on tickets.user_id = users.id
where tickets.group_id = 1 and users.track_id = "xxxxx";
or
select * from tickets join users on tickets.user_id = users.id
where users.group_id = 1 and users.track_id = "xxxxx";;
Is it right? which is better? I how what is the meaning of inner join, but I want to know I should use tickets.group_id or users.group_id? What is the difference?
How to understand mysql inner join? How does it works?
You need to test both group_id fields, just like you do in the queries that you're merging.
SELECT *
FROM tickets AS t
JOIN users AS u ON t.user_id = u.user_id
WHERE t.group_id = 1
AND u.group_id = 1
Consider additional join criteria: If the group_ID will always be the same in both tables then you should add group_ID as join criteria and filter by either or.
SELECT *
FROM tickets join users
on tickets.user_id = users.id
and tickets.group_ID = users.group_ID
WHERE tickets.group_id = 1 and users.track_id = "xxxxx";
Alternatively you must filter based on both: because group_ID could be different than the ticket_ID, you must filter by both.
This most accurately represents the union of your two queries. if the group_ID is not the same for tickets and users when joined.
SELECT *
FROM tickets join users
on tickets.user_id = users.id
WHERE tickets.group_id = 1
and users.group_id = 1
and users.track_id = "xxxxx";
Logically however, we would need to understand the purpose of "GROUP_ID" in both tables. What is the functional purpose of each? I can envision selling off groups of tickets... but I don't see how a user could be in a static group as depending on the event, a user could be in more than one group over time.
Perhaps you shouldn't have group_ID in both tables? Perhaps you needa user Groups table...
If the two groups represent different fundamental attributes of the entity, then they must be evaluated separately; thus the 2nd query makes the most sense. However if they represent the same entity attribute then adding to the join criteria makes more sense
Your first query will give you all tickets for group 1 and the users could be in any group. The second query will give you all tickets for users in group 1 even if the ticket is in group 2.

Return a set of SQL Results based on criteria from other tables

I wasn't sure how to actually title this question, but if you got in here, I'll try to explain it to the best of my ability.
You can find my SQL Fiddle here.
SELECT *
FROM `challenges`
LEFT JOIN `challenges_competitors` ON `challenges_competitors`.`challenge_id` = `challenges`.`id`
LEFT JOIN `users` ON `users`.`id` = `challenges_competitors`.`user_id`
WHERE `challenges`.`owner_id` != 3 AND `challenges`.`status` = 'pending'
ORDER BY `challenges`.`id`;
Basically what I want to do with the query is return any challenge that user3 does not own or is part of.
There are two tables involved for this. The first table is challenges. This table holds the user id as owner_id, as well as the challenge id and some other data. Then there's the challenges_competitors table that holds challenge_id and user_id to connect that table with both challenges and the users.
When I run the query and join the tables, there are bound to have duplicates because a challenge can have many competitors. So what I want to do is if there is a challenge that the user3 does not own, but he is part of this challenge, to not get that row back.
I really hope I explained this well. lol
To get the most basic information about which challenges (their id) aren't being owned by users.id = 3 and that don't have that user as a competitor altering your query by adding NOT EXISTS clause would be sufficient.
SELECT c.id
FROM `challenges` AS c
WHERE
c.`owner_id` <> 3 -- discard challenges that have user 3 as their owner
AND c.`status` = 'pending'
AND NOT EXISTS ( -- discard challenges that have user 3 as their competitor
SELECT 1
FROM `challenges_competitors` AS cc
WHERE
cc.`user_id` = 3 -- limit this query to return only rows where user 3 is a competitor
AND cc.`challenge_id` = c.`id` -- join condition with challenges table
);
Here's your modified SQL FIDDLE.
To get the entire set of columns that you have attached in your query you could keep the joins, as #xQbert suggested in his comment.
SELECT *
FROM `challenges` c
LEFT JOIN `challenges_competitors` cc
ON cc.`challenge_id` = c.`id`
LEFT JOIN `users` u ON u.`id` = cc.`user_id`
WHERE
c.`owner_id` != 3
AND c.`status` = 'pending'
AND NOT EXISTS (
SELECT 1
FROM `challenges_competitors` cc2
WHERE cc.`id` = cc2.`id` and cc2.`user_id` = 3
)
ORDER BY c.`id`;
Because owner is also a row in the user table you need to include the user table as an alias. I'm not quite sure what you need to get back - I don't understand exactly what the question is, but you should be able to add a where clause to this to get what you want.
SELECT *
FROM `challenges`
JOIN `challenges_competitors` ON `challenges_competitors`.`challenge_id` = `challenges`.`id`
JOIN `users` ON `users`.`id` = `challenges_competitors`.`user_id`
JOIN `users` AS `owners` ON `owners`.`id` = `challenges`.`owner_id`

mySQL SELECT help. IF or EXISTS?

I have this DB structure
* user
user_id
name
* client
client_id
name
* user_client
user_client_id
user_id
client_id
* message
message_id
client_id
description
If there are entries on user_client then the user has permissions restricted to the specific clients listed for his id on the table. If there are no entries, then the user has access to any client.
How can I select only messages that the user can read?
I'm trying to do an IF on the WHERE clause to check if any entries on the user_client table but I don't know where to go from there. It needs to select all messages from any client if no entries on user_client or only messages for client_id specified on user_client table
Thanks for the help!
I would suggest doing two different queries: one for the superusers and the other for the restricted users. Then you can join the two results with a UNION.
SELECT M.message_id,
M.client_id,
M.description
FROM message M
INNER JOIN user_client UC ON (UC.client_id = M.client_id)
INNER JOIN user U ON (UC.user_id = U.id)
WHERE U.id = :user_id
UNION
SELECT M.message_id,
M.client_id,
M.description
FROM message M
WHERE NOT EXISTS (
SELECT *
FROM user_client
WHERE user_id = :user_id
)
You can obtain the same result with other queries but IMHO this one is clearer and more maintainable.
Edit: If you want to ensure that the user exists you should join the second query with the user table.
SELECT M.message_id,
M.client_id,
M.description
FROM message M
JOIN user U
WHERE U.id = :user_id
AND NOT EXISTS (
SELECT *
FROM user_client
WHERE user_id = :user_id
)
One way the do this could be to use two different queries to create a set of the messages users can see and filter according to your needs; something like this should work:
select * from (
select u.user_id, u.name, c.name client, m.message_id, m.description
from user u
join user_client uc on u.user_id = uc.user_id
join client c on uc.client_id = c.client_id
join message m on c.client_id = m.client_id
union all
select u.user_id, u.name, c.name client, m.message_id, m.description
from user u
cross join client c
join message m on c.client_id = m.client_id
where user_id not in (select user_id from user_client)
) x
where x.user_id = 1;
Here users present in the user_client table are restricted to the set of messages that they have access to (the first set in the union), while users not present in the user_client table can see all messages (the second set in the union).
Sample SQL Fiddle
If I am understanding your question correctly, such as
1) Administrative user... They can look at EVERYTHING since they would have no records in the user_client table.
2) Client Supervisor... Such a person who's primary responsibility is to a specific client (or multiple clients). Therefore, the user DOES have a record in the user_client table. If so, then only allow the user to see records for those clients they DO have associations with.
select
m.message_id,
m.client_id,
m.description,
c.name as clientName
from
( select count(*) as HasClients
from user_client
where user_id = TheUserYouWant ) ClientCheck,
message m
left join user_client uc
on m.client_id = uc.client_id
AND uc.user_id = TheUserYouWant
join client c
on m.client_id = c.client_id
where
ClientCheck.HasClients = 0
OR NOT uc.user_id IS NULL
The query looks at the user_client table TWICE. The first time is to just get a count of those records that DO exist for the given user, regardless of which client associated with. The query will always come back with 1 row and it will either be 0 (no such records), or greater than 1 (however many they are associated with).
The second instance is a LEFT-JOIN to the user_client table, JUST IN CASE the person IS restricted to only looking at their own client messages.
The WHERE clause now comes in and says... if the underlying count of clients was zero, then ok to give me all messages. If any other value, then the user ID in the user_client table (as left-joined to the messages on both the CLIENT AND THE USER you want) MUST EXIST (via NOT a NULL value for the user_id column).
Now, you probably don't want to query EVERY message as it could get quite large as your database grows, but you could put whatever other criteria in the WHERE clause, such as date restrictions and/or client(s) you are interested in.

mySQL JOIN query for users with 0 assigned records

I have 2 tables, voter and user. I want to show all users with the total # of assigned voter records next to their user info.
At this point, this query kind of works -- it shows the total for users who have any number of records assigned, but it DOESN'T show users who have 0 records assigned in the voter table.
Voter records are assigned to a user with the field voter.owner. Here's what I have currently:
SELECT u.id
,u.admin
,u.active
,u.name
,u.email
,u.last
,if(count(v.owner) is NULL,'0',count( v.owner )) as assigned
from user u
left join voter v
on u.id = v.owner
where u.cid = '$cid'
order by u.id asc
Any advice on how to show all users, even ones that don't own and voter records?
if(count(v.owner) is NULL,'0',count( v.owner )) as assigned
should be...
SUM(if(v.owner is NULL,0,1) as assigned
A Left-Join is the wrong approach for this Problem.
The solution is to use a sub-select like so:
SELECT `user.id`, `user.admin`, `user.active`, `user.name`, `user.email`, `user.last`,
(SELECT COUNT(*) FROM voter WHERE `user.id` = voter.owner)
FROM `user`
WHERE user.cid = '$cid'
ORDER BY `user.id` ASC

Selecting using two column names, using the other one if one is known of each record

I'm not entirely sure how to phrase this as a concise question title.
I have a games table which contains of records representing games of a player against another. The relevant columns of this table are userid1 and userid2. I have another table called accounts, consisting of the columns id and username.
I'd like to fetch the username of the opponent from the accounts table. The opponent is the user of which the id is not equal to the known user id. So, if userid1 = 123 then I'd like to fetch accounts.username of the user where accounts.id is equal to userid2 of the same games record. The same goes the other way round if userid2 = 123.
I've so far only been able to select the opponent's username separately using two queries:
SELECT * FROM games, accounts
WHERE games.userid1 = 123 AND accounts.id = games.userid2
and:
SELECT * FROM games, accounts
WHERE games.userid2 = 123 AND accounts.id = games.userid1
(swapped: ^ ^)
However, I'd like to combine these two queries nicely into one. So, is it possible to get accounts.username where accounts.id is equal to:
games.userid2 if games.userid1 is equal to the known user id
games.userid1 if games.userid2 is equal to the known user id
and if so, how?
You can use a case statement in your join condition, something like this:
SELECT * FROM games g
JOIN accounts a
ON a.id = case g.userid1 when ? then g.userid2 else g.userid1 end
WHERE
g.userid1 = ? OR g.userid2 = ?
However, depending on your indexes, it may be quicker to use a union, eg.
SELECT * FROM games g
JOIN accounts a ON a.id = case g.userid2
WHERE g.userid1 = ?
UNION ALL
SELECT * FROM games g
JOIN accounts a ON a.id = case g.userid1
WHERE g.userid2 = ?
An alternative query using OR,
SELECT * FROM games g, accounts a
WHERE
(g.userid1 = ? AND g.userid2 = a.id)
OR (g.userid2 = ? AND g.userid1 = a.id)