JOIN - Select columns/details with different criteria than COUNT - php

I have a chat system. There are 3 tables:
data_chats - holds the IDs of the chats themselves. This is where you mark a chat as deleted.
data_chat_parties - holds the member or team ID that is included in chat along with chat permissions, who they were invited by, etc
data_chat_messages - holds the actual messages of chats
With my query, I am trying to fetch the info from data_chat_parties related to the party requesting this information (ie currently logged in user), but also get the total number of chat parties in the chat.
SELECT
data_chats.id AS chat,
data_chats_parties.*,
COUNT(data_chats_parties.id) AS total_parties,
data_chats_messages.created AS last_message_created,
data_chats_messages.author AS last_message_author,
data_chats_messages.author_type AS last_message_author_type,
data_chats_messages.message AS last_message
FROM data_chats
LEFT JOIN data_chats_parties ON data_chats_parties.chat=data_chats.id
LEFT JOIN data_chats_messages ON data_chats_messages.chat=data_chats.id AND data_chats_messages.active=1
WHERE
data_chats.active=1 AND
data_chats_parties.member=1 AND
data_chats_parties.status >= 1
GROUP BY data_chats_parties.chat
ORDER BY last_message_created DESC
This all works fine, except that total_chat_parties always returns 1, presumably because it's only matching the record of data_chats_parties.member=1. How would I fetch the party record specific to this user but at the same time, fetch the total number of parties for this chat?

You should use a correlated query :
SELECT data_chats.id AS chat,
(SELECT COUNT(data_chats_parties.id) FROM data_chats_parties
WHERE data_chats_parties.chat = data_chats.id) AS total_parties,
data_chats_messages.created AS last_message_created,
data_chats_messages.author AS last_message_author,
data_chats_messages.author_type AS last_message_author_type,
data_chats_messages.message AS last_message
FROM data_chats
LEFT JOIN data_chats_messages
ON data_chats_messages.chat = data_chats.id
AND data_chats_messages.active = 1
AND data_chats_parties.member = 1
AND data_chats_parties.status >= 1
WHERE data_chats.active = 1
ORDER BY last_message_created DESC
Another thing is the conditions on the WHERE clause, you can filter the RIGHT table of a LEFT JOIN in the WHERE clause, those condition should only be specified in the ON clause.
You also group by a column from the RIGHT table - this is not suggested at all! Either use an inner join, or group by another field.

you may be able to use a subquery in the select statement to give you the desired count.
(select COUNT(data_chats_parties.id) from data_chats_parties where data_chats_parties.chat=data_chats.id) AS total_parties,
Also you can then remove the line
LEFT JOIN data_chats_parties ON data_chats_parties.chat=data_chats.id
Hopefully I've typed that all correctly =)

Related

Doctrine left join Many to one

I have a table client and table client_evolution, with OneToMany relationship. In client_evolution table I have 3 entries for current client. Pulling data with Left Join I get always the first entry with smallest id from client_evolution , what I need is the latest row. I have tried to do subselect but it didn't work.
->addSelect('(
SELECT ce.id
FROM ClientEvolution cev
WHERE cev.client = c.id
GROUP BY cev.client
ORDER BY cev.id DESC) AS cevol')
I need more than one field from this table.
Any help please.
The GROUP BY is why you are getting only one entry per client also your SELECT is missing the client table. This will give you all the fields between the two tables in order of client ids. To only get the latest row for each client id, please look at MySQL Group By to display latest result
->addSelect('(
SELECT *
FROM ClientEvolution cev, Client c
WHERE cev.client = c.id
ORDER BY cev.client, cev.id DESC)')

MySQL efficient SELECT query

