<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Raz Galstyan</title>
    <description>The latest articles on DEV Community by Raz Galstyan (@rgalstyan).</description>
    <link>https://dev.to/rgalstyan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3662274%2F1da29b00-ac8e-4530-8d5a-9cd16fa6935c.png</url>
      <title>DEV Community: Raz Galstyan</title>
      <link>https://dev.to/rgalstyan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rgalstyan"/>
    <language>en</language>
    <item>
      <title>JOIN FETCH can be slower than N+1: a reproducible Doctrine benchmark (+ 1-row-per-entity JSON aggregation)</title>
      <dc:creator>Raz Galstyan</dc:creator>
      <pubDate>Thu, 18 Dec 2025 05:54:04 +0000</pubDate>
      <link>https://dev.to/rgalstyan/join-fetch-can-be-slower-than-n1-a-reproducible-doctrine-benchmark-1-row-per-entity-json-1hd9</link>
      <guid>https://dev.to/rgalstyan/join-fetch-can-be-slower-than-n1-a-reproducible-doctrine-benchmark-1-row-per-entity-json-1hd9</guid>
      <description>&lt;p&gt;If you’ve ever built a “product listing” in Symfony/Doctrine with multiple relations (category, brand, images, reviews, counts…), you’ve probably hit one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;classic N+1&lt;/li&gt;
&lt;li&gt;“EAGER” collections still triggering extra queries&lt;/li&gt;
&lt;li&gt;heavy entity hydration cost&lt;/li&gt;
&lt;li&gt;or JOIN FETCH that &lt;em&gt;looks&lt;/em&gt; like “2 queries” but becomes slower in practice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post is about &lt;strong&gt;read-heavy, DTO-style endpoints&lt;/strong&gt; (arrays / API responses), where you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;predictable query count&lt;/li&gt;
&lt;li&gt;predictable runtime&lt;/li&gt;
&lt;li&gt;stable scaling when multiple &lt;code&gt;OneToMany&lt;/code&gt; relations are involved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s not a Doctrine replacement — it’s an &lt;strong&gt;explicit read-model escape hatch&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real problem: multiple OneToMany JOINs explode rows
&lt;/h2&gt;

&lt;p&gt;JOINing multiple &lt;code&gt;OneToMany&lt;/code&gt; relations multiplies the SQL result set (cartesian product).&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 images × 5 reviews = &lt;strong&gt;15 rows per product&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;for 2000 products =&amp;gt; &lt;strong&gt;~30,000 rows&lt;/strong&gt; transferred from DB and processed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doctrine’s identity map hides duplicates &lt;em&gt;in PHP&lt;/em&gt;, but the DB still returns the multiplied rowset.&lt;br&gt;
That can kill performance even when query count is low.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links (package + reproducible benchmark)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Symfony Aggregated Queries (the bundle):&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/rgalstyan/symfony-aggregated-queries" rel="noopener noreferrer"&gt;https://github.com/rgalstyan/symfony-aggregated-queries&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reproducible benchmark project (Symfony app that uses the bundle):&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/rgalstyan/doctrine-aggregated-queries-benchmark" rel="noopener noreferrer"&gt;https://github.com/rgalstyan/doctrine-aggregated-queries-benchmark&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only want to &lt;strong&gt;run the benchmark&lt;/strong&gt;, go to the benchmark repo.&lt;br&gt;&lt;br&gt;
If you want to &lt;strong&gt;use the package in your project&lt;/strong&gt;, go to the bundle repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproducible benchmark (Symfony + PostgreSQL)
&lt;/h2&gt;

&lt;p&gt;I published a standalone Symfony benchmark project with fixtures and a CLI command so anyone can reproduce the results locally:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/rgalstyan/doctrine-aggregated-queries-benchmark" rel="noopener noreferrer"&gt;https://github.com/rgalstyan/doctrine-aggregated-queries-benchmark&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Sample run (limit=2000)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: timings depend on your machine/DB/cache state. The trend is what matters.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
txt
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRODUCTS PERFORMANCE TEST
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Dataset size: 2000 products

TRADITIONAL DOCTRINE (2000 records)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time:    327.73ms
Memory:  44559.6 KB (43.52 MB)
Queries: 43
Result:  2000 Product entities

DOCTRINE JOIN FETCH (entities) (2000 records)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time:    816.37ms
Memory:  39795.9 KB (38.86 MB)
Queries: 2
DB rows: ~30000 (Cartesian product in SQL)
Result:  2000 Product entities

