Background, I have a cron job that queries keywords associated with posts, if that keyword is found, a user is emailed about the post.
The cron job runs every minute so once a user has been emailed that notification should not happen again.
The following query runs:
SELECT DISTINCT p.id AS post_id, u.display_name, u.id AS user_id, u.email, k.keyword, k.id AS keyword_id
FROM posts p, user_subscriptions us, keywords_users ku, keywords k, users u
LEFT JOIN keyword_subscription_sent kss ON kss.user_id = u.id
LEFT JOIN keywords_posts kp ON kss.post_id = kp.post_id
WHERE us.keywords = 1
AND ku.user_id = u.id
AND ku.keyword_id = k.id
AND kp.keyword_id = k.id
#AND kss.user_id IS NULL AND kss.post_id IS NULL
LIMIT 0, 200
The query works as expected when the table keywords_posts kp is populated.
If the table keywords_posts kp is empty the query will never return anything.
I attempted to fix this by adding to the WHERE clause:
AND kss.user_id IS NULL AND kss.post_id IS NULL
But adding this to the WHERE clause will cause the query to never return anything at all at any time.
The desired result is:
The query will return all rows except listed in the keywords_posts kp table.
Suggestions:
Relocate the conditions that require a column to be non-NULL from the WHERE clause up to the appropriate ON clause of the outer join.
Don't mix the old-school comma syntax (for the join operation) with the JOIN keyword syntax. Ditch the comma syntax and just use JOIN. And move join conditions to the ON clause.
Moving the condition to the ON clause of the outer join will allow NULL values to be returned.
If we do it the WHERE clause, note that the condition
WHERE ( col = some_non_null_val AND col IS NULL )
is guaranteed to never evaluate to TRUE. (The condition is checked for each individual row.) Only rows where the condition evaluate to TRUE will be returned. Likely you meant to use OR in place of AND.
But we can avoid that OR if we just relocate the condition
col = some_non_null_val
to the ON clause of the outer join.
I hesitate to provide an example query as a demonstration. The original query is producing a semi-Cartesian product; for example what appears to be a cross join between users and user_subscriptions... the only criteria for matching to user_subscriptions is keywords=1. So I'm not understanding the specification.
OP reports that query is returning a correct result, but I'm suspicious that the query doesn't actually satisfy the intended specification. I suspect that the matching between users and user_subscriptions should include some additional criteria.
Without a more precise specification, including sample data and an example of expected output, I cannot in good conscience provide example SQL for OP use-case.
i think kp_keyword_id=k.id from your WHERE condition cause the query to return nothing where kp is empty,try to put that in JOIN instead so it become like this
SELECT DISTINCT p.id AS post_id, u.display_name, u.id AS user_id, u.email, k.keyword, k.id AS keyword_id
FROM posts p, user_subscriptions us, keywords_users ku, keywords k, users u
LEFT JOIN keyword_subscription_sent kss ON kss.user_id = u.id
LEFT JOIN keywords_posts kp ON kss.post_id = kp.post_id AND kp.keyword_id = k.id
WHERE us.keywords = 1
AND ku.user_id = u.id
AND ku.keyword_id = k.id
LIMIT 0, 200
Hello vveryone and thanks for you input.
The following worked:
SELECT * FROM (SELECT u.id as user_id, u.email, u.display_name, k.id as keyword_id, k.keyword, p.id AS post_id, kp.post_id AS kp_post_id, kp.keyword_id AS kp_keyword_id,
ku.user_id AS ku_user_id, ku.keyword_id AS ku_keyword_id
FROM users u, keywords k, posts p, keywords_posts kp, user_subscriptions us, keywords_users ku
WHERE kp.post_id = p.id AND us.keywords = 1 AND kp.keyword_id = k.id AND ku.keyword_id = k.id AND u.id = ku.user_id
GROUP BY kp.post_id, u.id) AS ukp LEFT JOIN keyword_subscription_sent kss ON ukp.user_id = kss.user_id AND kss.post_id = ukp.post_id WHERE kss.post_id IS NULL
Related
I am trying to write a query. I got it work half way, but I am having problems with the LEFT JOIN.
I have three tables:
user
user_preferences
user_subscription_plan
User will always have one user_preference, but it can have many or no entries in the user_subscription_plan
If the user has no entry in the user_subscription_plan, or if he has only one then my sql works. If I have more then one, then I have issue. In the case of two entries, how can I make it to return the last one entered? I tried playing with ORDER statement, but it does not work as expected. Somehow I get empty rows.
Here is my query:
SELECT u.id AS GYM_USER_ID, subscription_plan.id AS subscriptionId, up.onboarding_completed AS CompletedOnboarding,
(CASE
WHEN ((up.onboarding_completed = 1)
AND (ISNULL(subscription_plan.id)))
THEN 'freemiun'
WHEN (ISNULL(up.onboarding_completed)
AND (ISNULL(subscription_plan.id)))
THEN 'not_paying'
END) AS subscription_status
FROM user AS u
INNER JOIN user_preferences up ON up.user_id = u.id
LEFT JOIN (
SELECT * FROM user_subscription_plan AS usp ORDER BY usp.id DESC LIMIT 1
) AS subscription_plan ON subscription_plan.user_id = u.id
GROUP BY u.id;
If I run it as it is, then subscription_plan.id AS subscriptionId is always empty.
If I remove the LIMIT clause, then its not empty, but I am still getting the first entry, which is wrong in my case
I have more CASE's to cover, but I can't process until I solve this problem.
Please try to use "max(usp.id)" that "group by subscription_plan.user_id" instead of limit 1.
If you limit 1 in the subquery, the subquery's result will always return only 1 record (if the table has data).
So the above query can be rewritten like this.
Sorry, I didn't test, because I don't have data, but please try, hope this can help.
SELECT
u.id AS GYM_USER_ID,
subscription_plan.id AS subscriptionId,
up.onboarding_completed AS CompletedOnboarding,
(CASE
WHEN
((up.onboarding_completed = 1)
AND (ISNULL(subscription_plan.id)))
THEN
'freemiun'
WHEN
(ISNULL(up.onboarding_completed)
AND (ISNULL(subscription_plan.id)))
THEN
'not_paying'
END) AS subscription_status
FROM
user AS u
INNER JOIN
user_preferences up ON up.user_id = u.id
LEFT JOIN
(SELECT
usp.user_id, MAX(usp.id)AS id
FROM
user_subscription_plan AS usp
GROUP BY usp.user_id) AS subscription_plan ON subscription_plan.user_id = u.id;
$query = "SELECT a.comment_user_id as main_id, a.comment_date as timestamp, a.comment_content as content, a.comment_link_id as link_unique, a.comment_id as status, NULL as url, b.user_login as ulogin, b.user_avatar_source as uavatar, c.link_title as ltitle, NULL as desc FROM kliqqi_comments as a WHERE comment_user_id IN ('$following2')
LEFT JOIN kliqqi_users as b ON a.comment_user_id = b.user_id
LEFT JOIN kliqqi_links as c ON a.comment_user_id = c.link_author
ORDER BY timestamp DESC LIMIT 10";
$result = mysqli_query($db_conx, $query);
$row = $result->fetch_array(MYSQLI_ASSOC);
Can anybody tell me what's wrong with the code? It is always returning this error:
Fatal error: Call to a member function fetch_assoc() on boolean
Boolean means this query is not getting executed due to some error in $query variable which I am unable to figure out.
$following is an array. kliqqi_comments alias a, kliqqi_users alias b, kliqqi_links alias c. I am storing all the other fields as alias too. There is no typo or any other silly mistake. I've checked it thoroughly.
UPDATE:
I'm updating this thread because my query actually has many parts and many users may find it helpful.
$query = "SELECT a.comment_user_id as main_id, a.comment_date as timestamp2, a.comment_content as content, a.comment_link_id as link_unique, a.comment_id as status, b.user_login as ulogin, b.user_avatar_source as uavatar, c.link_title as ltitle FROM kliqqi_comments a
LEFT JOIN kliqqi_users b ON a.comment_user_id = b.user_id
LEFT JOIN kliqqi_links c ON a.comment_link_id = c.link_id
WHERE comment_user_id IN ('$following')
UNION ALL
SELECT d.link_author as main_id, d.link_date as timestamp2, d.link_status as content, d.link_id as link_unique, NULL as status, e.user_login as ulogin, e.user_avatar_source as uavatar, d.link_title as ltitle FROM kliqqi_links d
LEFT JOIN kliqqi_users e ON d.link_author = e.user_id
WHERE link_author IN ('$following') AND link_status IN ('new','published')
UNION ALL
SELECT f.vote_user_id as main_id, f.vote_date as timestamp2, f.vote_value as content, f.vote_link_id as link_unique, NULL as status, g.user_login as ulogin, g.user_avatar_source as uavatar, h.link_title as ltitle FROM kliqqi_votes f
LEFT JOIN kliqqi_users g ON f.vote_user_id = g.user_id
LEFT JOIN kliqqi_links h ON f.vote_link_id = h.link_id
WHERE vote_user_id IN ('$following')
ORDER BY timestamp2 DESC LIMIT 30";
What does it do?
I've 3 tables: kliqqi_links, kliqqi_users, kliqqi_votes
UNION ALL
All of them have a timestamp field.
I wanted to fetch contents from these 3 tables combined in decreasing order of timestamp. And to do so, I used UNION ALL (UNION can also be used here but UNION has to run duplicate checks so it's better to avoid it if you can.). But UNION ALL works only when all of the tables have same number of fields. So, I created NULL elements for equating the numbers.
It is to be noted that there is no restriction of datatype for uniting respective fields. But since I had to use timestamp for sequence, I kept them together.
Alias
Since all the respective fields have different names in different tables, I used alias to avoid confusion. Without alias, results are stored in fields mentioned in first SELECT statement which would be a mess.
Multiple LEFT JOIN
Now, I wanted to grab some data from other tables for each SELECT query.
e.g. for kliqqi_comments (first SELECT statement), I wanted to grab user data for the person who made the comment from kliqqi_users plus I wanted to fetch the link where this comment was made from kliqqi_links table. So, I used left join with kliqqi_comments query where comment_user_id from kliqqi_comments equals to user_id from kliqqi_users and comment_link_id from kliqqi_comments equals link_id from kliqqi_links.
Notice that I managed to equate fields in all 3 statements for UNION ALL.
WHERE IN
$following is comma separated array to ensure that it returns result from the people user is following.
ORDER BY DESC, LIMIT
To order by timestamp and LIMIT output result.
That's it.
The where clauses should come after the join clauses, not before them. Additionally, desc and timestamp are reserved words. If you absolutely must use them as a column aliases, you need to escape them:
SELECT a.comment_user_id as main_id,
a.comment_date as `timestamp`, -- Notice the escaping
a.comment_content as content,
a.comment_link_id as link_unique,
a.comment_id as status,
NULL as url,
b.user_login as ulogin,
b.user_avatar_source as uavatar,
c.link_title as ltitle,
NULL as `desc` -- Notice the escaping
FROM kliqqi_comments as a
LEFT JOIN kliqqi_users as b ON a.comment_user_id = b.user_id
LEFT JOIN kliqqi_links as c ON a.comment_user_id = c.link_author
WHERE comment_user_id IN ('$following2') -- Where clause after the joins
ORDER BY `timestamp` DESC LIMIT 10";
SELECT *
FROM notifications
INNER JOIN COMMENT
ON COMMENT.id = notifications.source_id
WHERE idblog IN (SELECT blogs_id
FROM blogs
WHERE STATUS = "active")
INNER JOIN reportmsg
ON reportmsg.msgid = notifications.source_id
WHERE uid =: uid
ORDER BY notificationid DESC
LIMIT 20;
Here I am INNER JOINing notifications with comment and reportmsg; then filtering content with WHERE.
But my problem is that for the first INNER JOIN [i.e, with comment], before joining notifications with comment, I want to match notifications.idblog with blogs.blogs_id and SELECT only those rows where blogs.status = "active".
For better understanding of the code above:
Here, for INNER JOIN, with comment I want to SELECT only those rows in notifications whose idblog matches blogs.blogs_id and has status = "active".
The second INNER JOIN with reportmsg needs not to be altered. I.e, it only filters through uid.
As you can see from the image below, you can just need to merge other tables to notifications table using LEFT JOIN like that:
SELECT n.notificationid, n.uid, n.idblog, n.source_id,
b.blogs_id, b.status,
c.id,
r.msgid
-- ... and the other columns you want
FROM notifications n
LEFT JOIN blogs b ON b.blogs_id = n.idblog AND b.STATUS = "active" AND n.uid =: uid
LEFT JOIN comment c ON c.id = n.source_id
LEFT JOIN reportmsg r ON r.msgid = n.source_id
ORDER BY n.notificationid DESC
LIMIT 20;
There's no need/reason to filter before the second join because you only use inner joins and then the order of joins and WHERE-conditions don't matter:
SELECT n.*, c.*, r.*
FROM notifications AS n
JOIN COMMENT as c
ON n.source_id = c.id
LEFT JOIN blogs as b
ON n.idblogs = b.blogs_id
AND B.STATUS = 'active'
JOIN reportmsg AS R
ON n.source_id = r.msgid
WHERE uid =: uid
ORDER BY notificationid DESC
LIMIT 20
You can switch the order of joins, you can move B.STATUS = 'active' into the join-condition, but all queries will return the same result. (After the edit it's a LEFT JOIN, of course now the result differs)
And of course you shouldn't use *, better list only the columns you actually need.
if query optimizer does its work, it does not matter where you put filtering statement in INNER JOIN case but in the LEFT JOIN it has effects. Putting filtering statement in LEFT JOIN conditions cause table filtered at first and joined after while putting filtering statement in WHERE clause will filter results of join. Hence, if you want to use LEFT JOIN your query must look like:
SELECT nt.*
FROM notifications nt
LEFT JOIN Blogs bg on nt.blogs_id = bg.blogs_id and bg.STATUS = "active"
LEFT JOIN COMMENT cm ON cm.id = nt.source_id
LEFT JOIN reportmsg rm ON rm.msgid = nt.source_id
WHERE uid =: uid
ORDER BY nt.notificationid DESC
LIMIT 20;
It's very unclear what you are after here.. while your table diagram is useful, you should really supply some sample data and an expected result even if it is just a couple of dummy rows for each table.
Queries work row by row, both INNER JOINs are applied to the same notification row and non-matching rows are discarded.
Any filter applies to both JOIN and any returned rows must have a match in BOTH comment and reportmsg.
Perhaps you want two LEFT JOINs that can apply different filters and guessing from the table names perhaps it could look like this:
SELECT *
FROM notifications n
LEFT JOIN blogs b
ON n.blogId = b.blogs_id
LEFT JOIN comment c
ON c.id = n.source_id
AND b.status = "Active"
LEFT JOIN reportmsg rm
ON rm.msgid = n.source_id
WHERE n.uid =: uid
AND (c.id IS NOT NULL OR rm.msgid IS NOT NULL)
ORDER BY n.notificationid DESC
LIMIT 20
You also should work on your naming convention:
notifications, comment -> pick either plural or singular table names
notifications.notificationid, comment.id -> pick adding table name to id
notificationid, source_id -> pick underscore or no separation
idblog, notificationid -> pick prepending or appending id
Currently you pretty much have to look up every id field every time you want to use one.
You should change your query to this:
SELECT *
FROM notifications
INNER JOIN comment ON comment.id = notifications.source_id
INNER JOIN reportmsg ON reportmsg.msgid=notifications.source_id
LEFT JOIN blogs ON notifications.idblog = blogs.blogs_id
WHERE blogs.status = 'active'
ORDER BY notificationid DESC
LIMIT 20;
I will try to explain things as much as I can.
I have following query to fetch records from different tables.
SELECT
p.p_name,
p.id,
cat.cat_name,
p.property_type,
p.p_type,
p.address,
c.client_name,
p.price,
GROUP_CONCAT(pr.price) AS c_price,
pd.land_area,
pd.land_area_rp,
p.tagline,
p.map_location,
r.id,
p.status,
co.country_name,
p.`show`,
u.name,
p.created_date,
p.updated_dt,
o.type_id,
p.furnished,
p.expiry_date
FROM
property p
LEFT OUTER JOIN region AS r
ON p.district_id = r.id
LEFT OUTER JOIN country AS co
ON p.country_id = co.country_id
LEFT OUTER JOIN property_category AS cat
ON p.cat_id = cat.id
LEFT OUTER JOIN property_area_details AS pd
ON p.id = pd.property_id
LEFT OUTER JOIN sc_clients AS c
ON p.client_id = c.client_id
LEFT OUTER JOIN admin AS u
ON p.adminid = u.id
LEFT OUTER JOIN sc_property_orientation_type AS o
ON p.orientation_type = o.type_id
LEFT OUTER JOIN property_amenities_details AS pad
ON p.id = pad.property_id
LEFT OUTER JOIN sc_commercial_property_price AS pr
ON p.id = pr.property_id
WHERE p.id > 0
AND (
p.created_date > DATE_SUB(NOW(), INTERVAL 1 YEAR)
OR p.updated_dt > DATE_SUB(NOW(), INTERVAL 1 YEAR)
)
AND p.p_type = 'sale'
everything works fine if I exclude GROUP_CONCAT(pr.price) AS c_price, from above query. But when I include this it just gives one result. My intention to use group concat above is to fetch comma separated price from table sc_commercial_property_price that matches the property id in this case p.id. If the records for property exist in sc_commercial_property_price then fetch them in comma separated form along with other records. If not it should return blank. What m I doing wrong here?
I will try to explain again if my problem is not clear. Thanks in advance
The GROUP_CONCAT is an aggregation function. When you include it, you are telling SQL that there is an aggregation. Without a GROUP BY, only one row is returns, as in:
select count(*)
from table
The query that you have is acceptable syntax in MySQL but not in any other database. The query does not automatically group by the columns with no functions. Instead, it returns an arbitrary value. You could imagine a function ANY, so you query is:
select any(p.p_name) as p_num, any(p.tagline) as tagline, . . .
To fix this, put all your current variables in a group by clause:
GROUP BY
p.p_name,
p.id,
cat.cat_name,
p.property_type,
p.p_type,
p.address,
c.client_name,
p.price,
pd.land_area,
pd.land_area_rp,
p.tagline,
p.map_location,
r.id,
p.status,
co.country_name,
p.`show`,
u.name,
p.created_date,
p.updated_dt,
o.type_id,
p.furnished,
p.expiry_date
Most people who write SQL think it is good form to include all the group by variables in the group by clause, even though MySQL does not necessarily require this.
Add GROUP BY clause enumerating whatever you intend to have separate rows for. What happens now is that it picks some value for each result column and group_concats every pr.price.
I have a litte problem with a mysql query.
I use 5 tables:
user_has_data (uid, dataid); users (uid, uname); user_in_group(uid, groupid, data); groups(groupid, data, packageid); packages(packageid, name)
all ids are PK. I want to build a sql query that finds a user, which belongs to a specified dataid, by its uname and checks if the user is in a group (relation in table user_in_group) belonging to a specified package (a group is assigned to one package). if so data from users, package and group should be fetched, otherwise only the user data should be fetched. Therefore I use left joins, so I can also get the users with no group:
SELECT `uac`.`uid`, `u`.`uid`, `uig`.`groupid`, `ag`.`packageid`
FROM `user_has_data` AS `uac`
INNER JOIN `users` AS `u` ON u.uid = uac.uid
LEFT JOIN `user_in_group` AS `uig` ON uig.uid = uac.uid
LEFT JOIN `groups` AS `ag` ON (ag.groupid = uig.groupid) AND (ag.packageid = 2)
WHERE (uac.dataid = '3') AND (u.uname LIKE 'test%')
GROUP BY `u`.`uid`
Unfortunately I get wrong results: I get groups that have a different packageid than stated in the join, if the user has another group assigned to him with a different packageid.
probably this is because the first left join has no restrictions to packageid and the second is a left join and so it has no restrictions on the result (packageid is NULL for all results, but should have values). If I change the second left join to a ordinary join, the group problem would be fixed but the query cant find users without group any more.
Any ideas how to fix this or even possible?
thanks in advance!
Are you saying that you are actually seeing the value ag.packageid = 2 in your query results?
If not, I think you might try something like:
SELECT `uac`.`uid`, `u`.`uid`, `g`.`groupid`, `g`.`packageid`
FROM `user_has_data` AS `uac`
INNER JOIN `users` AS `u` ON u.uid = uac.uid
LEFT JOIN (`user_in_group` AS `uig`
INNER JOIN `groups` AS `ag` ON (ag.groupid = uig.groupid) AND (ag.packageid = 2) )
AS `g` ON uac.uid = g.uid
WHERE (uac.dataid = '3') AND (u.uname LIKE 'test%')
GROUP BY `u`.`uid`
Because you are limiting your search to a specific group packageid of '2', why not just make both of your LEFT JOIN INNER JOINS and then throw in ag.packageid = 2 in your WHERE clause?
SELECT `uac`.`uid`, `u`.`uid`, `uig`.`groupid`, `ag`.`packageid`
FROM `user_has_data` AS `uac`
INNER JOIN `users` AS `u` ON u.uid = uac.uid
LEFT OUTER JOIN `user_in_group` AS `uig` ON uig.uid = uac.uid
LEFT OUTER JOIN `groups` AS `ag` ON ag.groupid = uig.groupid
WHERE (uac.dataid = '3') AND (u.uname LIKE 'test%')
AND (ag.packageid = 2 OR uig.uid IS NULL)
GROUP BY `u`.`uid`
I know LEFT JOIN and LEFT OUTER JOIN mean the same thing, but I like to be explicit. With the condition in your join, I bet you were getting groups with different packages, but weren't getting the packages?