Eloquent Performance Pattern - notes

use laravel debugbar

composer require barryvdh/laravel-debugbar --dev

use index to make it faster

// everything in column field needs to be indexed
order by {field}, where {field} = 1

do not use *

// *
User::query()
	->with('author')
	->latest('published_at')
	->get()
	->groupBy(fn($post) => $post->published_at->year);

use select

User::query()
	->select('id','title','slug','published_at','author_id') // <- this
	->with('author')
	->latest('published_at')
	->get()
	->groupBy(fn($post) => $post->published_at->year);

within with() can select too

User::query()
	->select('id','title','slug','published_at','author_id')
	->with('author:id,name') // <- this
	->latest('published_at')
	->get()
	->groupBy(fn($post) => $post->published_at->year);

When using with() loads more data, subquery only can return 1 column back

$user = User::query()
	->addSelect(['last_login_at' => 
		Login::select('created_at')
			->whereColumn('user_id', 'users.id')
			->latest()
			->take(1)
	]) // instead of with
	->withCasts(['last_login_at' => 'datetime']) // instead of diffForHumans()
	->orderBy('name')
	->paginate();

dynamic relationship

$user = User::query()
	->withLastLogin()
	->orderBy('name')
	->paginate();

public function lastLogin() {
	return $this->belongsTo(Login::class);
}

public function scopeWithLastLogin($query) {
	$query->addSelect(['last_login_id' => Login::select('id')
		->whereColumn('user_id', 'users.id')
		->latest()
		->take(1)
	])->with('lastLogin');
}

conditional aggregates

select
	count(case when status = 'Requested' then 1 end) as requested,
	count(case when status = 'Planned' then 1 end) as planned,
	count(case when status = 'Completed' then 1 end) as completed
from features

$statuses = Feature::toBase()
	->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
	->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
	->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
	->first();

optimizing circular relationship

public function show(Feature $feature)
{
	$feature->load('comments.user');
	$feature->comments->each->setRelation('feature',$feature);
	return view('feature', ['feature' => $feature]);
}

multi column searching

$users = User::query()
	->search(request('search'))
	->with('company')
	->paginate();
return view('users', ['users' => $users]);

