I Made My Laravel API 83% Faster by Rethinking Database Queries

I Made My Laravel API 83% Faster by Rethinking Database Queries

The Problem Everyone Knows ## The "Aha!" Moment ## The Solution: JSON Aggregation ## The Results ## Why Is It So Fast? ## 1. Network Latency (80% of the gain) ## 2. Avoiding Eloquent Hydration (15% of the gain) ## 3. Optimized SQL (5% of the gain) ## Real-World Impact ## How It Works ## Installation ## Output ## When Should You Use This? ## ✅ Perfect for: ## ⚠️ Not recommended for: ## Performance vs. Eloquent Models ## Trade-offs ## What's Next? ## Try It Out! How I solved the N+1 query problem using JSON aggregation instead of traditional eager loading tags: laravel, php, performance, database. Last month, I was debugging a slow admin dashboard. The page loaded 500 partner records with their profiles, countries, and promotional codes. Each page load took over 2 seconds. The culprit? The classic N+1 query problem. Even with Laravel's eager loading, I was still hitting the database 5 times per request: Each query means another round-trip to the database. With 50 records, that's 4 network round-trips, adding 15-20ms of latency each. I asked myself: "Can we load everything in ONE query?" That's when I remembered MySQL's JSON_OBJECT and JSON_ARRAYAGG functions. What if instead of multiple queries, we could aggregate all relations into JSON directly in SQL? I built a Laravel package that does exactly this: This generates a single optimized query: One query. All the data. I ran benchmarks on a dataset of 2,000 partners with 4 relations each, fetching 50 records: That's not a typo. 83% faster. The performance gain comes from three factors: Database round-trips are expensive. Even on localhost, each query adds 5-10ms. On a remote database? 15-20ms each. Before: 4 queries × 15ms = 60ms in network time After: 1 query × 15ms = 15ms By returning arrays instead of Eloquent models, we skip: The database does the aggregation work using highly optimized C code instead of PHP loops. On my dashboard handling 10,000 API requests per day: Add the trait to your model: The data structure is predictable and clean: The package offers two modes: Array mode is fastest because it skips Eloquent's hydration overhead. But even in Eloquent mode, you still save 1 database query, which gives a significant boost. Let's be honest about limitations: For read-heavy operations like APIs and dashboards, this trade-off is absolutely worth it. I'm currently working on v1.1.0 with: If you're building APIs or dashboards with Laravel, give it a try: 🔗 GitHub: rgalstyan/laravel-aggregated-queries 📦 Packagist: composer require rgalstyan/laravel-aggregated-queries I'd love to hear your results! Have you tried optimizing N+1 queries in other ways? Drop a comment below. P.S. The package has been featured in Laravel News! If you find it useful, a GitHub star would mean a lot ⭐ Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to ? It will become hidden in your post, but will still be visible via the comment's permalink. as well , this person and/or CODE_BLOCK: $partners = Partner::with(['profile', 'country', 'promocodes'])->get(); CODE_BLOCK: $partners = Partner::with(['profile', 'country', 'promocodes'])->get(); CODE_BLOCK: $partners = Partner::with(['profile', 'country', 'promocodes'])->get(); CODE_BLOCK: SELECT * FROM partners -- Query 1 SELECT * FROM profiles WHERE partner_id IN... -- Query 2 SELECT * FROM countries WHERE id IN... -- Query 3 SELECT * FROM promocodes WHERE partner_id IN...-- Query 4 CODE_BLOCK: SELECT * FROM partners -- Query 1 SELECT * FROM profiles WHERE partner_id IN... -- Query 2 SELECT * FROM countries WHERE id IN... -- Query 3 SELECT * FROM promocodes WHERE partner_id IN...-- Query 4 CODE_BLOCK: SELECT * FROM partners -- Query 1 SELECT * FROM profiles WHERE partner_id IN... -- Query 2 SELECT * FROM countries WHERE id IN... -- Query 3 SELECT * FROM promocodes WHERE partner_id IN...-- Query 4 CODE_BLOCK: $partners = Partner::aggregatedQuery() ->withJsonRelation('profile') ->withJsonRelation('country') ->withJsonCollection('promocodes') ->get(); CODE_BLOCK: $partners = Partner::aggregatedQuery() ->withJsonRelation('profile') ->withJsonRelation('country') ->withJsonCollection('promocodes') ->get(); CODE_BLOCK: $partners = Partner::aggregatedQuery() ->withJsonRelation('profile') ->withJsonRelation('country') ->withJsonCollection('promocodes') ->get(); CODE_BLOCK: SELECT base.*, JSON_OBJECT('id', profile.id, 'name', profile.name) AS profile, JSON_OBJECT('id', country.id, 'name', country.name) AS country, (SELECT JSON_ARRAYAGG(JSON_OBJECT('id', id, 'code', code)) FROM promocodes WHERE partner_id = base.id) AS promocodes FROM partners base LEFT JOIN profiles profile ON profile.partner_id = base.id LEFT JOIN countries country ON country.id = base.country_id CODE_BLOCK: SELECT base.*, JSON_OBJECT('id', profile.id, 'name', profile.name) AS profile, JSON_OBJECT('id', country.id, 'name', country.name) AS country, (SELECT JSON_ARRAYAGG(JSON_OBJECT('id', id, 'code', code)) FROM promocodes WHERE partner_id = base.id) AS promocodes FROM partners base LEFT JOIN profiles profile ON profile.partner_id = base.id LEFT JOIN countries country ON country.id = base.country_id CODE_BLOCK: SELECT base.*, JSON_OBJECT('id', profile.id, 'name', profile.name) AS profile, JSON_OBJECT('id', country.id, 'name', country.name) AS country, (SELECT JSON_ARRAYAGG(JSON_OBJECT('id', id, 'code', code)) FROM promocodes WHERE partner_id = base.id) AS promocodes FROM partners base LEFT JOIN profiles profile ON profile.partner_id = base.id LEFT JOIN countries country ON country.id = base.country_id CODE_BLOCK: composer require rgalstyan/laravel-aggregated-queries CODE_BLOCK: composer require rgalstyan/laravel-aggregated-queries CODE_BLOCK: composer require rgalstyan/laravel-aggregated-queries CODE_BLOCK: use Rgalstyan\LaravelAggregatedQueries\HasAggregatedQueries; class Partner extends Model { use HasAggregatedQueries; public function profile() { return $this->hasOne(PartnerProfile::class); } public function promocodes() { return $this->hasMany(PartnerPromocode::class); } } CODE_BLOCK: use Rgalstyan\LaravelAggregatedQueries\HasAggregatedQueries; class Partner extends Model { use HasAggregatedQueries; public function profile() { return $this->hasOne(PartnerProfile::class); } public function promocodes() { return $this->hasMany(PartnerPromocode::class); } } CODE_BLOCK: use Rgalstyan\LaravelAggregatedQueries\HasAggregatedQueries; class Partner extends Model { use HasAggregatedQueries; public function profile() { return $this->hasOne(PartnerProfile::class); } public function promocodes() { return $this->hasMany(PartnerPromocode::class); } } CODE_BLOCK: // Instead of this: $partners = Partner::with(['profile', 'promocodes'])->get(); // Do this: $partners = Partner::aggregatedQuery() ->withJsonRelation('profile', ['id', 'name', 'email']) ->withJsonCollection('promocodes', ['id', 'code', 'discount']) ->where('is_active', true) ->get(); CODE_BLOCK: // Instead of this: $partners = Partner::with(['profile', 'promocodes'])->get(); // Do this: $partners = Partner::aggregatedQuery() ->withJsonRelation('profile', ['id', 'name', 'email']) ->withJsonCollection('promocodes', ['id', 'code', 'discount']) ->where('is_active', true) ->get(); CODE_BLOCK: // Instead of this: $partners = Partner::with(['profile', 'promocodes'])->get(); // Do this: $partners = Partner::aggregatedQuery() ->withJsonRelation('profile', ['id', 'name', 'email']) ->withJsonCollection('promocodes', ['id', 'code', 'discount']) ->where('is_active', true) ->get(); COMMAND_BLOCK: [ 'id' => 1, 'name' => 'Partner A', 'profile' => [ 'id' => 10, 'name' => 'John', 'email' => '[email protected]' ], 'promocodes' => [ ['id' => 1, 'code' => 'SAVE10'], ['id' => 2, 'code' => 'SAVE20'] ] ] COMMAND_BLOCK: [ 'id' => 1, 'name' => 'Partner A', 'profile' => [ 'id' => 10, 'name' => 'John', 'email' => '[email protected]' ], 'promocodes' => [ ['id' => 1, 'code' => 'SAVE10'], ['id' => 2, 'code' => 'SAVE20'] ] ] COMMAND_BLOCK: [ 'id' => 1, 'name' => 'Partner A', 'profile' => [ 'id' => 10, 'name' => 'John', 'email' => '[email protected]' ], 'promocodes' => [ ['id' => 1, 'code' => 'SAVE10'], ['id' => 2, 'code' => 'SAVE20'] ] ] CODE_BLOCK: // Array mode (default, fastest - 83% faster) $partners = Partner::aggregatedQuery()->get(); // Eloquent mode (still faster - 27% improvement) $partners = Partner::aggregatedQuery()->get('eloquent'); CODE_BLOCK: // Array mode (default, fastest - 83% faster) $partners = Partner::aggregatedQuery()->get(); // Eloquent mode (still faster - 27% improvement) $partners = Partner::aggregatedQuery()->get('eloquent'); CODE_BLOCK: // Array mode (default, fastest - 83% faster) $partners = Partner::aggregatedQuery()->get(); // Eloquent mode (still faster - 27% improvement) $partners = Partner::aggregatedQuery()->get('eloquent'); - Object instantiation - Attribute casting - Relationship binding - Event firing - 40,000 fewer database queries - 4 minutes saved in total response time - 19GB less memory usage - Better server resource utilization - Relations are array or null (never undefined) - Collections are always array (never null) - No surprises in your code - API endpoints with multiple relations - Admin dashboards with complex queries - Mobile backends where every millisecond counts - Read-heavy applications (90%+ reads) - High-traffic services needing optimization - Write operations (use standard Eloquent) - When you need model events/observers - Complex nested relations (coming in v1.1!) - Model events (no created, updated, deleted events) - Accessors/mutators (in array mode) - The ability to call save() or update() (read-only) - 83% faster response times - 91% less memory usage - Simpler, more predictable data structures - Better scalability - Nested relations (profile.company.country) - Conditional loading with query constraints - Relation aliasing - Enhanced debugging tools