<?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: Ahmad Fauzan Alghifari</title>
    <description>The latest articles on DEV Community by Ahmad Fauzan Alghifari (@fazghfr).</description>
    <link>https://dev.to/fazghfr</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%2F3877662%2F0534252a-653d-417d-9e10-e1ce5a6194b2.jpeg</url>
      <title>DEV Community: Ahmad Fauzan Alghifari</title>
      <link>https://dev.to/fazghfr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fazghfr"/>
    <language>en</language>
    <item>
      <title>How I Fixed a Correlated Subquery That Was Behaving Like an N+1 Problem in Production</title>
      <dc:creator>Ahmad Fauzan Alghifari</dc:creator>
      <pubDate>Tue, 14 Apr 2026 07:36:39 +0000</pubDate>
      <link>https://dev.to/fazghfr/how-i-fixed-a-correlated-subquery-that-was-behaving-like-an-n1-problem-in-production-abn</link>
      <guid>https://dev.to/fazghfr/how-i-fixed-a-correlated-subquery-that-was-behaving-like-an-n1-problem-in-production-abn</guid>
      <description>&lt;h2&gt;
  
  
  What Happened
&lt;/h2&gt;

&lt;p&gt;A user reported seeing no data after an absurdly long loading time in one of the procurement menus. I was maintaining an e-procurement system for a state-owned enterprise subsidiary in the mining sector and this wasn't just a UI glitch. It completely blocked the user's business process. &lt;/p&gt;

&lt;p&gt;From the network tab, the problematic request was just sitting there with endless pending status, until it hit the server's timeout and resulted in 504, effectively sending no data to the user.&lt;/p&gt;

&lt;p&gt;The culprit turned out to be a correlated subquery behaving exactly like an N+1 problem: common, easy to miss, and quietly fatal in production. In this post, I'll walk through how I identified the root cause and fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an N+1 Query Problem?
&lt;/h2&gt;

&lt;p&gt;Imagine you have to fetch 500 data from the database. And then, for each of that row, you run another query to compute something like aggregations. That's 1 (the initial fetching) query plus 500 query for each computation. In production, because the data will keep growing, this would be a fatal problem.&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;// CLASSIC ORM N+1 EXAMPLE&lt;/span&gt;
&lt;span class="c1"&gt;// 1 query&lt;/span&gt;
&lt;span class="nv"&gt;$orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// N queries — one per order&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orders&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;supplier&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// hits the database every single iteration&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This commonly happens when we use ORM loops like in laravel. But N+1 problem does not happen only to ORMs. Something similar could also happen to the database query itself. Which is exactly what happened in my case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Happened
&lt;/h2&gt;

&lt;p&gt;This is a snippet of a long scope code that was implemented with laravel.&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="mf"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="no"&gt;WHEN&lt;/span&gt; &lt;span class="nb"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;po_items&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;THEN&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="nf"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;po_items&lt;/span&gt;
     &lt;span class="no"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;po_items&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;po_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;po&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="no"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sum_val&lt;/span&gt;
&lt;span class="mf"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do you see the problem? Yup. Its computing an aggregation function SUM() within the select statement. So for each data that comes from the &lt;code&gt;FROM&lt;/code&gt; keyword, let's assume we have 1000 rows, it will run that exact SUM() query for another 1000 times. This will work on small data, but in production environment and database, this query won't survive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;leftjoin&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;po_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_value&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;sum_val&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;po_items&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'y'&lt;/span&gt;
  &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;po_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;po_item_aggs&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;po&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;po_item_aggs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;po_id&lt;/span&gt;

&lt;span class="c1"&gt;-- accessing it directly&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;po&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="n"&gt;po_item_aggs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum_val&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what I did. Instead of doing the aggregations and the subquery inside the select statement, I moved it into another Join operations. This alone removes the computation for each row from the data source. So instead of doing 1000+ query computation, we only did one query for the computation itself.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqci5m53juvglfp2fu37w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqci5m53juvglfp2fu37w.png" alt="Before fix — request pending, 504 timeout" width="418" height="206"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzuyo8e2wve16229d7h8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzuyo8e2wve16229d7h8v.png" alt="After fix — request resolves normally" width="241" height="182"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response time&lt;/td&gt;
&lt;td&gt;~2 minutes / timeout&lt;/td&gt;
&lt;td&gt;~15 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP status&lt;/td&gt;
&lt;td&gt;504 Gateway Timeout&lt;/td&gt;
&lt;td&gt;200 OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User experience&lt;/td&gt;
&lt;td&gt;Blank table, blocked workflow&lt;/td&gt;
&lt;td&gt;Normal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The request that was hanging for over two minutes — or not returning at all — now resolves in around 15 seconds. That's not just a performance improvement, it's the difference between a system that works and one that doesn't.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>backend</category>
      <category>sql</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
