<?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: Yogendra Singh</title>
    <description>The latest articles on DEV Community by Yogendra Singh (@yogendra_singh_9737a86e38).</description>
    <link>https://dev.to/yogendra_singh_9737a86e38</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3393333%2Ff9339955-a8dd-4013-a276-a366af0a0fd4.png</url>
      <title>DEV Community: Yogendra Singh</title>
      <link>https://dev.to/yogendra_singh_9737a86e38</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yogendra_singh_9737a86e38"/>
    <language>en</language>
    <item>
      <title>Database Bloat Is a Silent Tax on Every Write-Heavy System — Here's What Nobody Tells You</title>
      <dc:creator>Yogendra Singh</dc:creator>
      <pubDate>Sun, 16 Aug 2026 17:27:03 +0000</pubDate>
      <link>https://dev.to/yogendra_singh_9737a86e38/database-bloat-is-a-silent-tax-on-every-write-heavy-system-heres-what-nobody-tells-you-196f</link>
      <guid>https://dev.to/yogendra_singh_9737a86e38/database-bloat-is-a-silent-tax-on-every-write-heavy-system-heres-what-nobody-tells-you-196f</guid>
      <description>&lt;p&gt;Every write-heavy system I've worked on eventually hits the same wall, and it's rarely the wall anyone budgeted time for.&lt;/p&gt;

&lt;p&gt;Not a traffic spike. Not a bad query someone forgot to index. Something quieter: the database itself, slowly getting heavier under its own weight, until one day a query that used to take milliseconds takes seconds, and nobody can immediately say why.&lt;/p&gt;

&lt;p&gt;That's bloat. And in 18 years of building payment and transaction systems, it's cost me more debugging hours than almost any other single category of problem — precisely because it doesn't announce itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens, in plain terms
&lt;/h2&gt;

&lt;p&gt;Most relational databases don't delete or update a row in place. Postgres marks the old row version as dead and writes a new one; MySQL's InnoDB does something conceptually similar with its own row versioning under MVCC. This is &lt;em&gt;by design&lt;/em&gt; — it's how databases give you consistent reads without locking half the table.&lt;/p&gt;

&lt;p&gt;The catch: those dead rows don't vanish. They sit there, taking up space in the table and every index on it, until something goes and reclaims them. In Postgres, that's &lt;code&gt;VACUUM&lt;/code&gt;. In InnoDB, it's a mix of purge threads and, eventually, a table rebuild if things get bad enough.&lt;/p&gt;

&lt;p&gt;If your write volume is high — lots of updates, lots of deletes, high-churn queues or state machines — you can generate dead rows faster than routine cleanup reclaims them. The table keeps growing on disk even though the &lt;em&gt;logical&lt;/em&gt; data isn't. Indexes get fatter and less cache-friendly. Every query touching that table pays a small, invisible tax. Multiply that tax by every request, every day, for months, and you get exactly what I saw once: a single-character logic bug in how we handled a status flag turned into 53 seconds of unexplained latency, because it was quietly generating far more row churn than anyone had accounted for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's so easy to miss until it's expensive
&lt;/h2&gt;

&lt;p&gt;A few reasons this class of problem slips past normal engineering discipline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It degrades gradually, not suddenly.&lt;/strong&gt; There's no alert that fires at "20% bloated." By the time it's visible in your latency graphs, it's usually been building for weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't show up in code review.&lt;/strong&gt; The bug isn't in your query. It's in the &lt;em&gt;pattern of writes&lt;/em&gt; your query generates, which is much harder to reason about statically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard monitoring doesn't surface it well.&lt;/strong&gt; You'll see slow queries. You often won't see &lt;em&gt;why&lt;/em&gt; they're slow, unless you're specifically looking at table and index bloat metrics — which most teams aren't, until they've been burned once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autovacuum/purge settings that were fine at launch stop being fine at scale.&lt;/strong&gt; The defaults are tuned for a generic workload, not your specific write pattern. Nobody revisits them until something breaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's actually worked for me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Treat bloat as a metric, not an incident.&lt;/strong&gt; Track table and index bloat percentage the same way you track p99 latency or error rate — as an ongoing dashboard, not something you check when things are already slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Match cleanup aggressiveness to write pattern, not table size.&lt;/strong&gt; A small table with extremely high churn (think: status flags, counters, queue-like tables) often needs more aggressive vacuum/purge tuning than a much larger, mostly-append table. Tune per table, not globally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be suspicious of "just add a flag column."&lt;/strong&gt; Status flags, soft-delete columns, and toggle-heavy schemas are the most common source of the update churn that drives bloat. They're also the easiest thing for an engineer to add without thinking about the write amplification they cause. I've learned to ask "how often does this column change, and what does that do to the row?" before approving that kind of schema change now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the runbook before you need it.&lt;/strong&gt; Know in advance what a manual bloat-reclaim operation costs you — how long a table rebuild or full vacuum takes, whether it locks writes, what your rollback plan is. Figuring this out during an incident, on a production payments table, is a bad place to learn it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;Bloat isn't really a database problem. It's a systems-thinking problem — it's what happens when the &lt;em&gt;shape&lt;/em&gt; of your writes over time isn't something anyone is explicitly tracking, only the shape of your reads. The fix isn't a smarter query. It's treating write patterns as a first-class design concern, with the same rigor you'd apply to an API contract.&lt;/p&gt;

