MySQL query to join 3 tables while finding non-matches - mysql

(I could not come up with an concise title)
Hello All
I have been a long time viewer of the site, but after running into an issue I cannot resolve I thought I would sign up to ask a question. I am not sure how to phrase this issue which made searching for help on it difficult and it could very well be that this has been answered and I simply do not know the right search criteria to find the answer. It may also be that there is not an answer for what I am trying to do. Apologies if I have not used the correct procedure to ask.
What I am trying to achieve I can easily do via scripting, and it may be that's really the only appropriate way to do this. My circumstance puts me in a situation where I am being asked if I can do this via MySQL query only.
ISSUE:
Here is the nature of the situation (for which I don't know how to easily summarize into a topic). I have 3 tables in a mySQL database:
Table: account
column: number (unique id)
Table: user
column: number (unique id)
column: account (links the user to an account)
Table: service
column: number (unique id)
column: user (links the service to a user)
column: servdef (the type of service)
Basically an account holds multiple users and each user can hold multiple services.
I have been asked to write a query which will show all accounts (account.number) which do not contain services (service.number) of a particular type (service.servdef). I have seen posts on this site about using NOT EXISTS, but I have not been able to get it to work correctly for my situation. Is this even possible with a single SQL query? Thanks for any help!

Use LEFT JOIN for that since some users have no records on table service. Try,
SELECT a.*
FROM account a
LEFT JOIN `user` b
on a.number = b.account
LEFT JOIN service c
ON b.number = c.`user`
WHERE c.`user` IS NULL OR
c.servdef <> 'particularService'
SQLFiddle Demo

Using NOT EXISTS:
SELECT a.number
FROM account a
WHERE NOT EXISTS (
SELECT u.account
FROM user u INNER JOIN service s ON (u.number = s.user)
WHERE u.account = a.number
AND s.servdef = 'your_particular_type'
)
DEMO (sqlfiddle).

Here is an adaptation of an accepted answer given in another post I saw yesterday.
#servdef is the service type not existing for the accounts you want to find.
select a.number
from account a
inner join user u on a.number = u.account
inner join service s on u.number = s.user
group by a.number
having max(case when s.servdef = #servdef then 1 else 0 end) = 0
(Gordon if you read this, feel free to laugh)

NOT IN is generally the slowest form, but NOT EXISTS may be faster than LEFT JOIN depending on your engine and indexes. Try each one and see.
SELECT a.number
FROM account a
WHERE a.number NOT IN (
SELECT u.account
FROM user u
INNER JOIN service s ON u.number = s.user
WHERE s.servdef = 'your_particular_type'
)
Edit: Wrong reasoning for NOT IN, sorry. Although I find it slightly easier to read than NOT EXISTS it doesn't work well with nullable columns, although I don't think that applies here.

The problem turned out to be that the version of MySQL was too old to support sub queries. One the server was upgraded to new hardware and a modern operating system, the issue was resolved. The other answers are technically correct, but only work in modern versions of MySQL. It was unbeknownst to me at the time of the question that the version of MySQL was a factor. I was not something I even thought to check.

Related

Proper Join Query

I have a table that has every group (gid) a user (uid, say 1031, for example) belongs to and another table that has every group that has permission to edit a given document (pid). I'm trying to figure out the most efficient query to confirm if the user has the permission to edit the said document (e.g. if the user is in any group that is listed as having editing privileges). It seems like there are usually performance issues in JOIN queries I don't anticipate up front.
This is what I came up with. Is there a better way to do this?
SELECT p.gid
, p.pid
, p.r
, p.w
FROM permissions p
JOIN groups_members m
ON m.gid = p.gid
WHERE groups_members.uid = 1

mysql inner join not working as expected

Sorry about this. There are a million posts about this. Somehow, I am still missing something. Simple inner join exactly as this:
http://www.w3resource.com/mysql/advance-query-in-mysql/inner-join-with-multiple-tables.php
Clueless what the problem is. Just trying to add the org to the machine record.
I expect that if there is no match on user_name, the machine record will be dropped, or not, but either way, <= number of records in machines. Getting multiples. Machines.machine_name and user_name are not unique in machines as each machine has multiple software packages tracked.
select users2.org, machines.User_name, machines.Machine_name, machines.model, machines.program, machines.version
from machines inner join users2 on users2.user_name = machines.User_name
You probably want to aggregate here because you will get a cartesian product for all of the machines that share a user_name:
select users2.org,
machines.User_name,
machines.Machine_name,
machines.model
from machines
inner join users2 on users2.user_name = machines.User_name
GROUP BY users2.org, machines.User_name, machines.Machine_name, machines.model
Thank you for your input. The issue was that somehow when the data was massaged (user_name was originally email in 1 case and windows network id in the other), the missing user_names in both tables got changed to ' '. This caused join matches all over the place that I was not expecting. The missing user_name fields were supposed to have different default values. I fixed the default values in the machines table and excluded them with "where machines.User_name <> 'Unknown';" Now I am getting the results I expected. Probably nothing to learn here except understand your data better.

LEFT JOIN - narrow things down

I'm currently having a problem with a legacy app I just inherited on my new job. I have a SQL query that's way too long to respond and I need to find a way to fasten it.
This query acts on 3 tables:
SESSION contains all users visits
CONTACT contains all the messages people have been sending through a form and contains a "session_id" field that links back to the SESSION id field
ACCOUNT contains users accounts (people who registered on the website) and whose "id" field is linked back in SESSION (through a "SESSION.account_id" field). ACCOUNT and CONTACT are no linked in any way, besides the SESSION table (legacy app...).
I can't change this structure unfortunately.
My query tries to recover ALL the interesting sessions to serve to the administrator. I need to find all sessions that links back to an account OR a contact form.
Currently, the query is structured like that :
SELECT s.id
/* a few fields from ACCOUNT and CONTACT tables */
FROM session s
LEFT JOIN account act ON act.id = s.account_id
LEFT JOIN contact c on c.session_id = s.id
WHERE s.programme_id = :program_id
AND (
c.id IS NOT NULL
OR
act.id IS NOT NULL
)
Problem is, the SESSION table is growing pretty fast (as you can expect) and with 400k records it slows things down for some programs ( :programme_id in the query).
I tried to use an UNION query with two INNER JOIN query, one between SESSION and ACCOUNT and the other one between SESSION and CONTACT, but it doesn't give me the same number of records and I don't really understand why.
Can somebody help me to find a better way to make this query ?
Thanks a lot in advance.
I think you just need indexes. For this query:
SELECT s.id
/* a few fields from ACCOUNT and CONTACT tables */
FROM session s LEFT JOIN
account act
ON act.id = s.account_id LEFT JOIN
contact c
ON c.session_id = s.id
WHERE s.programme_id = :program_id AND
(c.id IS NOT NULL OR act.id IS NOT NULL);
You want indexes on session(programme_id, account_id, id), account(id) and contact(session_id).
It is important that programme_id be the first column in the index on session.
#Gordon already suggested you add an index, which is generally the easy and effective solution, so I'm going to answer a different part of your question.
I tried to use an UNION query with two INNER JOIN query, one between
SESSION and ACCOUNT and the other one between SESSION and CONTACT, but
it doesn't give me the same number of records and I don't really
understand why.
That part is rather simple: the JOIN returns a result set that contains the rows of both tables joined together. So in the first case you would end up with a result that looks like
session.id, session.column2, session.column3, ..., account.id, account.column2, account.column3, ....
and a second where
session.id, session.column2, session.column3, ..., contact.id, contact.column2, contact.column3, ....
Then an UNION will faill unless the contact and account tables have the same number of columns with correspoding types, which is unlikely. Otherwise, the database will be unable to perform a UNION. From the docs (emphasis mine):
The column names from the first SELECT statement are used as the column names for the results returned. Selected columns listed in corresponding positions of each SELECT statement should have the same data type. (For example, the first column selected by the first statement should have the same type as the first column selected by the other statements.)
Just perform both INNER JOINs seperately and compare the results if you're unsure.
If you want to stick to an UNION solution, make sure to perform a SELECT only on corresponding columns : doing SELECT s.id would be trivial but it should work, for instance.

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.

How can I "input" a count for a column where it doesn't meet the WHERE criteria?

First of all, I'm terribly sorry for the title. I really had no idea how to properly word this question. If anyone can think of better wording, please either edit it in or tell me to.
On to the question:
I have two tables. usage_logs and servers. servers stores all the server information required for my client to communicate with a specific server. usage_logs, of course, store which user used a particular server, which server, and for how long.
I need a query that selects the server information for the server that is currently being used by the LEAST amount of users.
Here's my current query:
SELECT servers.ip, servers.serverkey, servers.port, servers.id, COUNT(usage_logs.sid) nbr
FROM servers
LEFT JOIN usage_logs ON servers.id = boot_logs.sid
WHERE usage_logs.time+usage_logs.date > 1344076143
AND usage_logs.sid IS NOT NULL
AND servers.status='ONLINE'
GROUP BY sid
ORDER BY nbr DESC;
The 1344076143 can be substituted with the current time (and is, in my actual script). This query almost works. It's selecting a server, but because the other server isn't actually in use, it's not meeting the WHERE usage_logs.time+usage_logs.date > 1344076143 criteria, and thus isn't getting listed in the output.
I need a way for it to still list that server, but I have no idea how.
Also, the usage_logs.sid IS NOT NULL is because I just recently started trying to improve the usage tracking, so prior to today, sid didn't exist.
usage_logs.sid is the server id, and matches up to servers.id.
I've been Googling the hell out of this, and have actually never even used a JOIN statement before, so also please don't hesitate to correct any problems you notice. I desperately want to improve my MySQL skills.
The rows are being excluded because the conditions in your WHERE clause are effectively filtering out rows which have NULL values generated by the LEFT outer join (since WHERE is evaluated after FROM / JOIN).
What you have to do is OR the conditions on the LEFT JOINed table to get either servers that have usage after a certain date OR servers that don't have any usage at all.
Try:
SELECT a.ip,
a.serverkey,
a.port,
a.id,
COUNT(b.sid) nbr
FROM servers a
LEFT JOIN usage_logs b ON a.id = b.sid
WHERE a.status='ONLINE' AND
(b.time + b.date > 1344076143 OR b.sid IS NULL)
GROUP BY a.id
ORDER BY nbr