Selecting the last record and comparing a datetime - mysql

I'm building a discussion board and I want to get a list of unread topics.
A topic should be unread and selected if the created_at datetime for the last post in a topic is greater than the last time the currently logged in user viewed this topic.
http://sqlfiddle.com/#!2/4e2e99/1
If you delete all of the user view inserts ALL of the topics should be listed.
I have three tables:
topics
id
user_id
created_at
topic_posts
id
topic_id
created_at
topic_user_views
id
topic_id
created_at
My query so far (but it doesn't work):
SELECT DISTINCT `topics`.`id`, `topics`.`name`
FROM `topics`
INNER JOIN `topic_user_views`
ON `topic_user_views`.`topic_id` = `topics`.`id`
INNER JOIN `topic_posts`
ON `topic_posts`.`topic_id` = `topics`.`id`
WHERE `topic_posts`.`created_at` > `topic_user_views`.`created_at`
AND `topic_user_views`.`user_id` = 1
ORDER BY `id` DESC
I don't know how to compare a topic's last post's created_at column post to the last time this user has viewed the topic.

here is one way of doing it. we will use exists to test if the topic was viewed before the last topic post was created. see the sql fiddle - http://sqlfiddle.com/#!2/4e2e99/11
select t.id as topic_id, t.name as topic_name
from topics t
where not exists(
select tuv.topic_id, max(tuv.created_at) as last_view, max(tp.created_at) as last_post
from topic_user_views tuv
inner join topic_posts as tp
on tuv.topic_id=tp.topic_id and tuv.created_at > tp.created_at
group by topic_id
having t.id=topic_id)
ORDER BY id DESC

With this example, topics also will be selected if user have not seen anything yet:
SELECT DISTINCT
`topics`.`id`,
`topics`.`name`
FROM `topics`
LEFT JOIN `topic_user_views` AS tuv
ON `tuv`.`topic_id` = `topics`.`id`
WHERE (SELECT
1
FROM topic_posts AS tp
WHERE tp.topic_id = `topics`.`id`
AND (tp.created_at > tuv.created_at
OR tuv.created_at IS NULL)
LIMIT 1)IS NOT NULL
ORDER BY `id` DESC;

Here is what I came up with :
SELECT t1.topic_id, t3.account_user_id, t1.created_at last_post_date, t4.created_at as last_seen_date
FROM topic_posts t1
INNER JOIN (SELECT topic_id, MAX(created_at) as created_at
FROM topic_posts
GROUP BY topic_id) t2
USING (topic_id, created_at)
INNER JOIN topic_user_views t3
USING (topic_id)
INNER JOIN (SELECT topic_id, account_user_id, created_at
FROM topic_user_views
INNER JOIN (SELECT topic_id, account_user_id, MAX(created_at) as created_at
FROM topic_user_views
GROUP BY topic_id, account_user_id) _
USING (topic_id, created_at, account_user_id)) t4
ON t1.topic_id = t4.topic_id and t3.account_user_id = t4.account_user_id and t3.created_at = t4.created_at
WHERE t1.created_at > t4.created_at
AND t3.account_user_id = 1;
This idea is to join the date of the last posted message in a topic (t2) and the date of the last seen message in a topic by a user (t4), and then you just have to filter out the results you don't want.
If I'm not mistaken, in the SQLFiddle you provided, there are no topics that have not already been seen by users, so this returns nothing. I slightly modified it (you can see it here) and it seems to work as wanted.
To consider the topics never viewed by a user, I think the best solution is to use the account table with a RIGHT OUTER JOIN. Something similar to :
SELECT t1.topic_id, t3.account_user_id, t1.created_at last_post_date, t4.created_at as last_seen_date
FROM topic_posts t1
INNER JOIN (SELECT topic_id, MAX(created_at) as created_at
FROM topic_posts
GROUP BY topic_id) t2
USING (topic_id, created_at)
INNER JOIN topic_user_views t3
USING (topic_id)
INNER JOIN (SELECT topic_id, account_user_id, created_at
FROM topic_user_views
INNER JOIN (SELECT topic_id, account_user_id, MAX(created_at) as created_at
FROM topic_user_views
GROUP BY topic_id, account_user_id) _
USING (topic_id, created_at, account_user_id)) t4
ON t1.topic_id = t4.topic_id and t3.account_user_id = t4.account_user_id and t3.created_at = t4.created_at
RIGHT OUTER JOIN account
ON account.user_id = t3.account_user_id
WHERE t1.created_at > t4.created_at or t4.account_user_id IS NULL
AND t3.account_user_id = 1;
which I did not test.

Related

SQL referring on one table after join with another table

I got this MySQL query to output latest chat message a user with ID of 18 has had with other users. The query works until I add-in last JOIN, intended to calculate amount of unviewed msgs (unviewed_total). The error I get is 'column not found' for T2.rid1 and T2.rid2. Can't figure out how to refer these columns in a proper way.
SELECT T2.maxDate, T2.ava, T2.uname,chat.user_to,chat.user_from,chat.body,chat.request_id,chat.secondary_rid, T3.unviewed_total FROM
(SELECT T1.user2_id, users.uname, users.ava, max(cdate) maxDate, T1.rid rid1, T1.sec_rid rid2 FROM
(SELECT chat.user_to user2_id, max(msg_time) cdate,request_id rid,secondary_rid sec_rid
FROM chat WHERE chat.user_from=18
GROUP BY chat.user_to
union distinct
(SELECT chat.user_from user2_id, max(msg_time) cdate,request_id rid,secondary_rid sec_rid
FROM chat WHERE chat.user_to=18
GROUP BY chat.user_from)) T1
inner join users on (users.uid = T1.user2_id)
group by T1.user2_id
order by maxDate desc) T2
join chat on (T2.maxDate = chat.msg_time)
join (SELECT COUNT(viewed) unviewed_total FROM chat WHERE viewed='0' AND user_to=18 AND request_id IN (T2.rid1,T2.rid2)) T3
ORDER BY T2.maxDate DESC
The inner query of that join doesn't know about the table T2.
I suggest moving the join to the select statement.
Change
SELECT T2.maxDate, T2.ava, T2.uname,chat.user_to,chat.user_from,chat.body,chat.request_id,chat.secondary_rid, T3.unviewed_total FROM
to
SELECT T2.maxDate, T2.ava, T2.uname,chat.user_to,chat.user_from,chat.body,chat.request_id,chat.secondary_rid, (SELECT COUNT(viewed) unviewed_total FROM chat WHERE viewed='0' AND user_to=18 AND request_id IN (T2.rid1,T2.rid2)) unviewed_total
More about this technique here: https://www.essentialsql.com/get-ready-to-learn-sql-server-20-using-subqueries-in-the-select-statement/

Mysql count and return just one row of data

I need to count the amount of users that have have answered all of those 3 profile_options (so they have at least 3 records in the profile_answers table).
SELECT COUNT(DISTINCT(users.id)) users_count
FROM users
INNER JOIN profile_answers ON profile_answers.user_id = users.id
WHERE profile_answers.profile_option_id IN (37,86,102)
GROUP BY users.id
HAVING COUNT(DISTINCT(profile_answers.id))>=3
The problem is that this query is return a table with rows for each user and how many they answered (in this case always 3). What I need is to return just one row that has the total number of users (so the sum of all rows of this example)
I know how to do it with another subquery but the problem is that I am running into "Mysql::Error: Too high level of nesting for select"
Is there a way to do this without the extra subquery?
SELECT SUM(sum_sub.users_count) FROM (
(SELECT COUNT(DISTINCT(users.id)) users_count
FROM users
INNER JOIN profile_answers ON profile_answers.user_id = users.id
WHERE profile_answers.profile_option_id IN (37,86,102)
GROUP BY users.id
HAVING COUNT(DISTINCT(profile_answers.id))>=3)
) sum_sub
Please give this query a shoot
SELECT COUNT(DISTINCT(u.id)) AS users_count
FROM users AS u
INNER JOIN (
SELECT user_id, COUNT(DISTINCT profile_option_id) AS total
FROM profile_answers
WHERE profile_option_id IN (37,86,102)
GROUP BY users.id
HAVING COUNT(DISTINCT profile_option_id) = 3
) AS a ON a.user_id = u.id
If you have lots of data in your tables, you will get a better/faster performance by using temporary tables like so
CREATE TEMPORARY TABLE a (KEY(user_id)) ENGINE = MEMORY
SELECT user_id, COUNT(DISTINCT profile_option_id) AS total
FROM profile_answers
WHERE profile_option_id IN (37,86,102)
GROUP BY users.id
HAVING COUNT(DISTINCT profile_option_id) = 3;
Then your final query will look like this
SELECT COUNT(DISTINCT(u.id)) as users_count
FROM a
INNER JOIN on a.user_id = u.id
Unless there is a need to join the users table you can go with this
SELECT COUNT(*) AS users_count
FROM (
SELECT user_id, COUNT(DISTINCT profile_option_id) AS total
FROM profile_answers
WHERE profile_option_id IN (37,86,102)
GROUP BY users.id
HAVING COUNT(DISTINCT profile_option_id) = 3
) AS a
Should you need another solution, please consider providing us you EXPLAIN EXTENDED for the query and the table definitions along with a better problem description.
I hope this helps
You can give the queries a name using the AS clause. See the updated query below.
SELECT SUM(sum_sub.users_count) FROM (
(SELECT COUNT(DISTINCT(users.id)) as users_count
FROM users
INNER JOIN profile_answers ON profile_answers.user_id = users.id
WHERE profile_answers.profile_option_id IN (37,86,102)
GROUP BY users.id
HAVING COUNT(DISTINCT(profile_answers.id))>=3)
) as sum_sub
You should not group by on a field not present in select statement.
select id, count(*) from users group by id is fine
select count(id) from users group by id is NOT
Regarding your query I think the link to user table is not necessary. Just using foreign key should be fine.
Try this one:
select count(*) from
(SELECT users_id count(*) as cnt
FROM profile_answers
INNER JOIN users ON profile_answers.user_id = users.id
WHERE profile_answers.profile_option_id IN (37,86,102)
group by users_id
having count(*) >3)

Duplicated rows

SQL Query:
SELECT
T.*,
U.nick AS author_nick,
P.id AS post_id,
P.name AS post_name,
P.author AS post_author_id,
P.date AS post_date,
U2.nick AS post_author
FROM
zero_topics T
LEFT JOIN
zero_posts P
ON
T.id = P.topic_id
LEFT JOIN
zero_players U
ON
T.author = U.uuid
LEFT JOIN
zero_players U2
ON
P.author = U2.uuid
ORDER BY
CASE
WHEN P.date is null THEN T.date
ELSE P.date
END DESC
Output:
Topics:
Posts:
Question: Why i have duplicated topic id 22? i have in mysql two topics (id 22 and 23) and two posts(id 24 and 25). I want to see topic with last post only.
If a join produces multiple results and you want only at most one result, you have to rewrite the join and/or filtering criteria to provide that result. If you want only the latest result of all the results, it's doable and reasonably easy once you use it a few times.
select a.Data, b.Data
from Table1 a
left join Table2 b
on b.JoinValue = a.JoinValue
and b.DateField =(
select Max( DateField )
from Table2
where JoinValue = b.JoinValue );
The correlated subquery pulls out the one date that is the highest (most recent) value of all the joinable candidates. That then becomes the row that takes part in the join -- or, of course, nothing if there are no candidates at all. This is a pattern I use quite a lot.

Struggling with a simple SQL JOIN

I'm pretty bad at SQL and have been having some troubles doing somewhat of a UNIQUE join of two tables. The SQL structure is somewhat abysmal, but I didn't design it.
I have two tables:
users
uid, ufn, uln, ue
Where users id = uid.
and
transactions
uid, unit, address, start_date
Basically in the transactions table, there are multiple entries per uid. What I am looking to do is select users.ufn, users.uln, users.ue, transactions.unit, transactions.address based on ONLY the newest start_date. Meaning I will only get ONE result per uid. Currently I'm getting returns for ALL uid entries in the transactions table.
I've tried doing some JOINS, LEFT JOINS, and things with MAX, but have been largely unsuccessful.
SELECT * FROM users JOIN ( SELECT unit, address, start_date FROM transactions GROUP BY uid) as a ON users.tenant_id = a.tenant_id
Is what I tried among a mix of other things.
Any hint as to the right direction would be much appreciated. Thank you!
This will get you close. The problem will be if 2 transactions have the same start date for the same user. But if you don't have that case this should work fine.
select u.ufn,
u.uln,
u.ue,
t.unit,
t.address
from users u
inner join (
select uid,
max(start_date) as newest_start_date
from transactions
group by uid) x
on u.uid = x.uid
inner join transactions t
on t.start_Date = x.newest_start_date
and t.uid = u.uid
Your example SQL has "tenant_id" but that is not in your tables example?
Are you running this once or 10000 times a day?
Try this:
SELECT users.ufn, users.uln, users.ue, transactions.unit, transactions.address
FROM users join transactions on users.uid = transactions.uid
WHERE transactions.UID, transactions.start_date IN
(SELECT UID, MAX(start_date) FROM TRANSACTIONS GROUP BY UID);
Another option is to use an ANTI JOIN on an inequality
select users.ufn,
users.uln,
users.ue,
t.unit,
t.address
from users
INNER JOIN transactions t
ON t.uid = u.uid
LEFT JOIN transactions t1
ON t.uid = t1.uid
and t.start_date < t1.start_date
WHERE
t1.uid is null
Because of t.start_date < t1.start_date and t1.uid is null only records that don't have another record with a greater start_date will be selected
As with MAX() if two or more transaction have start_dates that tie for a user you will get both
This query could work:
SELECT
u.ufn,
u.uln,
u.ue,
t2.unit,
t2.address
FROM
users AS u
INNER JOIN
(
SELECT
uid
, MAX(start_date) AS start_date
FROM
transactions
WHERE
uid = users.uid
) AS t1
INNER JOIN
transaction AS t2 ON t2.uid = t1.uid AND t2.start_date = t1.start_date
Temporary tables are also an option (may be faster, you have to try):
CREATE TEMPORARY TABLE
last_transactions
AS SELECT
uid
, MAX(start_date) AS start_date
FROM
transactions
GROUP BY
uid
;
SELECT
u.ufn,
u.uln,
u.ue,
t2.unit,
t2.address
FROM
users AS u
INNER JOIN
last_transactions AS t1 ON t1.uid = u.uid
INNER JOIN
transaction AS t2 ON t2.uid = t1.uid AND t2.start_date = t1.start_date
P.S.: You should definitely consider adding a primary key to the transactions table. This would allow for a better join clause between t1 and t2. Also, it would prevent duplicates that may occur when multiple start_date occur for the same user.
P.P.S.: wouldn't adding a last_transaction_start_date column to the user table be wiser?
I am not sure if this would be exactly same syntax in MySQL (most likely it will be), but here is how you would do it in SQL server.
Use a rank() function to determine the latest date.
SELECT x.* , y.*
FROM users as x
JOIN
(SELECT *, RANK() Over (Partition By UID Order By Start_Date DESC) as Rank_ FROM Transactions) as y
ON x.uid = y.uid and y.rank_ = 1
Hope this helps.
Cheers!

MySQL sort within a group with multiple joins

I am attempting to work with the following tables:
users (id, name)
topics (id, name, slug)
posts (id, title, content, topicid, authorid, datetime, slug)
replies (id, authorid, threadid, content, datetime)
I want to create a list of posts ordered by latest reply (or the post date if there are no replies). Each listing should contain: the post title, the number of replies, the date of the post, the date of the latest reply, the name of the author, the name of the person who last replied, etc.
I can get everything except the date and author of the latest reply. The date can be done via MAX(replies.datetime), but I'm not sure how to get the latest author.
My last attempt was trying to use an exclusion join:
select posts.id, posts.title, posts.authorid, posts.topicid, posts.content, posts.datetime, count(replies.id) as replies, r2.datetime, replies.datetime, GREATEST(posts.datetime, coalesce(max(replies.datetime), posts.datetime)) as latesttime, users.name as author, commenters.name as commenter, r2.id
from posts
left join replies on replies.threadid = posts.id
left join users on users.id = posts.authorid
left join users commenters on commenters.id = replies.authorid
left join replies as r2 on replies.id = r2.id and replies.datetime < r2.datetime
where r2.id is null
group by posts.id
order by latesttime DESC, replies.datetime DESC
limit 20;
Unfortunately, this still won't retrieve the latest comment author. Any ideas?
You have to first start with an inner most query on the max ID for a given thread and what it's REPLY ID was joined to the original replies to get ONE instance... From that, you can get the reply author's information. THEN, get original posting information as needed.
SELECT
p.id,
p.title,
p.content,
p.topicid,
p.authorid,
p.datetime as postdate,
p.slug,
postUser.Name as PostAuthor,
coalesce( MaxReplyUser.NumReplies, 0 ) NumReplies,
coalesce( MaxReplyUser.name, '' ) ReplyUser,
coalesce( MaxReplyUser.AuthorID, 0 ) ReplyAuthor,
coalesce( MaxReplyUser.ThreadID, 0 ) ReplyThread,
coalesce( MaxReplyUser.DateTime, '' ) ReplyDateTime,
coalesce( MaxReplyUser.Content, '' ) ReplyContent
from
Posts p
left join
( SELECT
u1.name,
r1.authorid,
r1.threadid,
r1.datetime,
r1.content,
MaxReplies.NumReplies
from
( SELECT
threadid,
COUNT(*) as NumReplies,
MAX( id ) as MaxReplyID
from
replies
group by
threadID ) MaxReplies
INNER JOIN replies r1
ON MaxReplies.MaxReplyID = r1.id
INNER JOIN Users u1
ON r1.AuthorID = u1.ID ) MaxReplyUser
ON p.id = MaxReplyUser.threadID
inner join users postUser
ON p.authorid = postuser.id
order by
if( MaxReplyUser.threadID IS NULL, p.DateTime, MaxReplyUser.DateTime ) DESC
uld you test it?
SELECT posts.id, posts.title, posts.authorid, posts.topicid, posts.content, posts.datetime,
(SELECT name FROM users WHERE id = posts.autorid) "Author",
(SELECT COUNT(*) FROM replies WHERE threadid = posts.id) "Replies",
(SELECT MAX(datetime) FROM replies r WHERE threadid = posts.id) "Lastreplytime",
(SELECT (SELECT name FROM users WHERE id = r.authorid LIMIT 1) FROM replies r WHERE threadid = posts.id ORDER BY datetime DESC LIMIT 1)
FROM posts
ORDER BY Lastreplytime DESC
LIMIT 20;