&lt;p&gt;The systems that don't get bitten by this aren't the ones with cleverer engineers. They're the ones who made "what does this write pattern do to the table over six months" a normal question to ask in design review — before it became a normal question to ask during an incident review.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What's the sneakiest "invisible tax" problem you've hit in a production system — the kind that doesn't show up until months in? Curious what others have run into.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>One '%' Cost Us 53 Seconds: A Redis, MySQL Buffer Pool Story</title>
      <dc:creator>Yogendra Singh</dc:creator>
      <pubDate>Tue, 04 Aug 2026 18:33:24 +0000</pubDate>
      <link>https://dev.to/yogendra_singh_9737a86e38/one-cost-us-53-seconds-a-redis-mysql-buffer-pool-story-5117</link>
      <guid>https://dev.to/yogendra_singh_9737a86e38/one-cost-us-53-seconds-a-redis-mysql-buffer-pool-story-5117</guid>
      <description>&lt;p&gt;Every engineering team has that one meeting. Someone proudly announces,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"We reduced database traffic by 95% using Redis!"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everyone smiles. Graphs look beautiful. CPU usage drops. Someone even starts preparing a "Performance Improvement" presentation.&lt;/p&gt;

&lt;p&gt;Then...&lt;br&gt;
Production throws an alert. A query that had been taking 9 seconds suddenly starts taking 62 seconds. Naturally, Redis becomes the prime suspect.&lt;/p&gt;

&lt;p&gt;Except...&lt;br&gt;
Redis wasn't the problem. It merely exposed a problem that had always existed.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Background
&lt;/h2&gt;

&lt;p&gt;We had an &lt;strong&gt;entity_attribute&lt;/strong&gt; table that stored dynamic attributes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;entity_id&lt;/th&gt;
&lt;th&gt;key&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;STATUS&lt;/td&gt;
&lt;td&gt;ACTIVE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;TYPE&lt;/td&gt;
&lt;td&gt;PREMIUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;td&gt;STATUS&lt;/td&gt;
&lt;td&gt;BLOCKED&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Almost every API fetched attributes by entity_id. Those lookups were extremely frequent.&lt;/p&gt;

&lt;p&gt;To reduce database load, we cached the attributes in Redis. Instead of hitting MySQL thousands of times every minute, requests were served directly from memory.&lt;/p&gt;

&lt;p&gt;The result?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lower database CPU ✅&lt;/li&gt;
&lt;li&gt;Lower latency ✅&lt;/li&gt;
&lt;li&gt;Happy developers ✅
Until one query reminded us that production always gets the last laugh.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The One Query That Couldn't Use Redis
&lt;/h2&gt;

&lt;p&gt;Most lookups were by entity_id. But one API searched using the last few digits of the value. 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;entity_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;entity_attribute&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%123'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the leading wildcard. That tiny % completely changes how MySQL works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LIKE '%123' Is Expensive
&lt;/h2&gt;

&lt;p&gt;Indexes are like dictionaries. They help MySQL quickly locate values that start with something.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;code&gt;WHERE value LIKE 'ABC%'&lt;/code&gt;&lt;br&gt;
can efficiently use an index.&lt;/p&gt;

