0

I have a messages table.

+----+---------+----------+
| id | conv_id | body     |
+----+---------+----------+
|  1 |       1 |     haha |
|  2 |       1 |     blabl|
| ...|     ... |    ...   |
|  25|       2 |     hehe |
+----+---------+----------+

... = rest of messages with conv_id of 2's or 1's or 3's or n's.

Let's say I have conv_id = array(2,1) and I want to obtain 10 messages after matched with an array of ids in conv_id so I did

select * from `messages` where `conv_id` in (2, 1) order by `created_at` desc limit 10

The sql above gave me 10 messages after matching both conv_ids and getting all combined messages. However, this is NOT what I wanted. Instead, I wanted 10 messages of EACH conv_id matched.

How do I get 10 messages of EACH conv_id matched? No PHP for loop, please. Thank you!

NOTE : the array conv_id can easily be extended to include many other values unique to each other, not only 2s or 1s.


P.s., bonus points for Laravel Eloquent answer! Here are the details :

  1. Two models, Conversations and Messages linked by Conversations hasMany Message and Message belongsTo a Conversation.
  2. My sql above was translated from Messages::with('User')->whereIn('conv_id',$conv_id)->orderBy('created_at','desc')->take(10);
John Evans Solachuk
  • 1,953
  • 5
  • 31
  • 67
  • One way would be a `UNION` between two queries, one dealing with `1`s, the other `2`s. – Jared Farrish Jun 23 '14 at 01:19
  • @JaredFarrish right, waiting for your answer. Thanks! – John Evans Solachuk Jun 23 '14 at 01:20
  • If only you were using SQL Server or Oracle, a simple partition would work. – Lock Jun 23 '14 at 01:52
  • Here's [an in-depth article](http://www.xaprb.com/blog/2006/12/07/how-to-select-the-firstleastmax-row-per-group-in-sql/) explaining the options. Note that none of the solutions are particularly elegant (all use a subquery, `UNION` and/or variables), because LIMIT in SQL just does not work like this. – Jared Farrish Jun 23 '14 at 02:19
  • This looks suspiciously like it does what you want: http://sqlfiddle.com/#!2/aefd7a/16 Randomizing it to get something other than the first ten every time would probably be trickier. It's also iffy if it would always return the same 10, so it might work more consistently if the values could only be queried once for the grouping of each value (maybe a `CROSS JOIN` of some kind?). – Jared Farrish Jun 23 '14 at 02:43
  • @JaredFarrish thanks, will take a look soon. – John Evans Solachuk Jun 23 '14 at 03:36

3 Answers3

1

I think Jared is right but if you can add another column to the table, solution would be more efficient. Add a column which indicates message number for each conv_id (earliest will have 1 and the latest will have number of messages conversation have). After that, you can achieve your goal by scanning table twice with HAVING clause.

SELECT * FROM messages JOIN
  (SELECT conv_id, MAX(msg_no) FROM messages WHERE conv_id IN (2,1) GROUP BY conv_id) as M 
ON messages.conv_id=M.conv_id HAVING messages.msg_no > M.msg_no-10
mayy00
  • 224
  • 5
  • 14
  • Can this query be simplified further? If possible, I need to translate it to Laravel's eloquent functions. Thanks! – John Evans Solachuk Jun 23 '14 at 01:58
  • Sorry I am not familiar with Laravel. However if it is difficult (or not possible? I do not really know) in Laravel, I think it is ok to get your hands dirty sometimes and execute raw SQL statements. – mayy00 Jun 23 '14 at 02:08
  • yup it's possible. I just wanted the codes to look more elegant. :P – John Evans Solachuk Jun 23 '14 at 02:13
1

Another possibility. Get last conv_ids and their 10th (message)id with a group_concat - substring_index trick and re-join of message-table.

