Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

mbeckett's avatar

Eloquent Model query with custom select/join/order

I have models called Post, and PostView. A Post can have many PostViews. I need to get a paginated list of Posts ordered by the count of PostViews created within the last 30 days.

I can do this using withCount:

// works but S L O W
$posts = Post::withCount(['views' => function($query) {
            $query->where('post_views.created_at', '>=', 'DATE_ADD(CURDATE(), INTERVAL -30 DAY)');
         }])
         ->orderBy('views_count')
         ->paginate(10);

However this generates a query that is really slow, taking ~24 seconds.

Using raw sql I can get the correct results much more efficiently, how can I turn that into the paginated model collection?

This generates the correct query to grab the first 10, but the resulting collection is empty. I assume it has something to do with the selectRaw

$posts = Post::selectRaw('posts.*, count(post_views.id) as views_count')
           ->join('post_views', function($join) {
                $join->on('posts.id', '=', 'post_views.post_id')
                   ->where('post_views.created_at', '>=', 'DATE_ADD(CURDATE(), INTERVAL -30 DAY)');
            })
            ->groupBy('posts.id')
            ->orderBy('views_count', 'DESC')
            ->take(10)
            ->get();

If I run the query that generates directly in mysql I do get results: (note - truncated to posts.id for brevity)

mysql> select posts.id, count(post_views.id) as views_count from `posts` inner join `post_views` on `posts`.`id` = `post_views`.`post_id` and `post_views`.`created_at` >= DATE_ADD(CURDATE(), INTERVAL -30 DAY) group by `posts`.`id` order by `views_count` desc limit 10;
+--------+-------------+
| id     | views_count |
+--------+-------------+
| 150277 |          22 |
|  43843 |           6 |
| 138789 |           4 |
| 180565 |           4 |
|  50555 |           3 |
|   2679 |           3 |
| 188572 |           3 |
| 217454 |           3 |
| 136736 |           3 |
| 126472 |           2 |
+--------+-------------+
10 rows in set (1.26 sec)

Any help is appreciated, thanks.

0 likes
5 replies
mbeckett's avatar

Thanks for the reply

I tried that like so:

$posts = Post::select('posts.*', DB::raw('count(post_views.id) as views_count'))
    ->join('post_views', function($join) {
        $join->on('posts.id', '=', 'post_views.post_id')
            ->where('post_views.created_at', '>=', 'DATE_ADD(CURDATE(), INTERVAL -30 DAY)');
        })
    ->groupBy('posts.id')
    ->orderBy('views_count', 'DESC')
    ->take(10)
    ->get();

    // ideally ->paginate(10) instead of take/get

That generates this query:

select `posts`.*, count(post_views.id) as views_count from `posts` inner join `post_views` on `posts`.`id` = `post_views`.`post_id` and `post_views`.`created_at` >= DATE_ADD(CURDATE(), INTERVAL -30 DAY) group by `posts`.`id` order by `views_count` desc limit 10

Which gives the same result as in the original question, but I still end with $posts being:

Illuminate\Database\Eloquent\Collection Object
(
    [items:protected] => Array
        (
        )

)

If there's something with a custom paginator that would do it please point me there, my confusion is that the query has results, but the collection doesn't contain the models.

mbeckett's avatar

Through a very time consuming method of trial & error and selective deleting things, including reducing the query to a DB::table() ignoring the model I have found that issue is somewhere in the JOIN

I don't know what yet though, as the query it produces seems valid, and when run in mysql directly has the correct results. However the join + where together creates no results. As far as I can tell the structure of it is in line with the docs for using a join + where

mbeckett's avatar
mbeckett
OP
Best Answer
Level 2

@bobbybouwmann - you were so close, and I completely missed it, but it's not just the select that requires the DB::raw treatment...

$join->on('posts.id', '=', 'post_views.post_id')
    ->where('post_views.created_at', '>=', 'DATE_ADD(CURDATE(), INTERVAL -30 DAY)');

The DATE_ADD() and CURDATE() were getting escaped, and of course in the query log you can't tell it's been escaped so the query log copied into mysql directly worked...

/facepalm

So... this did the trick

$join->on('posts.id', '=', 'post_views.post_id')
    ->where('post_views.created_at', '>=', DB::raw('DATE_ADD(CURDATE(), INTERVAL -30 DAY)'));

Thanks for the help!

Please or to participate in this conversation.