SQL - How would I select data involving multiple tables? - mysql

RDMS: MySQL
I'm designing a private messaging thread system and have the following schema laid out for the messaging system (simplified). I need to select every conversation that an account with a known ID (we'll say '1') is in, the last message that was sent in the conversation, and the username of the other account that account '1' is chatting with. Grouped by the conversation id. (imagine an inbox page like Facebook has)
My SQL knowledge is limited (I'm going to spend the weekend advancing it) but it the meantime does anyone have a query that would get the job done?
Thanks in advance. Let me know if you need more info.
Schema
EDIT: I believe I may have found a solution (still needs proper testing).
I'm still accepting answers as I'm not sure this is the best route to go yet. I've reviewed the current answers posted and they are getting me in the right direction but so far aren't returning the right results.
Here's what I've come up with.
SELECT account_has_conversations.account_id, messages.*, account.username FROM messages, account, account_has_conversations WHERE account_has_conversations.conversation_id IN
(
SELECT account_has_conversations.conversation_id FROM account_has_conversations WHERE account_has_conversations.account_id = '1'
)
AND account_has_conversations.account_id != '1' AND account_has_conversations.account_id = account.account_id
AND account_has_conversations.conversation_id = messages.conversation_id
GROUP BY account_has_conversations.conversation_id
(I probably should have created some aliases haha)

First, you don't have any text fields in any of the tables in your pictured schema - so I don't know where you're storing the actual message content or how to retrieve it. That said, this query should work for you and you can just add the field that you need.
This will get you the username, the last message received, and the sending username for all of the selected user's conversations. I will ASSUME the message content is in the messages table and is stored in a field called "message_text":
DECLARE #AccountID int = 1 //For testing, but this should be parameterized
SELECT A.username as [User],
A2.username as [SendingUser]
M.message_text as [LastMessage]
FROM account A
JOIN account_has_conversations AHC
ON A.account_id = AHC.account_id
JOIN conversation C
ON AHC.conversation_id = C.conversation_id
JOIN
(
Select
sender_account_id,
conversation_id,
message_text,
row_number() over(partition by conversation_id order by time_sent desc) as rn
FROM messages
) as M
ON C.conversation_id = M.conversation_id
AND M.sender_account_id <> #AccountID //This prevents a circular join in case the last message was sent by the user. You may not need this, but it's impossible to tell based on the information you provided.
JOIN account A2
ON M.sender_account_id = A2.account_ID
WHERE rn = 1
AND A.account_id = #AccountID

I have tested this, but maybe this will get you in the right direction.
Select m.*
from account a
inner join account_has_conversation ahc
on (a.account_id = account_id)
inner join messages m
on (ahc.conversation_id = m.conversation_id)
where a.account_id = 1
I would add:
Your conversation table doesn't really seem to have any value.
Group By is great, but you're going to need to squeeze all your results down as well. By that I mean that if you want to squish ten rows into one (grouping by one of the columns where all the rows have the same value in that column), you'll need to have all the other columns in an aggregate function.

Based on the new schema as below.
messages(msg_id, con_id, message, time_sent, status)
acc(acc_id, username)
con(con_id)
conrel(con_id, acc_id)
(typing from phone so ignore any smaller mistakes).
SELECT distinct msg.message, conrel.con_id, acc.username FROM messages AS msg INNER JOIN conrel ON conrel.con_id = msg.con_id INNER JOIN acc ON conrel.acc_id=acc.acc_id where conrel.con_id IN (SELECT DISTINCT con_id FROM connrel WHERE acc_id=1) ORDER BY msg.time_sent DESC
The solution can further be improved or changed according to needs. Plus remember to use coma separated result of INNER query above instead as INNER queries are slower but when they are passed with IDs like 1,4,5 they become fast.

SELECT a.username, m.*, c.* FROM account a, messages m, conversation c
WHERE a.account_id = m.sender_account_id AND m.conversation_id = c.conversation_id

Related

SQL query that returns all the ids without a certain value

I have a table that maps User and Feature. Basically what features are enabled for each user. The table is |userId|featureId| with one(user) to many(feature) relationship.
I would like to create a query that takes a list of userIds and returns the list of userIds that are missing a specific feature.
Meaning I need to make sure that every id has a specific featureId.
userId featureId
1 A
1 B
2 A
3 C
4 D
3 A
So in this example, I'll get the list of ids (1, 2, 3, 4) and a featureId A and the query will return one row with userId 4 since it's the only userId with the feature A enabled.
To find a list of users that don't have feature X I would left join to the list of users that has that feature and return the ones not there. Like this:
SELECT *
FROM table_you_did_not_name as base
LEFT JOIN (
SELECT DISTINCT userID
FROM table_you_did_not_name
WHERE feature = 'X'
) as sub ON base.userID = sub.userID
WHERE sub.userID is null
I think I may have answered a different question: this doesn't address your data; but I'm unsure how you determine it is user 4 you want returned. as each user is missing some of the features the others have. Perhaps we just need to add a where clause below for the specific feature(A) in your example?
Think of data in terms of sets
You need
a set of data for all users (User or something)
a set of data for all features (feature)
and what features a user has (User_Feature)
Then you need to
Generate a set of every feature to every users (cross join)
Identify which of those the user has identified. (left join in user_feature)
and then only keep those where no feature has been identified (where no record in user_feature)
One method: This basically says return the features for each user that exist in a feature list, but have not been associated to a user.
SELECT U.userID, F.FeatureID as FeatureIDMissing
FROM USER U
CROSS JOIN FEATURE F
LEFT JOIN UserFeature UF
on U.UserID = UF.UserID
and F.FeatureID = UF.FeatureID
WHERE UF.UserID is null
-- and F.FeatureID = 'A' --maybe add this?
Alternate method: (combine two steps (2,3) by simply excluding those features which already exist for the user.
In english this says, return all the features for each user for which a user has not been associated
SELECT U.userID, F.FeatureID as FeatureIDMissing
FROM USER U
CROSS JOIN FEATURE F
WHERE not exists (SELECT *
FROM userFeature UF
WHERE U.UserID = UF.UserID
and F.FeatureID = UF.FeatureID)
--and F.FeatureID = 'A' --maybe add this?
Either answer should return the same results. It's a matter of preference database and performance .. Look at the execution plans to help decide which is best for you and your data.
Now maybe you mean you give a list of userID's you want to generate a unique set of features for all those users, and then return users w/o those features. If so instead of a cross join to feature you just need to use (Select distinct FeatureID from userFeatures where UserID IN ('yourListHere') this will generate a unique set of features for those users and identify which users are missing certain features shared with that set of users.
So...
SELECT U.userID, F.FeatureID as FeatureIDMissing
FROM USER U
CROSS JOIN (SELECT distinct FeatureID
FROM userFeatures
WHERE UserID IN ('yourListHere')F
LEFT JOIN UserFeature UF
on U.UserID = UF.UserID
and F.FeatureID = UF.FeatureID
WHERE UF.UserID is null
-- and F.FeatureID = 'A' --maybe add this?
as an example.

How can I filter out results based on another table. (A reverse join I guess?)

Basically, I have a table which contains two fields: [id, other] which have user tokens stored in them. The goal of my query is to select a random user that has not been selected before. Once the user is selected it is stored in the table shown above. So if Jack selects Jim randomly, Jack cannot select Jim again, and on the flip side, Jim cannot select Jack.
Something like this is what comes to mind:
SELECT * FROM users
WHERE (SELECT * FROM selected WHERE (id=? AND other=?) OR (id=? AND other=?));
Well, first of all I've read that uses sub-queries like this is extremely inneficient, and I'm not even sure if I used the correct syntax, the problem is however, that I have numerous tables in my scenario which I need to filter by, so it would look more like this.
SELECT * FROM users u
WHERE (SELECT * FROM selected WHERE (id=? AND other=?) OR (id=? AND other=?))
AND (SELECT * FROM other_table WHERE (id=? AND other=?) OR (id=? AND other=?))
AND (SELECT * FROM diff_table WHERE (id=? AND value=?))
AND u.type = 'BASIC'
LIMIT = 1
I feel like there's a much, much more efficient way of handling this.
Please note: I don't want a row returned at all if the users id is present in any of the nested queries. Returning "null" is not sufficient. The reason I have the OR clause is because the user's id can be stored in either the id or the other field, so we need to check both.
I am using Postgre 9.5.3, but I added the MySQL tag as the code is mostly backwards comptable, Fancy Postgre only solutions are accepted(if any)
You can left join to another table, which produces nulls where no record is found:
Select u.* from users u
left selected s on s.id = u.id or s.other = u.other
where s.id is null
The or in a join is different, but should work. Example is kinda silly...but as long as you understand the logic. Left join first table to second table, where second table column is not null means there was atleast one record found that matched the join conditions. Where second table column is null means no record was found.
And you are right...avoid the where field = (select statement) logic when you can, poor performer there.
Use an outer join filtered on missed joins:
SELECT * FROM users u
LEFT JOIN selected s on u.id in (s.id, s.other) and ? in (s.id, s.other)
WHERE u.id != ?
AND s.id IN NULL
LIMIT 1

Get Data From 2 Tables in One Query

I am trying to retrieve data for my notification system. I have three tables. One table (notifications) holds the actual information for the notification itself. I have two other tables to keep track of who gets certain notifications. There are two types of notifications, user and global. A global notification goes to all users while a user notification only goes to specific users. For this I have two tables, notifications_users and notifications_global. The table structures are below:
notifications (id, title, url, start, end)
notifications_users (notification_id, user_id, viewed)
notifications_global (notification_id, user_id, viewed)
What I want to do is to grab the notification title and url (from notifications table) along with the viewed value for all notifications that go to a specific user from both notifications_users and notifications_global tables. Would a UNION query be the best option here? I thought about just separating the queries but then I have two different arrays to loop through in my PHP script which I do not want. There has to be a way to grab all of this data with one query into one array. The following gives me an empty set:
SELECT notification.title, notification.url
FROM notifications
RIGHT JOIN notifications_users ON notifications.id = notifications_users.notification_id
RIGHT JOIN notifications_global ON notifications.id = notifications_global.notification_id
WHERE notifications_users.user_id = 11508 AND notifications_global.user_id = 11508;
SELECT a.title, a.url, b.viewed as 'User View', c.viewed as 'Global View'
FROM Notifications a
INNER JOIN Notifications_Users b
ON a.id = b.notification_id
INNER JOIN Notifications_Global c
ON b.notification_id = c.notification_id
WHERE b.user_id = 11508 and c.user_id = 11508
I think I might have been over-complicating this a bit. I just wrote the two queries for each table separate and then put UNION between them. If anyone is interested, here is what I ended up going with.
(SELECT notification_id, notification_title, notification_url, tbl_notifications_users_lookup.viewed
FROM tbl_notifications
INNER JOIN tbl_notifications_users_lookup ON tbl_notifications.id = tbl_notifications_users_lookup.notification_id
WHERE tbl_notifications_users_lookup.user_id = 11508)
UNION
(SELECT notification_id, notification_title, notification_url, tbl_notifications_global_lookup.viewed
FROM tbl_notifications
INNER JOIN tbl_notifications_global_lookup ON tbl_notifications.id = tbl_notifications_global_lookup.notification_id
WHERE tbl_notifications_global_lookup.user_id = 11508);

mysql query finding results using joined status table that needs EXIST and NOT EXIST results

I have been looking around for ages for a solution to my problem.
I have something that works but i am not sure it is the most efficient way of doing things and can't find anyone trying to do this when googling around.
I have a table with customers and a table with statuses that that customer has had.
If I want to find results where a customer has had a status happen I have managed to get the required results using a join, but sometimes I want to be able to find clients where not only has a status been reached but also where a few other statuses haven't been.
Currently I am doing this with a NOT EXISTS Sub query but it seem a bit slow and thinking about it if I have to check after finding a result that matches the first status through all the results again to see if it doesn't match another it could explain the slowness.
for instance a client could have a status of invoiced and a status of paid.
If I wanted to see which clients have been invoiced thats fine, If I want to see which clients have been invoiced and paid thats fine, but if I wanted to see which clients have been invoiced but NOT paid thats where I start having to use a NOT EXIST subquery
Is there another more efficient way around this? or is this the best way to proceed but I need to sort out how mysql uses indxes with these tables to be more efficient?
I can provide more detail of the actual sql if that helps?
Thanks
Matt
If this is over multiple clients then the usual solution would be to have a subselect for the status per client and then use LEFT OUTER JOIN to connect this.
Something like
SELECT *
FROM Clients a
LEFT OUTER JOIN (SELECT ClientId, COUNT(*) FROM ClientsStatus WHERE Status IN (1,2) GROUP BY ClientId) b
ON a.ClientId = b.ClientId
WHERE b.ClientId IS NULL
This (very rough) example is to give you a list of clients who do not have a status of 1 or 2.
You should be able to expand this basic idea to cover the scenarios / data you are dealing with
Edited for below
I have had a play with your SQL. I think you can use a JOIN onto the subselect fairly easily, but this doesn't seem to be checking anything other than whether a claim has had a status of 3 or 95.
SELECT claims.ID, claims.vat_rate, claims.com_rate,
claims.offer_val, claims.claim_value, claims.claim_ppi_amount, claims.claim_amount, claims.approx_loan_val, claims.salutationsa, claims.first_namesa, claims.last_namesa,
clients.salutation, clients.first_name,clients.last_name, clients.phone, clients.phone2, clients.mobile, clients.dob,clients.postcode, clients.address1, clients.address2, clients.town, client_claim_status.person,clients.ID
AS client_id,claims.ID AS claim_id, claims.date_added AS status_date_added,client_claim_status.date_added AS last_client_claim_status_date_added,work_suppliers.name AS refname, financial_institution.name AS lendname, clients.date_added AS client_date_added,ppi_claim_type_2.claim_type AS ppi_claim_type_name
FROM claims
RIGHT JOIN clients ON claims.client_id = clients.ID
RIGHT JOIN client_claim_status
ON claims.ID = client_claim_status.claim_id
AND client_claim_status.deleted != 'yes'
AND ((client_claim_status.status_id IN (1, 170))
AND client_claim_status.date_added < '2012-12-02 00:00:00' )
LEFT OUTER JOIN (SELECT claim_id FROM client_claim_status WHERE status_id IN (3, 95 )) Sub1
ON claims.ID = Sub1.claim_id
LEFT JOIN financial_institution ON claims.claim_against = financial_institution.ID
LEFT JOIN work_suppliers ON clients.work_supplier_id = work_suppliers.ID
LEFT JOIN ppi_claim_type_2 ON claims.ppi_claim_type_id = ppi_claim_type_2.ID
WHERE claims.deleted != 'yes'
AND Sub1.claim_id IS NULL
ORDER BY last_client_claim_status_date_added DESC
I would suggest that you rearrange the code to remove the RIGHT OUTER JOINs though to be honest. Mixing left and right joins up tend to be very confusing.

MySQL - Join a table twice with a main table

I'm not sure if this can be done. But I just wanted to check with the experts out here.
My case is:
I have a table tbl_campaign which basically stores a campaigns which has a one to many relation with a table called tbl_campaign_user where the users that were selected during the campaign are stored along with the campaign id (tbl_campagin_user.cu_campaign_id = tbl_campaign.campaign_id ).
The second table (tbl_campaign_user) has a status field which is either 0 / 1 denoting unsent/sent. I wanted to write a single sql query which would read the campaign data as well as the number of sent and unsent campaign users (which is why I'm joining twice on the second table).
I tried this below, but I get the same number of count as sent and unsent.
SELECT `tbl_campaign`.*,
COUNT(sent.cu_id) as numsent,
COUNT(unsent.cu_id) as num_unsent FROM (`tbl_campaign`)
LEFT JOIN tbl_campaign_user as sent on (sent.cu_campaign_id = tbl_campaign.campaign_id and sent.cu_status='1')
LEFT JOIN tbl_campaign_user as unsent on (unsent.cu_campaign_id = tbl_campaign.campaign_id and unsent.cu_status='0')
WHERE `tbl_campaign`.`campaign_id` = '19'
I tried debugging by breaking the query into two parts:
=>
SELECT `tbl_campaign`.*,
COUNT(unsent.cu_id) as num_unsent FROM (`tbl_campaign`)
Left join tbl_campaign_user as unsent on (unsent.cu_campaign_id = tbl_campaign.campaign_id and unsent.cu_status='0')
WHERE `tbl_campaign`.`campaign_id` = '19'
The above works exactly as wanted. And so does the one below:
=>
SELECT `tbl_campaign`.*,
COUNT(sent.cu_id) as numsent FROM (`tbl_campaign`)
Left join tbl_campaign_user as sent on (sent.cu_campaign_id = tbl_campaign.campaign_id and sent.cu_status='1')
WHERE `tbl_campaign`.`campaign_id` = '19'
I am not sure what I've been doing wrong while merging the two. I know I don't know much about joins so possibly a conceptual error? Please could anyone help me?
Thx in advance!
You only need to join tbl_campaign_user once and
count (sum, whatever) how many times cu_status was zero/one.
SELECT `tbl_campaign`.id,
count(u.id) as num_all_campaign_users
sum(u.cu_status) as num_sentcampaign_users,
count(u.id) - sum(u.cu_status) as num_unsent_campaign_users
FROM `tbl_campaign` c
LEFT JOIN tbl_campaign_user as u on (u.cu_campaign_id = c.campaign_id)
WHERE `tbl_campaign`.`campaign_id` = '19'
group by `tbl_campaign`.id
Note that this is sort of pseudo code, you may have to elaborate
the sum/count in the select clause and the group by clause as well.