&lt;p&gt;But&lt;br&gt;
&lt;code&gt;WHERE value LIKE '%123'&lt;/code&gt;&lt;br&gt;
asks MySQL:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Find every value that ends with 123."&lt;br&gt;
Since MySQL doesn't know where those values begin, it cannot efficiently traverse a normal B-tree index. Instead, it has to inspect a huge number of rows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words...&lt;br&gt;
The leading % quietly disables one of the database's biggest performance advantages.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Did It Become Worse After Redis?
&lt;/h2&gt;

&lt;p&gt;Before Redis, this table was one of the hottest tables in MySQL. Thousands of reads kept its pages inside the &lt;strong&gt;InnoDB Buffer Pool.&lt;/strong&gt; Even though the query performed a large scan, much of the data was already in memory.&lt;/p&gt;

&lt;p&gt;The query was slow...&lt;br&gt;
but tolerable. &lt;br&gt;
Around 9 seconds.&lt;/p&gt;

&lt;p&gt;After Redis, almost every lookup disappeared. MySQL gradually evicted those pages from the Buffer Pool.&lt;/p&gt;

&lt;p&gt;Now the same scan had to fetch data from disk. Nothing about the SQL changed. Nothing about the indexes changed.&lt;/p&gt;

&lt;p&gt;Only one thing changed. The table became cold.&lt;/p&gt;

&lt;p&gt;The result?&lt;br&gt;
&lt;strong&gt;9 seconds → 62 seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Redis didn't slow MySQL. It simply stopped keeping the table warm.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Investigation
&lt;/h2&gt;

&lt;p&gt;At first, everyone blamed Redis. Then we looked deeper. Execution plans were nearly identical. The real difference was physical I/O.&lt;/p&gt;

&lt;p&gt;The query wasn't CPU-bound anymore. It had become disk-bound. That's when we realized something important.&lt;/p&gt;

&lt;p&gt;Even if we warmed the Buffer Pool again...&lt;br&gt;
The query itself was fundamentally inefficient.&lt;/p&gt;

&lt;p&gt;We needed a better query.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Real Fix
&lt;/h2&gt;

&lt;p&gt;Instead of searching using&lt;br&gt;
&lt;code&gt;WHERE value LIKE '%123'&lt;/code&gt;&lt;br&gt;
We extracted the searchable suffix into a Generated Column.&lt;br&gt;
For example:&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;entity_attribute&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;gen_val&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;STORED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we indexed it.&lt;br&gt;
&lt;code&gt;CREATE INDEX idx_gen_val ON entity_attribute(gen_val);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now the query became&lt;br&gt;
&lt;code&gt;SELECT entity_id FROM entity_attribute WHERE gen_val = '123';&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;No wildcard. No table scan. A simple indexed lookup. &lt;br&gt;
Exactly what MySQL loves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;Instead of asking MySQL to inspect nearly every row...&lt;/p&gt;

&lt;p&gt;We gave it a proper index. The query became dramatically faster.&lt;/p&gt;

&lt;p&gt;More importantly...&lt;br&gt;
Its performance was now independent of whether the table happened to be warm in the InnoDB Buffer Pool.&lt;br&gt;
The optimization wasn't just faster. It was predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cache doesn't fix bad queries.&lt;/strong&gt; It often hides them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lower database traffic changes memory behavior.&lt;/strong&gt;&lt;br&gt;
Redis reduced reads so effectively that MySQL no longer kept this table in the InnoDB Buffer Pool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leading wildcards are expensive.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;LIKE '%123'&lt;br&gt;
is one of those queries that should immediately make every DBA slightly uncomfortable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Generated Columns are underrated.&lt;/strong&gt;&lt;br&gt;
They let you convert an expensive expression into something MySQL can index efficiently, without changing the original data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Redis wasn't the villain. The Buffer Pool wasn't broken.&lt;br&gt;
MySQL wasn't slow.&lt;br&gt;
Our query was asking MySQL to do something it was never optimized to do.&lt;br&gt;
Redis simply removed the accidental performance boost that had been masking the problem.&lt;br&gt;
Sometimes production doesn't create bugs. &lt;strong&gt;It reveals assumptions&lt;/strong&gt;. And in our case, the biggest optimization wasn't Redis.&lt;/p&gt;

&lt;p&gt;It was replacing one innocent-looking % with an indexed generated column.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This may not be the perfect solution. Comment what could have been done better.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>mysql</category>
      <category>redis</category>
      <category>bugsmash</category>
      <category>devchallenge</category>
    </item>
  </channel>
</rss>