SIMPLE JOINS (naive) (2000 records)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time:    103.14ms
Memory:  11659.4 KB (11.39 MB)
Queries: 1
DB rows: 30000 (Cartesian product!)
Result:  2000 products (after deduplication)

AGGREGATED QUERIES (2000 records)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time:    66.18ms
Memory:  13989.1 KB (13.66 MB)
Queries: 1
DB rows: 2000
Result:  2000 products (arrays)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
COMPARISON
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Approach                   | Return   |  Time (ms) |     Mem (KB) | Queries |    DB rows | Products
───────────────────────────────────────────────────────────────────────────────────────────────────
1) Traditional Doctrine    | entities |     327.73 |      44559.6 |      43 |        N/A |     2000
2) Doctrine JOIN fetch     | entities |     816.37 |      39795.9 |       2 |      30000 |     2000
3) Simple JOINs (naive)    | arrays   |     103.14 |      11659.4 |       1 |      30000 |     2000
4) JSON aggregation        | arrays   |      66.18 |      13989.1 |       1 |       2000 |     2000
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>performance</category>
      <category>database</category>
      <category>architecture</category>
      <category>php</category>
    </item>
    <item>
      <title>I Made My Laravel API 83% Faster by Rethinking Database Queries</title>
      <dc:creator>Raz Galstyan</dc:creator>
      <pubDate>Mon, 15 Dec 2025 06:36:45 +0000</pubDate>
      <link>https://dev.to/rgalstyan/i-made-my-laravel-api-83-faster-by-rethinking-database-queries-23jh</link>
      <guid>https://dev.to/rgalstyan/i-made-my-laravel-api-83-faster-by-rethinking-database-queries-23jh</guid>
      <description>&lt;p&gt;How I solved the N+1 query problem using JSON aggregation instead of traditional eager loading&lt;br&gt;
tags: laravel, php, performance, database.&lt;/p&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The culprit? The classic N+1 query problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem Everyone Knows
&lt;/h2&gt;

&lt;p&gt;Even with Laravel's eager loading, I was still hitting the database 5 times per request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$partners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Partner&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'profile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'country'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'promocodes'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;partners&lt;/span&gt;                        &lt;span class="c1"&gt;-- Query 1&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;partner_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;-- Query 2  &lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;countries&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;        &lt;span class="c1"&gt;-- Query 3&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;promocodes&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;partner_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="c1"&gt;-- Query 4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each query means another round-trip to the database. With 50 records, that's &lt;strong&gt;4 network round-trips&lt;/strong&gt;, adding 15-20ms of latency each.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Aha!" Moment
&lt;/h2&gt;

&lt;p&gt;I asked myself: &lt;strong&gt;"Can we load everything in ONE query?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's when I remembered MySQL's &lt;code&gt;JSON_OBJECT&lt;/code&gt; and &lt;code&gt;JSON_ARRAYAGG&lt;/code&gt; functions. What if instead of multiple queries, we could aggregate all relations into JSON directly in SQL?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: JSON Aggregation
&lt;/h2&gt;

&lt;p&gt;I built a Laravel package that does exactly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$partners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Partner&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;aggregatedQuery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withJsonRelation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'profile'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withJsonRelation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'country'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withJsonCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'promocodes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a &lt;strong&gt;single optimized query&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JSON_ARRAYAGG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JSON_OBJECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'code'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
     &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;promocodes&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;partner_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;promocodes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;partners&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partner_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;countries&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One query. All the data.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;I ran benchmarks on a dataset of 2,000 partners with 4 relations each, fetching 50 records:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;th&gt;Queries&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard Eloquent&lt;/td&gt;
&lt;td&gt;27.44ms&lt;/td&gt;
&lt;td&gt;2.06MB&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON Aggregation&lt;/td&gt;
&lt;td&gt;4.41ms&lt;/td&gt;
&lt;td&gt;0.18MB&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Improvement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;83% faster&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;91% less&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;75% fewer&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's not a typo. &lt;strong&gt;83% faster.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Is It So Fast?
&lt;/h2&gt;

&lt;p&gt;The performance gain comes from three factors:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Network Latency (80% of the gain)
&lt;/h3&gt;

&lt;p&gt;Database round-trips are expensive. Even on localhost, each query adds 5-10ms. On a remote database? 15-20ms each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; 4 queries × 15ms = 60ms in network time&lt;br&gt;&lt;br&gt;
&lt;strong&gt;After:&lt;/strong&gt; 1 query × 15ms = 15ms&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Avoiding Eloquent Hydration (15% of the gain)
&lt;/h3&gt;