public function scopeSearch($query, string $terms = null) {
	collect(explode(' ', $terms))->filter()->each(function($term) use ($query) {
		$term = '%' . $term . '%';
		$query->where(function($query) use ($term) {
			$query->where('first_name', 'like', $term)
				->orWhere('last_name', 'like', $term)
				->orWhereHas('company', function($query) use ($term) {
					$query->where('name', 'like', $term);
				});
		}
	}
}

normalize field using regex

Schema::created('companies', function(Blueprint $table) {
	...
	$table->string('first_name_normalize')
	->virtualAs("regexp_replace(first_name, '[^A-Za-z0-9]', '')")->index();
	...
});

limit rows by user_id with loggedin user

public function scopeVisibleTo($query, User $user) {
	if ($user->is_owner) {
		return;
	}
	$query->where('sales_rep_id', $user->id);
}

compound index where it’s a multiple index into 1, the order of the index matters, the example needs to be last_name first then the first_name not the other way around.

$table->index(['last_name','first_name']);

subquery has one relationship, order without using leftjoin

$user = User::query()
	->orderBy(Company::select('name')
		->whereColumn('user_id','users.id')
		->orderBy('name')
		->take(1)
	)
	->with('company')
	->paginate();

subquery belongs to relationship,

order by without using leftjoin but for this case leftjoin is faster so use left join.

//join (faster)
$user = User::query()
	->select('users,*')
	->join('companies','companies.id','=','users.company_id')
	->orderBy('companies.name')
	->with('company')
	->paginate();
//subquery
$user = User::query()
	->orderBy(Company::select('name')
		->whereColumn('id','users.company_id')
		->orderBy('name')
		->first()
	)
	->with('company')
	->paginate();

subquery has many relationship

$user = User::query()
	->select('users.*')
	->join('logins', 'logins.user_id', '=', 'users.id')
	->orderByDesc('logins.created_at')
	->groupBy('users.id') // error below
	->withLastLogin()
	->paginate();

trouble when adding groupBy → order by clause is not in group by clause and contains nonaggregated column

// use this
->orderByRaw('max(logins.created_at) desc')

subquery

$user = User::query()
	->select('users.*')
	->orderByDesc(Login::select('created_at')
		->whereColumn('user_id', 'users.id')
		->latest()
		->take(1)
	)
	->withLastLogin()
	->paginate();

Subquery belongs to many

$books = Book::query()
	->select('books.*')
	->join('checkouts', 'checkouts.book_id', '=', 'books.id')
	->groupBy('books.id')
	->orderByRaw('max(checkouts.borrowed_date) desc')
	->withLastCheckout()
	->with('lastCheckout.user')
	->paginate();

// to subquery
$books = Book::query()
	->orderByDesc(Checkout::select('borrowed_date')
		->whereColumn('book_id', 'books.id')
		->latest('borrowed_date')
		->take(1)
	)
	->withLastCheckout()
	->with('lastCheckout.user')
	->paginate();

// another way
$books = Book::query()
	->orderByDesc(function($query) {
			$query->select('borrowed_date')
				->from('checkouts)
				->whereColumn('book_id', 'books.id')
				->latest('borrowed_date')
				->take(1)
		}
	)
	->withLastCheckout()
	->with('lastCheckout.user')
	->paginate();

// another way
$books = Book::query()
	orderBy(User::select('name')
		->join('checkouts', 'checkouts.book_id', '=', 'books.id')
		->whereColumn('checkouts.book_id', 'books.id')
		->latest(checkouts.borrowed_date')
		->take(1)
	)
	->withLastCheckout()
	->with('lastCheckout.user')
	->paginate();

order with nulls first

$book = Book::query()
	->with('user')
	->orderByDesc('user_id') // messes up the order name
	->orderBy('name')
	->paginate();

$book = Book::query()
	->with('user')
	->orderByRaw('user_id is null')
	->orderBy('name')
	->paginate();
// this query shows the null first
$users = User::query()
	->when(request('sort') === 'town', function($query) {
			$query->orderBy('town', request('direction'));
		})
	->orderBy('name')
	->paginate();

$users = User::query()
	->when(request('sort') === 'town', function($query) {
			$query->orderByRaw('town is null')
				->orderBy('town', request('direction'));
		})
	->orderBy('name')
	->paginate();

Ordering by custom algorithm

status using when

public function scopeOrderByStatus($query, $direction)
{
	$query->orderBy(DB::raw('
		case
			when status = "Requested" then 1
			when status = "Approved" then 2
			when status = "Completed" then 3
		end
	'), $direction);
}

then in migration

Schema::create('features', function(Blueprint $table) {
	$table->rawIndex('(
		case
			when status = "Requested" then 1
			when status = "Approved" then 2
			when status = "Completed" then 3
		end
	'), 'feature_status_ranking_index');
			
}

activity using count

$features = Feature::query()
	->withCount('comments', 'votes')
	...

public function scopeOrderByActivity($query, $direction)
{
	$query->orderBy(
		DB::raw('(votes_count + (comments_count *))'),
		$direction
	)
}

Filtering and sorting

$users = User::query()
	->orderByBirthday()
	->whereBirthdayThisWeek()

public function scopeOrderByBirthday($query)
{
	$query->orderByRaw('date_format(birth_date, "%m-%d")');
}

Schema::create('features', function(Blueprint $table) {
	$table->rawIndex("date_format(birth_date, '%m-%d'), name", 'users_birthday_name_index');
}

public function scopeWhereBirthdayThisWeek($query)
{
	$query->whereRaw('date_format(birth_date, "%m-%d") between ? and ?', [
		Carbon::now()->startOfWeek()->format('m-d'),
		Carbon::now()->endOfWeek()->format('m-d'),
	]);

	// OR

	$query->whereRaw(date_format(birthdate, '%m-%d') in (?,?,?,?,?,?) ,
		iterator_to_array($dates));
}

Fulltext

DB::statement('
	create fulltext index posts_fulltext_index
	on posts(title, body)
	with parser ngram
');

// example
select id, title, match(title, body) against ('fox' in boolean mode) as score
from posts
where match(title, body) against ('fox' in boolean mode)

$posts = Post::query()
	->with('author')
	->when(request('search'),function($query, $search) {
		$query->selectRaw('*,match(title, body) against(? in boolean mode)' as score, [$search])
			->whereRaw('match(title, body) against(? in boolean mode)', [$search])
	}, function($query) {
		$query->latest('published_at');
	})
	->paginate();

Getting distance between geo points

Schema::create('stores', function(Blueprint $table) {
	...
	$table->point('location', 4326);
	...
});

$myLocation = [-79.47, 43.14];
$stores = Store::query()
	->selectDistanceTo($myLocation)
	->withinDistanceTo($myLocation, 10000)
	->orderByDistance($myLocation)
	->paginate();

public function scopeWithinDistanceTo($query, [] $coordinates, int $distance) {
	$distance = strtolower($direction) === 'asc' ? 'asc', : 'desc';
	$query->whereRaw('ST_Distance(
		location,
		ST_SRID(Point(?, ?),4326),
	) <= ?', $direction, $coordinates);
});

public function scopeWithinDistanceTo($query, [] $coordinates, int $distance) {
	$query->whereRaw('ST_Distance(
		ST_SRID(Point(longitude, latitude),4326),
		ST_SRID(Point(?, ?),4326),
	) <= ?', [...$coordinates, $distance]);
});

public function scopeSelectDistanceTo($query, array $coordinates) {
	if (is_null($query->getQuery()->columns)) {
		$query->select('*');
	}

	$query->selectRaw('ST_Distance(
		ST_SRID(Point(longitude, latitude),4326),
		ST_SRID(Point(?, ?),4326),
	) as distance', $coordinates);
});

// change to this

	$query->selectRaw('ST_Distance(
		location,
		ST_SRID(Point(?, ?),4326),
	) as distance', $coordinates);

filter by geospatial

Schema::create('customer', function(Blueprint $table) {
	...
	$table->point('location');
	...
});

Schema::create('regions', function(Blueprint $table) {
	...
	$table->geometry('geometry');
	...
});

public function scopeInRegion($query, $region) {
	$query->whereRaw('ST_Contains(?, customers.location)', [$region->geometry]);
});