<?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: Dilip V P</title>
    <description>The latest articles on DEV Community by Dilip V P (@dilip_v_p).</description>
    <link>https://dev.to/dilip_v_p</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%2F1344527%2Ff6c13742-88fe-4d7a-a966-16313ddb2c2f.jpg</url>
      <title>DEV Community: Dilip V P</title>
      <link>https://dev.to/dilip_v_p</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dilip_v_p"/>
    <language>en</language>
    <item>
      <title>How Database Indexes Actually Work (and When They Backfire)</title>
      <dc:creator>Dilip V P</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:58:15 +0000</pubDate>
      <link>https://dev.to/dilip_v_p/how-database-indexes-actually-work-and-when-they-backfire-2c59</link>
      <guid>https://dev.to/dilip_v_p/how-database-indexes-actually-work-and-when-they-backfire-2c59</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Without an index, your database finds a row by reading &lt;em&gt;every&lt;/em&gt; row (a full table scan). An index is a sorted structure that lets it jump straight to the row instead. But indexes are a trade, not free speed: they only help selective queries, and they slow down every write. Use &lt;code&gt;EXPLAIN&lt;/code&gt; to see what your database is actually doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Your query was instant in development. In production, it crawls. Same code. The only thing that changed is the amount of data.&lt;/p&gt;

&lt;p&gt;Nine times out of ten, this is why: without an index, the database has only one way to find your row, which is to read every row, one at a time, until it matches. That is a full table scan.&lt;/p&gt;

&lt;p&gt;On 10,000 rows you don't notice. On 10,000,000, it is painful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an index exists (first principles)
&lt;/h2&gt;

&lt;p&gt;An index exists to avoid that work. Scanning the whole table doesn't scale: a query that filters on one column shouldn't have to read every row to find a handful.&lt;/p&gt;

&lt;p&gt;So the database keeps a separate, sorted directory of one column's values, stored alongside the table, like the index at the back of a textbook. Instead of flipping through every page, you look up the term and jump straight to the page.&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_users_email&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why it's fast
&lt;/h2&gt;

&lt;p&gt;Because the directory is sorted, the database doesn't read it top to bottom either. It navigates straight toward the value: narrow the range, discard the half that can't contain it, repeat. That is the same halving idea as binary search, and the structure that makes it work on disk is a B-tree (the disk-friendly generalization, not literally a binary search over rows).&lt;/p&gt;

&lt;p&gt;The payoff: finding one row among a million takes on the order of ~20 steps, not a million.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part most engineers miss: it's a trade
&lt;/h2&gt;

&lt;p&gt;Indexes are not free performance. They are a trade-off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;They only help when your query is selective&lt;/strong&gt;, when it returns a small fraction of the table. &lt;code&gt;WHERE id = ?&lt;/code&gt; (one row) flies. &lt;code&gt;WHERE active = true&lt;/code&gt; (half the table) can be &lt;em&gt;slower&lt;/em&gt; with the index than a plain scan, because there is no shortcut when you are returning most of the rows. A plan can literally say "using index" and still be slow; what matters is how many rows it touches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every index has a write cost.&lt;/strong&gt; Inserts, updates, and deletes all have to keep every index current. The more indexes you have, the slower your writes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They cost storage.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the rule isn't "add indexes everywhere." It is: index the columns your queries actually filter, join, and sort on, and only where it is selective enough to help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop guessing: read the plan
&lt;/h2&gt;

&lt;p&gt;You never have to guess at any of this. Put &lt;code&gt;EXPLAIN&lt;/code&gt; in front of your query and the database shows you its plan before it runs anything.&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;EXPLAIN&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;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&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;What you are looking for is "reads the whole table" turning into "uses the index":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Slow (full scan)&lt;/th&gt;
&lt;th&gt;Fast (uses index)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SCAN users&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SEARCH users USING INDEX&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Seq Scan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Index Scan&lt;/code&gt; / &lt;code&gt;Index Only Scan&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;type: ALL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;type: ref&lt;/code&gt; / &lt;code&gt;range&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(In Postgres, &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; also shows the real row counts versus the planner's estimates, which is where a lot of "why didn't it use my index?" mysteries get solved.)&lt;/p&gt;

&lt;p&gt;Get in the habit of reading the plan, and you will catch the slow query before it pegs your database and takes the app down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch it visually
&lt;/h2&gt;

&lt;p&gt;I made a short, visual breakdown of all of this, from the full table scan to the index, to why it is fast, to when it backfires, to reading &lt;code&gt;EXPLAIN&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ai_Kx7vjbPA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;It is the first episode of &lt;strong&gt;The Leap&lt;/strong&gt;, a series explaining the systems we build on, from databases and networking to memory and distributed systems, from first principles. Next up: the dark side of indexes, and when adding one is the wrong move.&lt;/p&gt;

&lt;p&gt;What's the nastiest slow query you have had to debug in production?&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
