<?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: diata</title>
    <description>The latest articles on DEV Community by diata (@diata0210).</description>
    <link>https://dev.to/diata0210</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%2F3895174%2Fab1b3ff4-7a5b-47d2-9b28-6e2832259ed9.jpg</url>
      <title>DEV Community: diata</title>
      <link>https://dev.to/diata0210</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/diata0210"/>
    <language>en</language>
    <item>
      <title>An index made our query faster. It slowly suffocated our database.</title>
      <dc:creator>diata</dc:creator>
      <pubDate>Fri, 24 Apr 2026 03:19:59 +0000</pubDate>
      <link>https://dev.to/diata0210/an-index-made-our-query-faster-it-slowly-suffocated-our-database-2emn</link>
      <guid>https://dev.to/diata0210/an-index-made-our-query-faster-it-slowly-suffocated-our-database-2emn</guid>
      <description>&lt;p&gt;Hello, I'm Tuan.&lt;/p&gt;

&lt;p&gt;When backend engineers encounter a slow query, the first instinct is often something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Check the WHERE and ORDER BY, then just add a composite index."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I used to think the same way.&lt;/p&gt;

&lt;p&gt;And to be fair, in many cases, that approach works perfectly fine.&lt;/p&gt;

&lt;p&gt;But once, a seemingly correct optimization turned into a production incident. The read query became significantly faster, the EXPLAIN plan looked clean, and everything seemed perfect.&lt;/p&gt;

&lt;p&gt;Yet slowly, the entire production system began to degrade.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database CPU usage spiked.&lt;/li&gt;
&lt;li&gt;Disk I/O increased dramatically.&lt;/li&gt;
&lt;li&gt;API latency crept upward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It took me a while to realize the real problem:&lt;/p&gt;

&lt;p&gt;I optimized the read path, but completely ignored the write cost.&lt;/p&gt;

&lt;p&gt;If you're about to run &lt;code&gt;CREATE INDEX&lt;/code&gt; to save a slow API, take a few minutes to read this first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Problem
&lt;/h2&gt;

&lt;p&gt;One day, the product team asked for a simple feature:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Create an API that returns the top 20 hottest products in a category."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Essentially, a real-time trending ranking.&lt;/p&gt;

&lt;p&gt;At first glance, the solution seemed trivial — just sort products by a score and return the top 20.&lt;/p&gt;

&lt;p&gt;The products table already had around 10 million rows, and traffic was already in the thousands of requests per second. Since this API would appear in a highly visible part of the product, slow responses were not acceptable.&lt;/p&gt;

&lt;p&gt;My thinking at the time was straightforward:&lt;/p&gt;

&lt;p&gt;Just add the right index and it will be fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Perfectly Correct" Optimization
&lt;/h2&gt;

&lt;p&gt;The query looked like this:&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;p&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;p&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="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interest_score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&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;'ACTIVE'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stock_quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interest_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this on a table with millions of rows would cause a full scan and sort, which obviously wouldn't scale.&lt;/p&gt;

&lt;p&gt;So I applied the classic solution:&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_products_category_status_score&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interest_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results looked fantastic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The query became dramatically faster&lt;/li&gt;
&lt;li&gt;The EXPLAIN plan looked perfect&lt;/li&gt;
&lt;li&gt;Response time dropped immediately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the perspective of query performance, everything seemed solved. At that moment, I felt pretty confident about the fix.&lt;/p&gt;

&lt;p&gt;Unfortunately, that confidence didn't last long.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Production Started Acting Strange
&lt;/h2&gt;

&lt;p&gt;The issue was something I completely overlooked.&lt;/p&gt;

&lt;p&gt;interest_score was not a static column.&lt;/p&gt;

&lt;p&gt;Every time users interacted with a product — viewing details, liking it, or adding it to the cart — the score increased. Something like this happened constantly:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;interest_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;interest_score&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, this seemed harmless. Incrementing a number is one of the most common operations in any system.&lt;/p&gt;

&lt;p&gt;But the moment interest_score became part of an index, that simple update was no longer simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  The System Didn't Crash — It Slowly Suffocated
&lt;/h2&gt;

&lt;p&gt;The worst kind of production issue is the one that doesn't fail loudly.&lt;/p&gt;

&lt;p&gt;There were no crashes. No obvious errors. The system just became slower and slower.&lt;/p&gt;

&lt;p&gt;Over time we observed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API latency gradually increased&lt;/li&gt;
&lt;li&gt;Database CPU usage spiked&lt;/li&gt;
&lt;li&gt;Disk I/O skyrocketed&lt;/li&gt;
&lt;li&gt;Some requests started timing out&lt;/li&gt;
&lt;li&gt;Slow query logs filled with UPDATE statements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Initially we blamed traffic growth. After all, the SELECT query was indexed and looked perfectly fine.&lt;/p&gt;

