Relationships are the heart of real data models, and N+1 queries are the most common Laravel performance bug.
Relationships are methods on models that return relationship objects:
hasMany/belongsTo: aUserhas manyPosts (posts.user_id), and each Post belongs to a User.hasOne,belongsToMany(many-to-many through a pivot table such aspost_tag, withattach,detachandsync),hasManyThrough, and polymorphicmorphMany/morphTo.
Access a relationship as a property to get the results ($user->posts is a Collection, loaded once and cached on the model) or as a method to keep querying: $user->posts()->where('published', true)->count(). Create related rows through the relation: $user->posts()->create([...]) fills user_id for you.
The N+1 problem: looping over 50 posts and printing $post->author->name runs 1 query for posts plus 50 for authors. Fix it with eager loading:
$posts = Post::with(['author', 'tags'])->latest()->get(); // 3 queries totalwith('comments.author')loads nested relations; constrain withwith(['comments' => fn ($q) => $q->latest()]).load('author')eager-loads onto models you already have.withCount('comments')adds acomments_countattribute without loading rows;whereHas('comments', ...)filters by related rows.
Add Model::preventLazyLoading(! app()->isProduction()); in AppServiceProvider::boot so lazy loading throws during development and N+1 bugs surface immediately.
Example
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\{BelongsTo, BelongsToMany, HasMany};
class Post extends Model
{
public function author(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); }
public function comments(): HasMany { return $this->hasMany(Comment::class); }
public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class)->withTimestamps(); }
}
// N+1: 1 query + 1 per post
foreach (Post::all() as $post) echo $post->author->name;
// Eager loaded: 3 queries no matter how many posts
$posts = Post::with(['author', 'tags'])
->withCount('comments') // adds comments_count
->whereHas('tags', fn ($q) => $q->where('name', 'laravel'))
->latest()->get();
foreach ($posts as $p) echo "{$p->title} by {$p->author->name} ({$p->comments_count})";
$post->tags()->sync([1, 3]); // pivot rows now exactly 1 and 3
$user->posts()->create(['title' => 'Hi']); // user_id set automaticallyRemember
- Relationship as property = loaded results; as method = query builder
- with() eager-loads relations in one query each, fixing N+1
- preventLazyLoading in development surfaces N+1 bugs early
Watch out for
- Calling $post->comments()->count() inside a loop; use withCount on the parent query.