I'm not a database professional, but currently working on one query (PHP->MySQL):
I have 3 tables:
'Items': id, name, link
'ItemsToUsers': id, item_id, user_id
'Users': id, email
Each 'Item' availability is submitted to regular changes which I check on fly by some algorithm.
My goal is to
1) SELECT all Items and check on fly if they are available
2) If Item is available, notify users who are monitoring it by email. For that I need to SELECT users from 'ItemsToUsers' and then get their emails from Users table.
I know how to do it in a straightforward way, but I feel that I will fall into running to many queries. (individual SELECT for every user...)
Is there a way to do it more efficiently: in one query or by changing the algorithm?
Thank you for your time!
There's not enough information to determine how an item is available. This severely impedes the ability to query item 2.
That said, let's suppose we add a "available" column to the Items table that is a tinyint of 0 for not available, 1 for available.
A query, then, which would get all email addresses for persons watching items that are available is:
SELECT u.email FROM Users AS u JOIN ItemsToUsers AS k ON k.user_id = u.id JOIN Items AS i on i.id = k.item_id WHERE i.available = 1;
Alternatively, you could use a subquery and IN.
Let's suppose you have a different table called Availability with the columns id, item_id and available, which again is a tinyint containing a 1 for available and 0 for not available.
SELECT u.email FROM Users AS u JOIN ItemsToUsers AS k ON k.user_id = u.id WHERE k.item_id IN (SELECT a.item_id FROM Availability AS a WHERE a.available = 1);
Again, without an idea of how you are getting a list of available products, it is impossible to optimize your queries for retrieving a list of email addresses.
Your steps allude to doing this in n+1 queries (where n = number of entries in the Items table):
SELECT * FROM Items; -- This is the +1 part
While iterating over that result set, you intend to determine if it's available and, if it is, to notify users who are watching it. Assuming you have a given item id and you want to select all users' email if that product id is active, then you could do this:
SELECT email FROM Users u
INNER JOIN ItemsToUsers iu ON iu.user_id = u.id
INNER JOIN Items i ON iu.item_id = i.id
WHERE i.id = {your item id}
You would be running this query for every item in your table. This is the n part.
In general you could instead generate a list of emails for all users who are watching all products that are active, after you have already determined which ones should be active:
SELECT DISTINCT email FROM Users u
INNER JOIN ItemsToUsers iu ON iu.user_id = u.id
INNER JOIN Items i ON iu.item_id = i.id
WHERE i.is_active = 1
This will get the job done in a total of 2 queries, regardless of how many users or items you have. As a bonus, this one can give you distinct emails, whereas the first solution would still need application-level code to remove duplicates returned by the multiple queries.
SELECT Items.id, Items.name, Items.link FROM Items
INNER JOIN ItemsToUsers ON ItemsToUsers.item_id = Items.items.id
INNER JOIN Users ON ItemsToUsers.user_id = Users.id ;

User specified content sharing like G+

