Add Nightwatch configuration guidance and MCP development skill docs across agent integrations, and refresh existing Laravel skill references.
4.1 KiB
Database Performance Best Practices
Always Eager Load Relationships
Lazy loading causes N+1 query problems — one query per loop iteration. Always use with() to load relationships upfront.
Incorrect (N+1 — executes 1 + N queries):
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name;
}
Correct (2 queries total):
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name;
}
Constrain eager loads to select only needed columns (always include the foreign key):
$users = User::with(['posts' => function ($query) {
$query->select('id', 'user_id', 'title')
->where('published', true)
->latest()
->limit(10);
}])->get();
Prevent Lazy Loading in Development
Enable this in AppServiceProvider::boot() to catch N+1 issues during development.
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}
Throws LazyLoadingViolationException when a relationship is accessed without being eager-loaded.
Select Only Needed Columns
Avoid SELECT * — especially when tables have large text or JSON columns.
Incorrect:
$posts = Post::with('author')->get();
Correct:
$posts = Post::select('id', 'title', 'user_id', 'created_at')
->with(['author:id,name,avatar'])
->get();
When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match.
Chunk Large Datasets
Never load thousands of records at once. Use chunking for batch processing.
Incorrect:
$users = User::all();
foreach ($users as $user) {
$user->notify(new WeeklyDigest);
}
Correct:
User::where('subscribed', true)->chunk(200, function ($users) {
foreach ($users as $user) {
$user->notify(new WeeklyDigest);
}
});
Use chunkById() when modifying records during iteration — standard chunk() uses OFFSET which shifts when rows change:
User::where('active', false)->chunkById(200, function ($users) {
$users->each->delete();
});
Add Database Indexes
Index columns that appear in WHERE, ORDER BY, JOIN, and GROUP BY clauses.
Incorrect:
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('status');
$table->timestamps();
});
Correct:
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->index()->constrained();
$table->string('status')->index();
$table->timestamps();
$table->index(['status', 'created_at']);
});
Add composite indexes for common query patterns (e.g., WHERE status = ? ORDER BY created_at).
Use withCount() for Counting Relations
Never load entire collections just to count them.
Incorrect:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->comments->count();
}
Correct:
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
Conditional counting:
$posts = Post::withCount([
'comments',
'comments as approved_comments_count' => function ($query) {
$query->where('approved', true);
},
])->get();
Use cursor() for Memory-Efficient Iteration
For read-only iteration over large result sets, cursor() loads one record at a time via a PHP generator.
Incorrect:
$users = User::where('active', true)->get();
Correct:
foreach (User::where('active', true)->cursor() as $user) {
ProcessUser::dispatch($user->id);
}
Use cursor() for read-only iteration. Use chunk() / chunkById() when modifying records.
No Queries in Blade Templates
Never execute queries in Blade templates. Pass data from controllers.
Incorrect:
@foreach (User::all() as $user)
{{ $user->profile->name }}
@endforeach
Correct:
// Controller
$users = User::with('profile')->get();
return view('users.index', compact('users'));
@foreach ($users as $user)
{{ $user->profile->name }}
@endforeach