&lt;p&gt;By returning arrays instead of Eloquent models, we skip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Object instantiation&lt;/li&gt;
&lt;li&gt;Attribute casting&lt;/li&gt;
&lt;li&gt;Relationship binding&lt;/li&gt;
&lt;li&gt;Event firing&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  3. Optimized SQL (5% of the gain)
&lt;/h3&gt;

&lt;p&gt;The database does the aggregation work using highly optimized C code instead of PHP loops.&lt;/p&gt;
&lt;h2&gt;
  
  
  Real-World Impact
&lt;/h2&gt;

&lt;p&gt;On my dashboard handling 10,000 API requests per day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;40,000 fewer database queries&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4 minutes saved in total response time&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;19GB less memory usage&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better server resource utilization&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require rgalstyan/laravel-aggregated-queries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;Add the trait to your model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Rgalstyan\LaravelAggregatedQueries\HasAggregatedQueries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Partner&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasAggregatedQueries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PartnerProfile&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;promocodes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PartnerPromocode&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instead of this:&lt;/span&gt;
&lt;span class="nv"&gt;$partners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Partner&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'profile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'promocodes'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Do this:&lt;/span&gt;
&lt;span class="nv"&gt;$partners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Partner&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;aggregatedQuery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withJsonRelation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'profile'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withJsonCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'promocodes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'code'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'discount'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'is_active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Output
&lt;/h3&gt;

&lt;p&gt;The data structure is predictable and clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Partner A'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'profile'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'email'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'john@example.com'&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'promocodes'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'code'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'SAVE10'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'code'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'SAVE20'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Guaranteed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Relations are &lt;code&gt;array&lt;/code&gt; or &lt;code&gt;null&lt;/code&gt; (never undefined)&lt;/li&gt;
&lt;li&gt;Collections are always &lt;code&gt;array&lt;/code&gt; (never &lt;code&gt;null&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;No surprises in your code&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Should You Use This?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Perfect for:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API endpoints&lt;/strong&gt; with multiple relations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin dashboards&lt;/strong&gt; with complex queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile backends&lt;/strong&gt; where every millisecond counts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-heavy applications&lt;/strong&gt; (90%+ reads)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-traffic services&lt;/strong&gt; needing optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⚠️ Not recommended for:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Write operations (use standard Eloquent)&lt;/li&gt;
&lt;li&gt;When you need model events/observers&lt;/li&gt;
&lt;li&gt;Complex nested relations (coming in v1.1!)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance vs. Eloquent Models
&lt;/h2&gt;

&lt;p&gt;The package offers two modes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Array mode (default, fastest - 83% faster)&lt;/span&gt;
&lt;span class="nv"&gt;$partners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Partner&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;aggregatedQuery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Eloquent mode (still faster - 27% improvement)&lt;/span&gt;
&lt;span class="nv"&gt;$partners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Partner&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;aggregatedQuery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'eloquent'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;Let's be honest about limitations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you lose:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model events (no &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;updated&lt;/code&gt;, &lt;code&gt;deleted&lt;/code&gt; events)&lt;/li&gt;
&lt;li&gt;Accessors/mutators (in array mode)&lt;/li&gt;
&lt;li&gt;The ability to call &lt;code&gt;save()&lt;/code&gt; or &lt;code&gt;update()&lt;/code&gt; (read-only)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What you gain:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;83% faster response times&lt;/li&gt;
&lt;li&gt;91% less memory usage&lt;/li&gt;
&lt;li&gt;Simpler, more predictable data structures&lt;/li&gt;
&lt;li&gt;Better scalability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For read-heavy operations like APIs and dashboards, this trade-off is absolutely worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;I'm currently working on v1.1.0 with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested relations&lt;/strong&gt; (&lt;code&gt;profile.company.country&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional loading&lt;/strong&gt; with query constraints&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relation aliasing&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enhanced debugging tools&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Out!
&lt;/h2&gt;

&lt;p&gt;If you're building APIs or dashboards with Laravel, give it a try:&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/rgalstyan/laravel-aggregated-queries" rel="noopener noreferrer"&gt;rgalstyan/laravel-aggregated-queries&lt;/a&gt;&lt;br&gt;&lt;br&gt;
📦 &lt;strong&gt;Packagist:&lt;/strong&gt; &lt;code&gt;composer require rgalstyan/laravel-aggregated-queries&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear your results! Have you tried optimizing N+1 queries in other ways? Drop a comment below.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. The package has been featured in Laravel News! If you find it useful, a GitHub star would mean a lot ⭐&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>performance</category>
      <category>laravel</category>
      <category>php</category>
    </item>
  </channel>
</rss>
