laravel-database

Auto-invoked skill

Optimize queries, fix N+1 issues, design schemas with indexes

Trigger Keywords

This skill automatically activates when Claude detects these keywords:

migration query N+1 Big O O(n) complexity nested loop eloquent index database

Overview

The laravel-database skill provides expertise for database design, query optimization, and Eloquent best practices. It helps identify and fix N+1 problems, design efficient schemas, and write performant queries.

What This Skill Provides

  • Schema Design - Proper column types, indexes, foreign keys
  • N+1 Detection - Identify and fix eager loading issues
  • Big O Fixes - Detect O(n²) patterns and fix with O(1) lookups
  • Query Optimization - Efficient queries with proper indexing
  • Migrations - Safe, reversible database changes
  • Eloquent Patterns - Scopes, accessors, relationships
  • Raw Queries - When and how to use raw SQL safely

Example Conversations

# Fixing N+1 issues
"My page is making 100 queries to load 10 posts with their authors"

# Schema design
"Design a database schema for a multi-tenant e-commerce platform"

# Query optimization
"This query is slow - how can I optimize it?"

# Migration safety
"How do I add a column to a table with millions of rows without downtime?"

N+1 Problem & Solution

// BAD: N+1 queries (1 + N queries)
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->author->name; // Query for each post!
}

// GOOD: Eager loading (2 queries total)
$posts = Post::with('author')->get();
foreach ($posts as $post) {
    echo $post->author->name; // No additional query
}

// BETTER: Eager load nested relationships
$posts = Post::with(['author', 'comments.user'])->get();

// BEST: Constrain eager loads
$posts = Post::with(['comments' => function ($query) {
    $query->where('approved', true)->latest()->limit(5);
}])->get();

Big O Complexity Issues

Big O complexity issues cause exponential slowdowns as data grows. Detect and fix O(n²) patterns.

// BAD: O(n²) - Nested loops comparing all items
$users = User::all();
$orders = Order::all();
foreach ($users as $user) {
    foreach ($orders as $order) {
        if ($order->user_id === $user->id) {
            // Process - runs n×m times!
        }
    }
}

// GOOD: O(n) - Eager load relationships
$users = User::with('orders')->get();
foreach ($users as $user) {
    foreach ($user->orders as $order) {
        // Process - each order accessed once
    }
}

// GOOD: O(n) - Use groupBy for O(1) lookups
$ordersByUser = Order::all()->groupBy('user_id');
foreach ($users as $user) {
    $userOrders = $ordersByUser->get($user->id, collect());
}

// BAD: O(n²) - contains() is O(n), called n times
$existingEmails = User::pluck('email');
foreach ($newUsers as $userData) {
    if (!$existingEmails->contains($userData['email'])) {
        User::create($userData);
    }
}

// GOOD: O(n) - flip() for O(1) has() lookups
$existingEmails = User::pluck('email')->flip();
foreach ($newUsers as $userData) {
    if (!$existingEmails->has($userData['email'])) {
        User::create($userData);
    }
}
Pattern Bad Good
Nested loops O(n²) Eager load / keyBy → O(n)
In-loop queries O(n) queries Batch query → O(1) queries
contains() in loop O(n²) flip()/has() → O(n)
filter() in loop O(n×m) groupBy() → O(n+m)

Index Strategies

Index Type When to Use
$table->index('column') Columns in WHERE, ORDER BY, JOIN
$table->unique('email') Columns that must be unique
$table->index(['a', 'b']) Columns frequently queried together
$table->fullText('content') Text search columns

Migration Best Practices

// Safe column addition (nullable first, then backfill)
public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('timezone')->nullable()->after('email');
    });

    // Backfill in chunks to avoid memory issues
    User::query()->chunk(1000, function ($users) {
        foreach ($users as $user) {
            $user->update(['timezone' => 'UTC']);
        }
    });
}

// Safe column removal (make nullable first in separate deploy)
public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->dropColumn('legacy_field');
    });
}

Common Pitfalls

// 1. N+1 Queries - Always eager load relationships
// BAD
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->author->name; // N+1!
}

// GOOD
$posts = Post::with('author')->get();

// 2. Missing Indexes - Add indexes for WHERE and ORDER BY
Schema::table('orders', function (Blueprint $table) {
    $table->index(['user_id', 'created_at']);
    $table->index('status');
});

// 3. SELECT * - Only select needed columns
// BAD
$users = User::all();

// GOOD
$users = User::select(['id', 'name', 'email'])->get();

// 4. No Chunking - Use chunk() for large datasets
User::chunk(1000, function ($users) {
    foreach ($users as $user) {
        // Process
    }
});

// 5. No Foreign Keys - Always use constraints
$table->foreignId('user_id')->constrained()->cascadeOnDelete();

// 6. No Transactions - Wrap related operations
DB::transaction(function () {
    $order = Order::create([...]);
    $order->items()->createMany([...]);
});

Package Integration

  • beyondcode/laravel-query-detector - N+1 detection
  • barryvdh/laravel-debugbar - Query profiling
  • spatie/laravel-query-builder - API query building

Best Practices

  • Design indexes based on actual queries
  • Use foreign key constraints
  • Monitor slow queries in production
  • Use database transactions for related operations
  • Prevent lazy loading in development: Model::preventLazyLoading(!app()->isProduction())

Query Debugging

// Enable query log
DB::enableQueryLog();

// Run your code
$posts = Post::with('author')->get();

// Dump queries
dd(DB::getQueryLog());

// Or use Laravel Debugbar / Telescope in development

Related Commands

Related Agent