Eloquent group messages - sql

I´m playing with Laravel´s eloquent and a MYSQL database
I need to display a list of conversations
this is my table structure
messages:
- id
- from_id
- to_id
- message
- read
|----|-----------|---------|-------|------|
| id | message | from_id | to_id | read |
|----|---------------------|--------------|
| 1 | hello | 1 | 2 | 1 |
| 2 | hey | 2 | 1 | 0 |
| 3 | there | 2 | 1 | 0 |
| 4 | hi! | 3 | 1 | 1 |
|----|-----------|---------|-------|------|
users:
- id
- name
|-----------|
| id | name |
|-----------|
| 1 | john |
| 2 | mary |
| 3 | jack |
|-----------|
expected:
if I´m user 1 (it should show me the last message and the count of unread
|----|------|---------|--------|
| id | name | message | unread |
|----|------|---------|--------|
| 3 | mary | there | 2 |
| 4 | jack | hi! | 0 |
|----|------|-------- |--------|
I´m not sure if this is the right table structure for what I want to do but the SQL should be something like
SELECT users.name, messages.message
FROM users, messages
WHERE messages.from_id = 1 OR messages.to_id = 1
GROUP BY ??
ORDER BY messages.id DESC
I´m not sure how to GROUP because if I group by messages.from_id = 1 it will only show messages I sent and not messages I received. please help me thing :)
also how to handle the read status? for sure that read attribute is something like the to_id will set to 1. how to check that in the query as well?