I'm building a site that requires sharing with either group(s) or individual user(s). I know for a fact that google does not use mysql, but i was wondering how i could replicate such feature on my site. On g+, one can:
Share a post with the "public" (everyone can see it).
Share a post with "all circles" (everyone in your circles can see it).
Share a post with both circles and individual users. E.g. post = "my first post" and is shared with family,friends, user 1(Joey tribbiani), user 2 (Ross geller) etc.
Conditions:
If a post is shared with a circle and a new user is added to the circle, then (s)he should be able to see all the previous posts shared with that circle.
If a user is removed from a circle. (s)he cannot see posts shared with that circle except posts (s)he has commented on.
Currently my database tables look like this.
Circle_category
Cat_id
Cat_name
user_id
Posts
post_id
user_id
post
is_public
all_circle
Post_to_circle
entry_id
post_id
cat_id
Post_to_user
entry_id
post_id
user_id
Post a user in family circle(which is in Circle_category with cat_id of 1 ) can see
They can see posts that are public.
They can see posts shared with all circles.
They can see posts shared with family circle.
They can see posts shared with them (Individual user).
SQL
SELECT p.* FROM posts p
JOIN Post_to_circle pc
ON p.post_id = pc.post_id
JOIN Post_to_user pu
ON p.post_id = pu.post_id
WHERE p.is_public = 1
OR all_circle = 1
OR pc.cat_id = $cat_id
OR pu.user_id = $user_id
Quetions:
Firstly, I've been able to get posts from case 1(see all public post), case 2 (Posts shared with all circles) but the other 2 cases do not work. I thought about it and saw that the main problem is that i specified the where clause to get posts where p.is_public = 1 which means it neglets rows where p.is_public = 0. How do i update the query so it shows posts covering all four cases and also covers the conditions we talked about at the beginning.
Secondly, is there a better way to structure my tables? i'm not sure i'm doing it the right way.
From a quick read trough, all i can say is:
you are using a join statement instead of a left join statement.
using join means:
keep all rows from the table used in from-clause that validate true for the condition specified in that join clause.
since you are using 2 statements, the first join throws away all the records that dont have the needed join, the second join throws away all the records that dont have the needed join in the second one, but it only uses records that matched the first join.
you should use left join instead. this keeps all rows from the first table. all rows that didnt have a match, get the values NULL for the columns specified in the joined table(s)
simple example:
users table:
user_id
name
user_posts
post_id
user_id
content
created
related queries:
select *
from users u
JOIN user_posts up on up.user_id = u.user_id and up.created > date_sub(curdate(), interval 1 day)
this will use all users and make match with each post that was created less then a day ago by that user.
if the user didnt have a post in the last day, he will NOT be in the resultset.
change this example to
select *
from users u
LEFT JOIN user_posts up on up.user_id = u.user_id and up.created > date_sub(curdate(), interval 1 day)
this will use all users and make a match with each post that was created less then a day ago by that user
if the user hasn't posted in the last day, he will STILL be in the resultset, but all the columns from the posts table will be NULL
the where filters all the rows you have left after the joins. (mysql will use where clauses before joining, if they can speed up the query).
altering your query:
make sure the clauses in where statement are wrapped between () for all the different cases. ALSO this is NOT the complete answer, as there is info missing (example user tables, circle relation tables, friend relations)
also the all_circles option confuses me, so it's missing from the query, but this should get you on the right track
SELECT p.* FROM posts p
left JOIN Post_to_circle pc
ON p.post_id = pc.post_id and /* define statement for valid circles for user you're trying to get the posts for */
left JOIN Post_to_user pu
ON p.post_id = pu.post_id and /* define statement for valid friends for user you're trying to get the posts for */
WHERE
/* 1 day old */
p.created > date_sub(curdate(), interval 1 day)
AND (
/* is public */
p.is_public = 1 OR
/* or to friends */
pu.id is not null OR
/* or to circles */
pc.id is not null
)
Also, i'm suspecting you'll need 2 subqueries, which is not the best thing to do, and my advise would be to find all correct ids for the friends, and all ids for the valid circles and then using an IN clause in each join statement (part thats in comment)

SQL Joins across multiple tables

