beyondcode/laravel-query-detector
Detect N+1 query issues in Laravel during development. Monitors database queries in real time and alerts you when repeated queries suggest missing eager loading, helping you optimize performance and reduce unnecessary database calls.
Install the package in development:
composer require beyondcode/laravel-query-detector --dev
First use case: Run your Laravel app in debug mode (default). The package will automatically:
posts relation on Author model")Where to look first:
php artisan vendor:publish --provider="BeyondCode\QueryDetector\QueryDetectorServiceProvider")@foreach($authors as $author) {{ $author->posts }} @endforeach)Detection Phase:
debug mode (or enable via QUERY_DETECTOR_ENABLED=true).Alerting Phase:
Alert, Log, Debugbar) via config.laravel.log with details like:
[2024-05-20 12:00:00] local.DEBUG: N+1 Query Detected: "posts" relation on "App\Models\Author" (threshold: 1, query: "select * from `posts` where `posts`.`author_id` = ?")
Fixing Phase:
@foreach($authors as $author) {{ $author->posts }} with:
$authors = Author::with('posts')->get();
except config to whitelist relations that should trigger N+1 (e.g., lazy-loaded relations).Debugbar Integration:
Install barryvdh/laravel-debugbar and add \BeyondCode\QueryDetector\Outputs\Debugbar::class to the output array. N+1 queries appear in the "Messages" tab.
Clockwork Integration:
Add \BeyondCode\QueryDetector\Outputs\Clockwork::class and install itsgoingd/clockwork. Queries show in the "Queries" tab with a warning icon.
JSON Output for APIs:
Add \BeyondCode\QueryDetector\Outputs\Json::class to inject warnings into API responses:
{
"data": { ... },
"meta": {
"query_warnings": [
{
"relation": "posts",
"model": "App\Models\Author",
"query": "select * from `posts` where `posts`.`author_id` = ?",
"count": 5
}
]
}
}
Event Listeners:
Listen for \BeyondCode\QueryDetector\Events\QueryDetected to trigger custom actions (e.g., Slack alerts):
QueryDetected::dispatch($query, $relation, $model, $count);
Lumen Support:
Manually register the provider in bootstrap/app.php:
$app->register(\BeyondCode\QueryDetector\LumenQueryDetectorServiceProvider::class);
Dynamic Thresholds: Override the threshold per environment:
QUERY_DETECTOR_THRESHOLD=3 # Warn only if a relation is loaded >3 times
Whitelisting Complex Relations:
Whitelist nested relations (e.g., posts.comments):
'except' => [
Author::class => [
Post::class,
'posts',
'posts.comments',
],
],
CI/CD Enforcement:
Use the Log output in CI to fail builds with N+1 queries:
php artisan query:detect --fail-on-n1 # Hypothetical future CLI command
False Positives:
optional($user->profile)). Whitelist these relations in except.except:
'except' => [
User::class => [
Profile::class,
'profile',
],
],
Debugbar Conflicts:
Debugbar output class is updated to resolve the facade at runtime (handled in v2.3.0+).Lumen Quirks:
JSON Output Overhead:
Json output adds metadata to every API response. Disable it in production:
'output' => [
\BeyondCode\QueryDetector\Outputs\Log::class, // Only in dev
],
Backtrace Limitations:
Threshold Misconfiguration:
threshold=0 disables detection entirely. Use threshold=1 (default) to catch all N+1 queries.Inspect Queries:
Use telescope or laravel-debugbar to verify the actual queries being fired. Compare with the detector’s output to confirm false positives/negatives.
Console Output:
Add \BeyondCode\QueryDetector\Outputs\Console::class to see detailed warnings in the browser console, including:
posts)App\Models\Author)Event Debugging:
Listen to QueryDetected in AppServiceProvider to log raw event data:
public function boot()
{
\BeyondCode\QueryDetector\Events\QueryDetected::listen(function ($event) {
\Log::debug('Raw event:', $event->toArray());
});
}
Performance Impact: The detector adds ~1–5ms overhead per query. Disable it in production:
APP_DEBUG=false
QUERY_DETECTOR_ENABLED=false
Custom Output Channels:
Extend \BeyondCode\QueryDetector\Contracts\Output to create a new channel (e.g., Datadog, Sentry):
class SentryOutput implements Output
{
public function handle(QueryDetected $event)
{
Sentry::captureMessage(
sprintf('N+1 Query: %s on %s', $event->relation, $event->model),
'warning'
);
}
}
Query Filtering:
Override the shouldDetect method in the service provider to ignore specific queries (e.g., cache misses):
QueryDetector::shouldDetect(function ($query) {
return !str_contains($query->sql, 'cache:');
});
Relation Normalization:
Customize how relations are matched by extending the RelationResolver:
QueryDetector::extend(function ($detector) {
$detector->relationResolver = new CustomRelationResolver();
});
Multi-Tenant Support: Filter queries by tenant ID to avoid cross-tenant N+1 noise:
'except' => [
Tenant::class => [
User::class,
'users',
],
],
Pair with laravel-debugbar:
Use both packages to correlate N+1 queries with execution time, memory usage, and SQL query plans.
Automate Fixes:
Write a PHPStan rule or PSR-12 linter to detect missing with() clauses:
// Hypothetical rule: "Relation `$user->posts` is accessed without eager loading."
Benchmark Before/After:
Use laravel-debugbar to measure query count reduction:
- Queries: 50 (before)
+ Queries: 5 (after adding `with('posts')`)
Educate the Team: Add a Slack bot or GitHub template to notify devs when they introduce N+1 queries:
🚨 N+1 Query Detected in PR #123:
Relation `orders` on `Customer` loaded 10 times.
Fix: Add `->with('orders')` to the query.
How can I help you explore Laravel packages today?