THere's a couple of things I see here.
1. while aggregating your data to count the number of unread emails, you are still showing at least one message which indicates message text. In the expected response:
|----|------|---------|--------|
| id | name | message | unread |
|----|------|---------|--------|
| 3 | mary | there | 2 |
| 4 | jack | hi! | 0 |
|----|------|-------- |--------|
You have two unread messages for mary, however the message column says there, which is only the message of one the two unread emails.
I'm just wondering if that's on purpose.
However, to get you where you need to go,
you have the read column which appears binary in nature: 1 for messages read, 0 for messages not read.
That being said, you can utilize that logic to count the number of unread messages by using the algorithm: count(read) - sum(read).
the second thing is your joins. You have to join the tables together in order to establish names to user_ids. in your case, you are not using a join at all but filtering by field. if you want a list of from users, filtered by the to users, then you need to join the from_id to the user_id so you can get the name.
There you can group by name and at least get id, name, unread. as forth in this example:
SELECT users.name, count(read) - sum(read) as unread
FROM users, messages
WHERE users.id = messages.from_id and messages.to_id = 1 --to me
GROUP BY users.name
ORDER BY messages.from_id ASC
in laravel ORM, try this (I know I"m close, but I'm mobile and haven't been able to test it, so play around):
$users = DB::table('messages')
->select(DB::raw('users.name, count(read) - sum(read) as unread'))
->join('users', 'users.id', '=', 'messages.from_id')
->where('to_id', '=', 1)
->orWhere('from_id', '=', '1')
->groupBy('users.name')
->orderBy('messages.from_id','ASC')
->get();
Try this.
UPDATE 1:
to purposely get the "latest" message with the list:
select u.name, fm.message, count(m.unread) - sum(m.unread) as unread,
case when m.from_id = 1 then 'sent'
when m.to_id = 1 then 'received'
else null
end status
from messages m
join users u on u.id = m.from_id
left join (
Select max(id) as id, from_id, message from messages group by from_id
) fm on m.from_id = fm.from_id
where m.from_id = 1 or m.to_id = 1
group by u.name;
Eloquent:
DB::table('messages')
->select('u.name', 'fm.message', 'count(m.read) - sum(m.read) as unread', 'case when m.from_id = 1 then "sent" when m.to_id = 1 then "received" else null end status')
->join(users u', 'u.id', '=', 'u.from_id')
->leftjoin(DB::raw('Select max(id) as id, from_id, message from messages group by from_id', 'm.from_id', 'fm.from_id'))
->groupBy('users.name')
->orderBy('messages.from_id','ASC')
->get();

Related

Attempt to find total unique occurrences column as well as return the max value in that column

I am attempting to find the number of occurrences of the "message to user" event per user session as well as return the maximum number of sessions per user. So this means that i would like to keep track of how many user sessions have the event "message to user" but would like to eliminate duplicates that occur in the same session if that makes sense?
I am also looking for the total user sessions across all users
I was unsuccessful trying to find these values. My table looks like so:
user_id | event | user_session_id
1 | message to user | 1
1 | message to user | 1
1 | message from user | 1
1 | message to user | 1
1 | message from user | 2
1 | message to user | 2
1 | message to user | 3
2 | message to user | 1
2 | message to user | 1
2 | message from user | 1
2 | message to user | 1
2 | message from user | 2
2 | message to user | 2
My expected output would be something like this:
user_id | event | user_session_id | max_session_by_user | total_sessions
1 | message to user | 1 | 3 | 5
1 | message to user | 2 | 3 | 5
1 | message to user | 3 | 3 | 5
2 | message to user | 1 | 2 | 5
2 | message to user | 2 | 2 | 5
Thank you
EDIT: I added more clarification about what i meant when i am looking for with regards to the event column
First filter for the desired event and eliminate duplicates.
Then add counts using window functions.
SELECT user_id, event, user_session_id,
count(*) OVER (PARTITION BY user_id) AS max_session_by_user,
count(*) OVER () AS total_sessions
FROM (SELECT DISTINCT user_id, event, user_session_id
FROM events_table
WHERE event = 'message to user') AS q;
I'm not sure where the event column comes from. But you seem to want:
select user, user_session_id,
count(*) over (partition by user) as max_sessions
from t
group by user, user_session_id;

Given a user id, get all their channels with recipients and last message preview?

This is to get back a list of group chats like you'd see on any chat app.
The example shows direct messages, but technically 3+ users could join the chat channel. Assume there is no user table.
I want to be able to pass in some_user and "get all channels some_user is participating in, with the channel members that are not some_user (the recipients), and last message sent to the channel for a preview, ordered by last message created_at desc".
channel
---
id(pk)
channel_user
---
channel_id(fk) | user_id
message
---
id(pk) | channel_id(fk) | sender_id | text | created_at
channel
---
1
2
channel_user
---
1 | "Elon"
1 | "Mark"
2 | "Steve"
2 | "Elon"
message
---
3 | 1 | "Elon" | "AI will destroy us all" | timestamp(late)
4 | 1 | "Mark" | "No it won't" | timestamp(later)
5 | 2 | "Steve"| "Sup Elon" | timestamp(latest)
Pass in user "Elon" and get something like:
channel_id | recipient(s) | last_message | last_message_sender | last_message_created_at
---
2 | "Steve" or ["Steve",...] | "Sup Elon" | "Steve" | timestamp(latest)
1 | "Mark" or ["Mark",...] | "No it won't" | "Mark" | timestamp(later)
I think something like the following should get you in the ballpark:
SELECT *
FROM
(
SELECT m.channel_id, m.sender_id, m.text, m.created_at, row_number() over (PARTITION BY m.channel_id ORDER BY created_at desc) as message_rank
FROM
channel_user cu
INNER JOIN message m ON
cu.channel_id = m.channel_id
WHERE
cu.user_id = 'Elon'
AND m.user_id <> 'Elon'
) sub
WHERE sub.message_rank = 1

Limiting subqueries in SQL

I have a situation which is a little hard to describe. I'll try to explain with an example and the result which I want.
I have three tables like so
Employee
| id | Name |
|----+-------|
| 1 | Alice |
| 2 | Bob |
| 3 | Jane |
| 4 | Jack |
Task
| id | employee_id | description |
|----+-------------+---------------------|
| 1 | 1 | Fix bug |
| 2 | 1 | Implement feature |
| 3 | 1 | Deploy master |
| 4 | 2 | Integrate feature |
| 5 | 2 | Fix cosmetic issues |
Status
| id | task_id | time | details | Terminal |
|----+---------+-------+-----------+----------|
| 1 | 1 | 12:00 | Assigned | false |
| 2 | 1 | 12:30 | Started | false |
| 3 | 1 | 13:00 | Completed | true |
| 4 | 2 | 12:10 | Assigned | false |
| 5 | 2 | 14:00 | Started | false |
| 6 | 3 | 12:15 | Assigned | false |
| 7 | 4 | 12:20 | Assigned | false |
| 8 | 5 | 12:25 | Assigned | false |
| 9 | 4 | 12:30 | Started | false |
(I have also put these into a sqlfiddle page at http://sqlfiddle.com/#!9/728c85/1)
The basic idea is that I have some employees and tasks. The tasks can be assigned to employees and as they work on them, they keep adding "status" rows.
Each status entry has a field "terminal" which can either be true or false.
If the last status entry for a task has terminal set to true, then that task is over and there's nothing more to be done on it.
If all tasks assigned to an employee are over, then the employee is considered free.
I need to get a list of free employees. This would basically mean, given an employee, a list of all his or her tasks with statuses. So, something like this for Alice
| Task | Completed |
|-------------------+-----------|
| Fix bug | true |
| Implement feature | false |
| Deploy master | false |
From which I know that she's not free right now since there are 'false' entries in completed.
How would I do this? If my tables are not constructed properly for this kind of query, I'd very much like some advice on that too.
(I titled the question like this since I want to order the statuses of each task per user and them limit them to the last row).
Update
It was suggested to me that the status field should really go inside the tasks table and the Status table should simple be a log table.
I would go for the idea to have the status in the tasks table. (Please see my comment on your request on this.) However, here are two queries to select free employees:
If tasks cannot be re-opened, it is simple: Get all incompleted tasks by checking whether a record with terminal = true exists. Free employees are all that have no incomplete task.
select *
from employee
where id not in
(
select employee_id
from task
where id not in (select task_id from status where terminal = true)
);
If tasks can be re-opened, however, then you do the same but must find the last status. This can be done with Postgre's DISTINCT ON for instance.
select *
from employee
where id not in
(
select employee_id
from task
where not
(
select distinct on (task_id) terminal
from status
where task_id = task.id
order by task_id, id desc
)
);
(I am using the ID to find the last entry per task, as the time without a date seems inappropriate. You could only use the time column instead, if a task will always run within one day only.)
SQL fiddles:
http://sqlfiddle.com/#!15/f0ea8/2
http://sqlfiddle.com/#!15/f0ea8/1
You have to group all the statuses togheter and you can then use MAX() to find if one of them is true, like this:
SELECT t.description, MAX(s.terminal)
FROM Employee e
INNER JOIN task t ON t.employee_id = e.id
INNER JOIN status s ON s.task_id = t.id
GROUP BY t.id;
When you want this just for one user add something like this WHERE e.id = 1.
Hope this helps
select T.employee_id, T.description, S.Terminal
from Employee E
INNER JOIN Task T ON E.id=T.employee_id
INNER JOIN (Select task_id, max(id) as status_id FROM Status GROUP BY task_id) as ST on T.id=ST.task_id
INNER JOIN Status S on S.id=ST.status_id
I hope this will help you...??
select E.Name,T.id as[Task Id],T.description,S.Terminal from Employee E
inner join Task T on E.id=T.employee_id
inner join Status S on S.task_id=T.id
where e.id not in (select employee_id from Task where id in (select task_id from Status where Terminal='true' and details='Completed') )

Active Record Query, return Users with over a certain number of check_ins at a restaurant

In my db I have Users that have check_ins. A check_in is tied to one restaurant with restaurant_id. What's the most efficient way to get all Users who have checked in at a given restaurant more then X times?
To write effect Active Record queries, you must first know how to write effective SQL queries. As with any programming problem, the first step is to break it down into smaller tasks.
TL;DR
Don't do two queries when you just need one.
users_with_check_in_counts = User.select('users.*, COUNT(*) AS check_in_count')
.joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
.where(check_ins: { restaurant_id: 1 })
.group(:id)
.having('check_in_count > ?', 3)
.all
# => [ #<User id=2, name="Nick", ..., check_in_count=4>,
# #<User id=4, name="Jordan", ..., check_in_count=4> ]
nick = users_with_check_in_counts.first
puts nick.check_in_count
# => 4
Prelude
Your check_ins table probably looks something like this:
id | restaurant_id | user_id | ...
-----+---------------+---------+-----
1 | 1 | 1 | ...
2 | 1 | 2 |
3 | 1 | 2 |
4 | 1 | 2 |
5 | 1 | 2 |
6 | 1 | 3 |
7 | 1 | 3 |
8 | 1 | 3 |
9 | 1 | 4 |
10 | 1 | 4 |
11 | 1 | 4 |
12 | 1 | 4 |
13 | 2 | 1 |
... | ... | ... | ...
In the above table we have 12 check-ins at the restaurant with restaurant_id = 1. The user with user_id = 1 checked in once, 2 checked in four times, 3 checked in twice, and 4 checked in four times.
The naïve way
The naive way to do this would be to break it down into the following tasks:
Get the check_ins records for the restaurant:
SELECT * FROM check_ins WHERE restaurant_id = 1;
Get the number of check-ins for each user for the restaurants by grouping by user_id and counting the number of records in each group:
SELECT check_ins.*, COUNT(user_id) AS check_in_count
FROM check_ins
WHERE restaurant_id = 1
GROUP BY user_id
Restrict the results to groups with at least than N records, e.g. N = 3:
SELECT check_ins.*, COUNT(user_id) AS check_in_count
FROM check_ins
WHERE restaurant_id = 1
GROUP BY user_id
HAVING check_in_count >= 3
Translate that into an Active Record query:
check_in_counts = CheckIn.where(restaurant_id: 1).group(:user_id)
.having("user_count > ?", 3).count
# => { 2 => 4, 4 => 4 }
Write a second query to get the associated users:
User.find(check_in_counts.keys)
# => [ #<User id=2, ...>, #<User id=4, ...> ]
That works, but there's something smelly about it—oh, it's that we're using a relational database. If we have a query that gets records from check_ins, we should just get the related users in the same query.
A better way
Now, it's relatively obvious that we could take our SQL query from (3) above and add a JOIN users ON check_ins.user_id = users.id to get the associated users records, but that leaves us in a bind because we still want Active Record to give us User objects, not CheckIn objects. To do that we need a different query, one that starts with users and joins check_ins.
To get there, we use LEFT OUTER JOIN:
SELECT *
FROM users
LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id
WHERE restaurant_id = 1;
The above query will give us results like this:
id | name | ... | restaurant_id | user_id
----+--------+-----+---------------+---------
1 | Sarah | 1 | 1 | 1
2 | Nick | 1 | 1 | 2
2 | Nick | 1 | 1 | 2
2 | Nick | 1 | 1 | 2
2 | Nick | 1 | 1 | 2
3 | Carmen | 1 | 1 | 3
3 | Carmen | 1 | 1 | 3
3 | Carmen | 1 | 1 | 3
4 | Jordan | 1 | 1 | 4
4 | Jordan | 1 | 1 | 4
4 | Jordan | 1 | 1 | 4
4 | Jordan | 1 | 1 | 4
This looks familiar: it has all of the data from check_ins, with the data from users added on to each row. That's what LEFT OUTER JOIN does. Now, just like before, we can use GROUP BY to group by user IDs and COUNT to count the records in each group, with HAVING to restrict the results to users with a certain number of check-ins:
SELECT users.*, COUNT(*) AS check_in_count
FROM users
LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id
WHERE restaurant_id = 1
GROUP BY users.id
HAVING check_in_count >= 3;
This gives us:
id | name | ... | check_in_count
----+--------+-----+----------------
2 | Nick | ... | 4
4 | Jordan | | 4
Perfect!
Finally...
Now all we have to do is translate this into an Active Record query. It's pretty straightforward:
users_with_check_in_counts = User.select('users.*, COUNT(*) AS check_in_count')
.joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
.where(check_ins: { restaurant_id: 1 })
.group(:id)
.having('check_in_count > ?', 3)
.all
# => [ #<User id=2, name="Nick", ..., check_in_count=4>,
# #<User id=4, name="Jordan", ..., check_in_count=4> ]
nick = users_with_check_in_counts.first
puts nick.check_in_count
# => 4
And best of all, it performs just one query.
Bonus: Scope it
That's a pretty long Active Record query. If there's only one place in your app where you're going to have a query like this, it might be okay to use it that way. If I were you, though, I would turn it into a scope:
class User < ActiveRecord::Base
scope :with_check_in_count, ->(opts) {
opts[:at_least] ||= 1
select('users.*, COUNT(*) AS check_in_count')
.joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
.where(check_ins: { restaurant_id: opts[:restaurant_id] })
.group(:id)
.having('check_in_count >= ?', opts[:at_least])
}
# ...
end
Then:
User.with_check_in_count(at_least: 3, restaurant_id: 1)
# ...or just...
User.with_check_in_count(restaurant_id: 1)
I cannot check this with your exact model schema, but something like this should work:
check_in_counts = CheckIn.group(:user_id).having(restaurant_id: 3).having('COUNT(id) > 10').count
This will return a Hash with user_id => check_in_count values, which you can use to fetch all the User objects:
users = User.find(check_in_counts.keys)

Check for new messages with SQL [facebook like chat]

I'm currently developing a facebook like chat for my website. Right now I am asking myself, what is the best way (the best sql query) to check for new messages?
My tables look like this:
messages
messageID | userID | roomID | message | time
-----------------------------------------------------------
4 | 1 | 1 | test | 1369062603
9 | 2 | 1 | great | 1369062609
rooms
roomID | host | createTime
-----------------------------
1 | 1 | 1369062600
room_to_user
roomID | userID
----------------
1 | 1
1 | 2
I plan to add group chatting that's why I don't want to add a sepereate read column to my message table.
My next attempt would be a sql query filtered by the time column.
What would be your attempt? Thanks!
Will this work?
Select m.*
From messages m join rooms r on m.roomID = r.roomID
join room_to_user ru on r.roomID = ru.roomID
where ru.userID = #userID and m.time > #timeSinceLastRead
You didn't specify which SQL you're using (Sql Server? Oracle? MySql?) and you'll probably have to play with the format for m.time.