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

thebookcollector's avatar

How can I get the top 10 highest rated books?

I have a 'books' table with an 'id' column and I have a 'ratings' table with a 'book_id' column (related to the 'id' field in the 'books' table obviously) and a 'value' column.

Users can rate books in the application so a given book can have zero ratings or may have hundreds of different ratings.

The Book model has the following relationship:

// Relationship to Rating
    public function ratings(){
        return $this->hasMany(Rating::class);
    }

and the Rating model has the following relationship:

// Relationship to Book
    public function book(){
        return $this->belongsTo(Book::class);
    }

How can I get the top 10 highest rated books? Important to also consider number or ratings if the average rating is tied. So a book with an average rating of 7 with 100 people that have rated it will be higher on the list than a book with a rating of 7 and only 50 people that have rated it.

Also ideally I'd like to be able to ignore books that don't meet a number of ratings threshold (maybe 5?) Like I don't want a book that was just entered into the database and then rated 10 to be at the top of the list just because one person rated it a 10.

0 likes
5 replies
Tray2's avatar
Tray2
Best Answer
Level 73

So you mean something like this`?

select book_id, 
    avg(rating)  avg_rating,
    count(*) rated_time, 
     avg(rating) * count(*) score 
 from book_rating 
  group by book_id 
  order by 4 desc;

Gives a result like this

+---------+------------+------------+----------+
| book_id | avg_rating | rated_time | score    |
+---------+------------+------------+----------+
|      98 |     6.2222 |         18 | 112.0000 |
|       9 |     6.7500 |         16 | 108.0000 |
|      81 |     5.8333 |         18 | 105.0000 |
|      43 |     7.0769 |         13 |  92.0000 |
|       8 |     6.0000 |         15 |  90.0000 |
|      36 |     6.2857 |         14 |  88.0000 |
|      55 |     6.1429 |         14 |  86.0000 |
|       3 |     5.2500 |         16 |  84.0000 |
|      92 |     5.1250 |         16 |  82.0000 |
|      59 |     5.5714 |         14 |  78.0000 |
|      49 |     7.0000 |         11 |  77.0000 |
|      80 |     5.0667 |         15 |  76.0000 |
|       6 |     6.9091 |         11 |  76.0000 |
|      71 |     5.4286 |         14 |  76.0000 
3 likes
thebookcollector's avatar

@Tray2 I may have been more complicated in my ask (or just unclear) but this is more that what I was looking for.

I was really just looking for a simple ordering by average rating but also take into account ties and breaking them by number or ratings - and then also throwing out any books that don't have at least 5 ratings. In Eloquent.

However, you've taken it to a point of combining the rating and number or ratings into a ranking system or algorithm which is very helpful - at least as a start that I can begin tinkering with. I'll start playing with this soon and see if it works for my needs. Thank you.

Tray2's avatar

@thebookcollector You are welcome.

You can add this to the query to rule out any with less than five ratings.

where  count(*) > 5

But that isn't really necessary with the way I wrote the query.

Doing something like that in eloquent usually gets a bit messy. I would suggest pushing it down into a view in the database and then you can just

$topBooks = BookRatingView::orderBy('score', 'DESC')->take(10)->get();

Here is a post on creating and using database views in Laravel.

https://tray2.se/posts/use-a-view-instead-of-a-complex-eloquent-query-in-your-laravel-application

2 likes
thebookcollector's avatar

@Tray2 - I found that using the DB view, I wasn't able to access relationships and eager loading, so I converted your first comment to eloquent and it's working great. Thank you for the help!

Tray2's avatar

@thebookcollector Glad it worked out for you.

The point of a database view is to decrease the number of relationships, you can have them yes, but your case, I would just pull in the author, title and whatever else into the view.

Here is an example from a project that I'm working on when I have the time.

        DB::statement("CREATE OR REPLACE VIEW book_index_views AS
                    SELECT
                    (SELECT GROUP_CONCAT(a.id ORDER BY a.id SEPARATOR ',')
                    FROM authors a, author_book ab
                    WHERE a.id = ab.author_id
                    AND ab.book_id = b.id) author_id,
                    (SELECT GROUP_CONCAT(concat(a.last_name, ', ', a.first_name)
                    ORDER BY a.last_name, a.first_name SEPARATOR ' & ')
                    FROM authors a, author_book ab
                    WHERE ab.author_id = a.id
                    AND ab.book_id = b.id) author_name,
                    b.id book_id,
                    b.title,
                    b.part,
                    b.published_year,
                    CASE s.name
                        WHEN 'Standalone'
                        THEN b.published_year
                        ELSE (SELECT MIN(bi.published_year)
                              FROM books bi
                              WHERE bi.series_id = b.series_id)
                        END series_started,
                    f.name format,
                    g.name genre,
                    s.name series,
                    s.id series_id
                    FROM books b,
                         formats f,
                         genres g,
                         series s
                    WHERE b.genre_id = g.id
                    AND b.format_id = f.id
                    AND b.series_id = s.id
        ");

As you can see, I'm pulling in data from four different tables in my main query

  1. books
  2. formats
  3. genres
  4. series

And I also pull in the authors in a subquery.

That allows me to do this in my controller

   public function __invoke(Request $request)
    {
        return view('books.index')
            ->with([
                'books' => BookIndexView::query()
                   ->orderBy('author_name')
                    ->orderBy('series')
                    ->orderBy('part')
                    ->orderBy('published_year')
                    ->get(),
            ]);
    }

You can find the project here https://github.com/Tray2/mediabase

1 like

Please or to participate in this conversation.