I am building an online survey system for which I wish to produce statistics. I want query based on the gender of the user. I have the following tables:
survey_question_options
survey_answer
users
I have constructed the following query so that it brings back a null response where there are no answers to the question:
SELECT COUNT(sa.option_id) AS answer , so.option_label
FROM survey_answer sa
RIGHT JOIN survey_question_options so
ON sa.option_id = so.option_id AND
sa.record_date>='2011-09-01' AND
sa.record_date<='2012-08-01'
LEFT JOIN users u
ON (sa.uid = u.uid AND u.gender='F')
WHERE so.question_id=24
GROUP BY so.option_label
ORDER BY so.option_id ASC
My query returns the following results set:
0 Red
1 Yellow
0 Blue
0 Green
However, the gender condition in the LEFT JOIN appears to be ignored in the query. When I change the gender to 'M' the same result is returned. However, the expected result would be 0 for everything.
I am not sure where I am going wrong. Please help.
Thanks in advance.
Well, you are doing a COUNT on a column from the main table, so the gender condition on the LEFT JOIN won't affect the result. You should do the COUNT on a column from the users table. I'm not sure if this is what you want, but you should try:
SELECT COUNT(u.uid) AS answer , so.option_label
FROM survey_answer sa
RIGHT JOIN survey_question_options so
ON sa.option_id = so.option_id AND
sa.record_date>='2011-09-01' AND
sa.record_date<='2012-08-01'
LEFT JOIN users u
ON (sa.uid = u.uid AND u.gender='M')
WHERE so.question_id=24
GROUP BY so.option_label
ORDER BY so.option_id ASC
The left join to the users table is evaluated after the join to the answer table - so although the user record is not returned if the user is the wrong gender, the answer record will be returned (regardless of the user's gender). Try:
SELECT COUNT(sa.option_id) AS answer , so.option_label
FROM (select a.option_id
from survey_answer a
JOIN users u ON a.uid = u.uid AND u.gender='F'
where a.record_date>='2011-09-01' AND
a.record_date<='2012-08-01') sa
RIGHT JOIN survey_question_options so
ON sa.option_id = so.option_id
WHERE so.question_id=24
GROUP BY so.option_label
ORDER BY so.option_id ASC
You're putting your condition in the wrong block. Since you're performing a LEFT JOIN, (which is a left-bound outer join) everything in the left table (the main table) is selected, together with the data from the joined table, where applicable. What you want is to add the data from all users and then restrict the full output of the query. What you've actually done is add the user data from only the female users and then displayed all data.
Sounds technical, but all you have to do is move the AND u.gender='F' into the main WHERE clause instead the ON clause. That will cause SQL to only select the rows for female users after the JOIN has taken place.

A MySQL query, some normalized tables, issues w/ counts from the normalized table rows (from PHP)

Update: Added Schema to the bottom...
I have a table of contracts: tbl_contract
And a table of users associated with the contract: tbl_contract2user
There can be any number of entries in tbl_contract2user, in which an entry existing means that the relationship exists (along with a pending column where 1 = pending and 0 = approved).
My goal here is to select all contracts where there is 1 (or more) active users within the time frame specified (see below).
The problem I'm having is the ability to sort out these contracts properly. The date range is working fine... For some reason I'm having trouble understanding when the number of users is 1 or more...(vs. 0) and yes - I'll be working with that data set (After the query).
See below for the start of the query...
$result = mysql_query("SELECT tbl_contract.id
FROM tbl_contract
LEFT JOIN tbl_contract2user ON tbl_contract.id = tbl_contract2user.contractID
WHERE tbl_contract2user.pending = 0
AND tbl_contract.startDate <= {$billing['start_time']}
AND tbl_contract.endDate >= {$billing['end_time']}");
Schema:
tbl_contract: id, startDate, endDate, value, name, dateCreated
tbl_contract2user: id, contractID, userID, pending
What is the actual problem?
Do you get all records instead of only those with a related user? If yes, turn the LEFT JOIN into a INNER JOIN and all contracts without a relation are gone...
The real issue is that if I have 6 users in one contract, I get 6 rows
returned instead of ONE row for that contract
This is exactly what a JOIN does. It takes all records from the left side and joins them with the records on the right side by using a specific condition. If you only want to know how many users a contract has, you can you a GROUP BY clause and a COUNT(*):
SELECT tbl_contract.id, COUNT(*) AS userCount
FROM tbl_contract
LEFT JOIN tbl_contract2user ON tbl_contract.id = tbl_contract2user.contractID
WHERE tbl_contract2user.pending = 0
AND tbl_contract.startDate <= {$billing['start_time']}
AND tbl_contract.endDate >= {$billing['end_time']}
GROUP BY tbl_contract.id
If you need more information about the user, you really need all these 6 rows...

Categories