SELECT `messages`.*
FROM (
    SELECT 
    conv_id,
    substring_index(
        substring_index(
            group_concat(`messages`.id),
            ',',
            10
        ),
        ',',
        -1
    ) AS lastMessageId
    FROM `messages` 
    WHERE `conv_id` in (2, 1)
    GROUP BY `conv_id`
) AS msub
INNER JOIN `messages` ON `messages`.conv_id = msub.conv_id AND `messages`.id <= msub.lastMessageId
Jared Farrish
  • 48,585
  • 17
  • 95
  • 104
lopo
  • 306
  • 1
  • 4
  • I don't know that the `AND messages.id <= msub.lastMessageId` will work; why not `FIND_IN_SET(messages.id, msub.lastMessageId)`? Also, as an aside, this will return the same 10 entries each time (if that's an issue). – Jared Farrish Jun 23 '14 at 21:51
  • Thats a performance question but it's surely possible the group_concat result use with a FIND_IN_SET. – lopo Jun 23 '14 at 22:33
0

Warning: This approach is potentially wrong. I'm trying to validate it and will update it once I reach a conclusion

Yesterday I just learnt something new about Eloquent relationship from an answer by deczo in Getting just the latest value on a joined table with Eloquent. And I think we can adapt it to your case as well.

What you're essentially trying to do, I put in my view as:

  • A Conversation model that has many Messages.
  • But I want only 10 latest messages per each conversation. All come eager-loaded.

In Eloquent, I would probably do something along the line of:

Conversation::with('messages')->whereIn('conv_id', [1, 2])->get();

But two things I note of your question.

  • I assume you don't have another table called conversations.
  • The code above does not limit to how many messages per conversation.

So I decided that here should be two models, one called Message, another called Conversation. Both call to the same table, but we will tell Eloquent to use different columns as primary key. So to get a Conversation model, I added a distinct() to my newQuery function, and select only conv_id out since that's the only information about a conversation.

So in the end I have this:

models/Message.php

class Message extends Eloquent
{
    protected $table = 'messages';
    protected $primaryKey = 'id';
    public $timestamps = false;
}

models/Conversation.php

class Conversation extends Eloquent
{
    protected $table = 'messages';
    protected $primaryKey = 'conv_id';
    public $timestamps = false;

    public function newQuery($excludeDeleted = true)
    {
        return parent::newQuery()
            ->select('conv_id')
            ->distinct();
    }

    public function messages()
    {
        return $this->hasMany('Message', 'conv_id')
            ->orderBy('id', 'desc')
            ->take(10);
    }
}

Now I can do:

Conversation::with('messages')->whereIn('conv_id', [1, 2])->get();

which gives me a list of conversations with conv_id 1 and 2, along with the 10 latest messages for each of them.

This is the first time I do something like this. So I code-tested and it works.


Update: The code in my answer perform these two queries only (retrieved from DB::getQueryLog()):

select distinct `conv_id` from `messages` where `conv_id` in (?, ?);
select * from `messages` where `messages`.`conv_id` in (?, ?) order by `id` desc limit 10;
Community
  • 1
  • 1
Unnawut
  • 7,500
  • 1
  • 26
  • 33
  • Btw, what's the newQuery function in Conversations model for and how is it used? I didn't see you using it in your code. Thank you! – John Evans Solachuk Jun 24 '14 at 09:45
  • It's from my assumption that "you don't have another table called conversations." The `newQuery()` is what Laravel calls when it starts to construct a query for you. So I append it with select distinct `conv_id` to emulate a conversations table. In case you already have a conversation table just remove my `newQuery()` part totally. – Unnawut Jun 24 '14 at 09:49
  • I'd be interested to see what that raw query looks like. – Jared Farrish Jun 24 '14 at 12:12
  • @JaredFarrish updated my answer with result from DB::getQueryLog() ! – Unnawut Jun 24 '14 at 12:16
  • Hmm wait, something is smelling fishy here. I'll get back with the update. – Unnawut Jun 24 '14 at 12:18
  • If that's really the query that's being used, that would not be any different than the one the OP is getting. – Jared Farrish Jun 24 '14 at 21:26
  • @JaredFarrish that is true. :( I'm checking my answer plus validating the previous answer that I referred to at the top of my answer too. – Unnawut Jun 25 '14 at 05:33