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

nc1991's avatar

Eloquent query is taking an extremely long time

So I am trying to get my friends and followers statuses with a left join, here is my query:

`/**
 * Scope a query to friends and followers statuses.
 *
 * @param \Illuminate\Database\Eloquent\Builder $query
 *
 * @return \Illuminate\Database\Eloquent\Builder
 */
public function scopeFriendsAndFollowers(Builder $query): Builder
{
    $user_id = auth()->user()->id;

    return $query
        ->leftJoin('followables', function ($join) {
            $join
                ->on('timeline.owner_id', 'followables.followable_id')
                ->on('timeline.owner_type', 'followables.followable_type');
        })
        ->leftJoin('friendships', function ($join) {
            $join
                ->on('timeline.owner_id', 'friendships.recipient_id')
                ->on('timeline.owner_type', 'friendships.recipient_type');
        })
        ->where(function ($query) use($user_id) {
            $query
                ->where(function ($query) use($user_id) {
                    $query->where('followables.user_id', $user_id);
                })
                ->where('privacy', 'public')
                ->orWhere('privacy', 'private');
        })
        ->Orwhere(function ($query) use($user_id) {
            $query
                ->where(function ($query) use($user_id) {
                    $query
                        ->where('friendships.sender_id', $user_id)
                        ->orWhere('friendships.recipient_id', $user_id);
                })
                ->where('privacy', 'public')
                ->orWhere('privacy', 'private');
        })
        ->orWhere(function ($query) use($user_id) {
            $query
                ->where('timeline.owner_id', $user_id)
                ->where('timeline.owner_type', User::class);
        });
}`

Here is my schema;

Timeline:

    Schema::create('timeline', function (Blueprint $table) {
        $table->increments('id')->unsigned()->index();
        $table->text('status')->nullable();
        $table->string('location')->nullable();
        $table->string('type')->nullable();
        $table->string('privacy')->default('public');
        $table->integer('owner_id');
        $table->string('owner_type');
        $table->timestamps();
        $table->softDeletes();

        $table->index(['owner_id', 'owner_type']);
        $table->index('privacy');
        $table->index('owner_id');

    });

Friends:

    Schema::create(config('friendships.tables.fr_pivot'), function (Blueprint $table) {
        $table->increments('id')->unsigned()->index();
        $table->morphs('sender');
        $table->morphs('recipient');
        $table->tinyInteger('status')->default(0);
        $table->timestamps();

        $table->index(['sender_id', 'recipient_id']);
    });

Followers

    Schema::create(config('follow.followable_table', 'followables'), function (Blueprint $table) {
        $table->increments('id')->unsigned()->index();
        $table->unsignedInteger('user_id')->index();
        $table->unsignedInteger('followable_id');
        $table->string('followable_type');
        $table->string('relation')->default('follow')->comment('folllow/like/subscribe/favorite/');
        $table->timestamp('created_at');

        $table->foreign('user_id')
            ->references(config('follow.users_table_primary_key', 'id'))
            ->on(config('follow.users_table_name', 'users'))
            ->onUpdate('cascade')
            ->onDelete('cascade');

        $table->index(['followable_id', 'followable_type']);

    });

Then I run the query like so:

    return $this->timeline->select('timeline.*')
        ->friendsAndFollowers()
        ->orderBy('timeline.created_at', 'desc')
        ->paginate(10);

The raw sql query:

SELECT `timeline`.* 
FROM   `timeline` 
    LEFT JOIN `followables` 
            ON `timeline`.`owner_id` = `followables`.`followable_id` 
                AND `timeline`.`owner_type` = `followables`.`followable_type` 
    LEFT JOIN `friendships` 
            ON `timeline`.`owner_id` = `friendships`.`recipient_id` 
WHERE  ( ( `followables`.`user_id` = :user_id 
        AND `privacy` = ':public' 
            OR `privacy` = ':private' ) 
        OR ( ( `friendships`.`sender_id` = :user_id 
                OR `friendships`.`recipient_id` = :user_id ) 
            AND `privacy` = ':public' 
                OR `privacy` = ':private' ) 
        OR ( `timeline`.`owner_id` = :user_id 
            AND `timeline`.`owner_type` = ':class' ) ) 
ORDER  BY `timeline`.`created_at` DESC 

And here is the EXPLAIN:

https://i.imgur.com/7qfkNYY.png

I only have 10000 rows in each table, so it's not big at all, I have indexes for the relationships, any idea on what is wrong?

0 likes
4 replies
Tray2's avatar

That is a monster.

AND `privacy` = ':public' 
            OR `privacy` = ':private' ) 

Do you have more than these two options for privacy? If yes use AND privacy IN (:public, :private) If no the remove the condition completly since you select them all anyway.

Every OR adds time to your execution so try to use them very sparingly.

1 like
Cronix's avatar

You need a LOT more indexes lol.

Anything you are joining on should be indexed (id's foreign id's etc)

$join
                ->on('timeline.owner_id', 'followables.followable_id')
                ->on('timeline.owner_type', 'followables.followable_type');

For example, you don't have indexes on any of those (owner_id, owner_type, followable_id, followable_type). Not just there though...

Anything in a where should be indexed

->where('privacy', 'public')

privacy is not indexed

Please watch this: https://serversforhackers.com/laravel-perf/mysql-indexing-one

1 like
nc1991's avatar

Thanks Cronix, I thought privacy is indexed because I have it in my schema:

$table->index('privacy');

would I index every one like so:

  $table->index('privacy');
  $table->index('owner_id');
  $table->index('owner_type');
  $table->index('status');

Or like so:

$table->index(['privacy', 'owner_id', 'owner_type', 'status']);
kbush's avatar

This isn't a laravel problem but a database one. Put simply, you're abusing your database and your query is asking a lot more than you probably think.

  1. You are asking for all matching records...
  2. You are using two joins, getting all matching records for a table using two joins means your database has to look at all records in all three tables and build a temporary table based on your where conditions...
  3. Your where clause is extremely complex... too complex. These can't be compared all at the same time so each check is causing the database to look at records multiple times.
  4. Some of your where clause seems like it might not be needed. But it is hard to tell without knowing more about the workings of your app. Some things to look at, how many values do you have for 'privacy'? If it is just the two then remove those checks all together. Another is if you are comparing by id, then do you really need to compare the type as well?

Your database building a result set by looking at 20,010 records across 3 tables. To do this it needs to perform 3 condition checks just to get a row of data that it can compare your where clause to. Your where clause requires 9 checks per record. You database then looks at every record and sorts them by your orderby clause. So it is performing 240,120 comparisons just to return your result set of 10,000 records or less. This will get exponentially worse as your database has more records in it. Better indexes will help with some of it, but you are still asking for a lot from the database engine.

Simplify your where clause. LIMIT your returned returned results. Add indexes to all compared fields. Do not sort if you don't need to. When applications have to deal with this level of complex data, they do so by making heavy use of result caching and spending a lot on high memory server farms as well as archiving less relevant data(completely moving out of the main database tables).

1 like

Please or to participate in this conversation.