&lt;p&gt;But after monitoring the system closely, the real culprit finally became clear — the heavy load was coming from the updates to interest_score.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem
&lt;/h2&gt;

&lt;p&gt;The index itself was not wrong. The real issue was the hidden write cost.&lt;/p&gt;

&lt;p&gt;Whenever interest_score changes, the database cannot simply update a number in place. Because the column participates in an index used for sorting, the database must also maintain the index structure.&lt;/p&gt;

&lt;p&gt;Conceptually, it means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The record must be removed from its old position in the index and reinserted into a new one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With a few updates, this is trivial. But when thousands of updates per second hit the system, maintaining that index becomes extremely expensive.&lt;/p&gt;

&lt;p&gt;In other words: the index optimized reads, but it dramatically increased the cost of writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hotspot Problem
&lt;/h2&gt;

&lt;p&gt;User interactions are not evenly distributed. Popular products receive far more clicks than others.&lt;/p&gt;

&lt;p&gt;That meant many updates were hitting the same rows repeatedly, creating contention inside the database. Even though the code looked harmless:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;interest_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;interest_score&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, the database was handling heavy concurrent updates to the same regions of data and index pages. The system was effectively fighting itself.&lt;/p&gt;

&lt;p&gt;That was the moment I realized something important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An index that speeds up a query does not necessarily make the system healthier.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And in hindsight, the real design mistake was trying to make the main transactional table handle real-time ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Decision: Removing the Index
&lt;/h2&gt;

&lt;p&gt;Removing the index felt wrong at first. After all, it had significantly improved the query performance.&lt;/p&gt;

&lt;p&gt;But the metrics were clear. As long as that index existed, write contention would remain.&lt;/p&gt;

&lt;p&gt;So we removed it.&lt;/p&gt;

&lt;p&gt;The result was immediate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write pressure dropped significantly&lt;/li&gt;
&lt;li&gt;Disk I/O stabilized&lt;/li&gt;
&lt;li&gt;Database CPU usage returned to normal levels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ranking query itself became slower again, but at least the entire system was no longer being dragged down by a single column update.&lt;/p&gt;

&lt;p&gt;That moment taught me an important lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Some problems that look like SQL optimization tasks are actually architecture problems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Alternative: Rethinking the Architecture
&lt;/h2&gt;

&lt;p&gt;Instead of forcing the database to handle both persistent data and real-time ranking, we split responsibilities.&lt;/p&gt;

&lt;p&gt;Score updates were moved to Redis Sorted Sets.&lt;/p&gt;

&lt;p&gt;When user actions occur, we increment the score in Redis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ZINCRBY trending:cat:42 1 12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new flow became simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User action updates score in Redis&lt;/li&gt;
&lt;li&gt;When ranking is needed, fetch the top IDs from Redis&lt;/li&gt;
&lt;li&gt;Fetch product details from the database using id IN (...)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allowed each system to focus on what it does best:&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%2F9yfu7k0ld28w0zr9v6yc.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%2F9yfu7k0ld28w0zr9v6yc.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, this design also came with trade-offs. Redis might return products that are out of stock or inactive — so we had to fetch slightly more results and filter them in the database. We also accepted eventual consistency instead of perfect real-time synchronization.&lt;/p&gt;

&lt;p&gt;But overall, the system became far more scalable and stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Since that incident, I approach slow queries very differently.&lt;/p&gt;

&lt;p&gt;Before adding an index, I now ask myself a few questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this column frequently updated?&lt;/li&gt;
&lt;li&gt;How much write overhead will this index introduce?&lt;/li&gt;
&lt;li&gt;Am I optimizing a query, or optimizing the entire workload?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For highly dynamic values like ranking scores, like counts, and view counts — I avoid updating the main transactional table directly. More often than not, the real bottleneck is not SQL syntax, but choosing the right system for the workload.&lt;/p&gt;

&lt;p&gt;That composite index wasn't technically wrong. But in the context of our production traffic, it was the wrong decision.&lt;/p&gt;

&lt;p&gt;And today, I care less about whether a query becomes faster. I care more about this question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this change actually make the whole system healthier?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because in production systems, correctness is not defined by the speed of a single query. It is defined by how the entire system behaves under real traffic.&lt;/p&gt;

&lt;p&gt;If you found this helpful, follow me for more deep dives into Backend Architecture.&lt;/p&gt;

</description>
      <category>database</category>
      <category>backend</category>
      <category>postgres</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
