<?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: OPEYEMI OLUWAGBEMIGA</title>
    <description>The latest articles on DEV Community by OPEYEMI OLUWAGBEMIGA (@opeyemi_oluwagbemiga_a213).</description>
    <link>https://dev.to/opeyemi_oluwagbemiga_a213</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%2F2923359%2Fbaae4694-a878-414c-a633-5bff52b9ce5c.png</url>
      <title>DEV Community: OPEYEMI OLUWAGBEMIGA</title>
      <link>https://dev.to/opeyemi_oluwagbemiga_a213</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/opeyemi_oluwagbemiga_a213"/>
    <language>en</language>
    <item>
      <title>Why Standard Indexes Fail: The Architecture of the Covering Index</title>
      <dc:creator>OPEYEMI OLUWAGBEMIGA</dc:creator>
      <pubDate>Sat, 02 May 2026 06:16:39 +0000</pubDate>
      <link>https://dev.to/opeyemi_oluwagbemiga_a213/why-standard-indexes-fail-the-architecture-of-the-covering-index-4g0o</link>
      <guid>https://dev.to/opeyemi_oluwagbemiga_a213/why-standard-indexes-fail-the-architecture-of-the-covering-index-4g0o</guid>
      <description>&lt;p&gt;In my last article, I broke down how and why to use indexes wisely for efficient lookups and data retrieval by identifying fields to index and which not to index. Can read that &lt;a href="https://dev.to/opeyemi_oluwagbemiga_a213/slapping-secondary-indexes-on-random-fields-is-silently-killing-your-database-15p3"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But adding a standard index in some cases is half the battle.&lt;/p&gt;

&lt;p&gt;Adding an index to a column creates a separate, organized B-Tree for that column. And at the bottom of the tree (leaf node) is the pointer. That pointer tells the database engine where the full row lives on the heap. The heap is the main table where the full rows live. It is unsorted, massive, and slow to search as it forces a full-table scan. Indexes help to make data retrieval faster.&lt;/p&gt;

&lt;p&gt;Standard indexes make searching fast, but they introduce a hidden bottleneck. Let’s look at a real-world example to see how and when to use a Covering Index to fix it.&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%2F5w7yvvdkt210gsazh89w.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%2F5w7yvvdkt210gsazh89w.png" alt="Spotify “Recently Played” Screen" width="550" height="524"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Spotify “Recently Played” Screen&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem: The “Recently Played” Feed
&lt;/h2&gt;

&lt;p&gt;Think of any music streaming platform like Spotify or Apple Music. Every time you open the app, it instantly fetches your most recently played tracks, around 10 tracks initially.&lt;/p&gt;

&lt;p&gt;The query looks 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;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;played_at&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;listen_history&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'018dc336-1234...'&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;played_at&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;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Method 1: No Index
&lt;/h2&gt;

&lt;p&gt;Without an index, the query is forced to do a full-table scan. Let’s say Spotify has 100 million records on its listen_history table, which means for each query.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;100 million rows will be scanned just to isolate this single user’s history&lt;/li&gt;
&lt;li&gt;Then it is sorted in memory.&lt;/li&gt;
&lt;li&gt;And then the top 10 i
s returned.
At scale, this doesn’t just increase response times. This manual RAM sort will choke your disk I/O, exhaust your connection pool, and eventually bring down your application with 500 server errors.&lt;/li&gt;
&lt;/ol&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%2F5ml0lc9rbp8cho7xw7pp.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%2F5ml0lc9rbp8cho7xw7pp.png" alt="Result of using Simple SELECT query" width="800" height="276"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Execution Time roughly 217ms with no index as a result of the Parallel Sequential Scan with 1,000,000 rows scanned. (Nano Banana for sharpening text in the terminal)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Using Standard Index
&lt;/h2&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_user_history&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;listen_history&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&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;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;played_at&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;listen_history&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'018dc336-1234...'&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;played_at&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;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, an index is added on &lt;code&gt;user_id&lt;/code&gt;, and the database engine creates a new B-Tree for this column. So let's say a user has played 5000 different tracks after signing up. No need for a 100 million row scan. It takes &lt;code&gt;O(log n)&lt;/code&gt; time to traverse through the B-Tree to get the records and their pointers (where &lt;code&gt;n&lt;/code&gt; is the number of tracks played by the user).&lt;/p&gt;

&lt;p&gt;Is it faster, right? But here is the hidden catch.&lt;/p&gt;

&lt;p&gt;But the index only contains &lt;code&gt;user_id&lt;/code&gt;. The query is asking for &lt;code&gt;track_id&lt;/code&gt; and &lt;code&gt;played_at&lt;/code&gt;. Because these columns are missing from the index, the database engine must use the pointers to jump from the B-Tree to the main heap to get the missing data.&lt;/p&gt;

&lt;p&gt;You know what that means? 5000 physical jump to the Heap. And after the jump, sorting is done in the memory using the &lt;code&gt;played_at&lt;/code&gt; column, and then it returns the top 10. This physical jump is what’s called the Heap Fetch, and this destroys I/O performance.&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%2Fettathe3y746p6kwyssr.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%2Fettathe3y746p6kwyssr.png" alt="Result of using Standard Indexes" width="800" height="287"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The standard index eliminates the Sequential Scan, but introduces a new bottleneck, which are the implicit Heap Fetch and heap sort in RAM. (Nano Banana for sharpening text in the terminal)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 3: Covering Index
&lt;/h2&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_user_recent_plays&lt;/span&gt; 
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;listen_history&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;played_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;track_id&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;track_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;played_at&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;listen_history&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'018dc336-1234...'&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;played_at&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;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look closely at this query:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;user_id&lt;/code&gt; and the &lt;code&gt;played_at DESC&lt;/code&gt; are used to build the core structure of the B-Tree. With this, the data is sorted by &lt;code&gt;user_id&lt;/code&gt; and then by &lt;code&gt;played_at&lt;/code&gt; in descending order&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;INCLUDE&lt;/code&gt; keyword bolts the &lt;code&gt;track_id&lt;/code&gt; to the leaf nodes of the B-Tree&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So when the &lt;code&gt;SELECT&lt;/code&gt; query is run:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The database engine jumps to the &lt;code&gt;user_id&lt;/code&gt; in the B-Tree.&lt;/li&gt;
&lt;li&gt;And because the tree is pre-sorted using the &lt;code&gt;played_at&lt;/code&gt;, no need to take all tracks and sorting; all the database engine does is to fetch the first 10 rows as it is exactly the 10 newest tracks.&lt;/li&gt;
&lt;li&gt;It grabs the track_id from the leaf node, and returns the data to the user.
It never jumps to the Heap. It never sorts data in memory. That is why it is called an Index-Only Scan, and it executes at the speed of memory.&lt;/li&gt;
&lt;/ol&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%2Fvi5ujdbiuiw5jg7z9379.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%2Fvi5ujdbiuiw5jg7z9379.png" alt="Result of using Covering Index" width="800" height="177"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Covering Index doesn’t require the Heap Fetch and in-memory sorting isn’t required, reducing execution time to a blistering 0.106ms.&lt;br&gt;
The magic. From a 217ms execution time to a 0.106ms execution time&lt;/em&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%2Fkw53uiyokjfs4s8vvmbv.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%2Fkw53uiyokjfs4s8vvmbv.png" alt="From a 217ms execution time to a 0.106ms execution time" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NOTE: If you run this exact experiment locally immediately after doing a 1-million-row bulk insert, your initial Index-Only Scan might show a small number of Heap Fetches (e.g., Heap Fetches: 10). This is not a failure of the index. This happens because PostgreSQL hasn't had time to update its Visibility Map. The database checks the heap to ensure that another transaction hasn't deleted those specific 10 rows. Running a manual &lt;code&gt;VACUUM&lt;/code&gt; on the table updates the map and instantly drops the Heap Fetches back down to zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHEN TO USE COVERING INDEX:
&lt;/h2&gt;

&lt;p&gt;Covering indexes are powerful, but if misused, they can slow down write speeds. They should not be abused.&lt;/p&gt;

&lt;p&gt;Use them strictly for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Highly critical, high-frequency read queries (endpoints hit thousands of times a second).&lt;/li&gt;
&lt;li&gt;Queries that return a small, lightweight columns (Integers, UUIDs, Timestamps, Booleans)
. Never INCLUDE massive text blocks or JSON data.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>sql</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Slapping Secondary Indexes on Random Fields is Silently Killing Your Database.</title>
      <dc:creator>OPEYEMI OLUWAGBEMIGA</dc:creator>
      <pubDate>Tue, 03 Mar 2026 06:17:52 +0000</pubDate>
      <link>https://dev.to/opeyemi_oluwagbemiga_a213/slapping-secondary-indexes-on-random-fields-is-silently-killing-your-database-15p3</link>
      <guid>https://dev.to/opeyemi_oluwagbemiga_a213/slapping-secondary-indexes-on-random-fields-is-silently-killing-your-database-15p3</guid>
      <description>&lt;p&gt;Why do we add indexes to our SQL fields? To make the search faster, right?&lt;/p&gt;

&lt;p&gt;But do you know it has a massive downside? Writes become slower, forcing developers to be strategic about which fields should be labelled as secondary indexes.&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%2Fsovz2dclat6sn45t9rss.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%2Fsovz2dclat6sn45t9rss.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What is the magic behind indexes? Let's say we have a table of books called “book” with fields (id, title, author, pub_date, isbn).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SELECT * FROM book WHERE author=”C.S. Lewis”&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This causes the database engine to search through the Heap (a physical unstructured file where all data in the database actually lives). Let's say we have a total of 1 million rows in the table and only 20 Lewis books. The database engine would scan through all the 1 million rows, even when it had gotten the total of 20 Lewis books, just to be sure it didn’t miss any.&lt;/p&gt;

&lt;p&gt;This is a Full Table Scan with &lt;em&gt;O(n)&lt;/em&gt; complexity. It is brutally slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  THE FIX
&lt;/h2&gt;

&lt;p&gt;When you add the field “author” as a secondary index&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CREATE INDEX idx_author ON book(author);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The database engine creates a separate, highly organized B-Tree structure. It stores the Author names in alphabetical order, paired with a pointer (physical addresses) to where the full row actually lives.&lt;/p&gt;

&lt;p&gt;So instead of scanning through all 1 million rows, the database traverses through the B-Tree in &lt;em&gt;O(log n)&lt;/em&gt; time to get the 20 records and their pointers. It then uses the pointers to get the data it needs from the heap. That’s how secondary indexes make reads faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  BUT WHY DO WRITES GET SLOWER?
&lt;/h2&gt;

&lt;p&gt;Adding or updating data no longer means just changing the heap data, but also the additional B-Tree created by the secondary indexes. So if you blindly add 5 secondary indexes to a table, every single &lt;code&gt;INSERT&lt;/code&gt; or &lt;code&gt;UPDATE&lt;/code&gt; means writing to the Heap plus updating 5 separate B-Trees on the disk.&lt;/p&gt;

&lt;p&gt;That is the hidden cost. As a backend developer, your job isn’t to index everything; instead, your job is to understand the tradeoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  TIPS FOR SECONDARY INDEXES
&lt;/h2&gt;

&lt;p&gt;Do not index fields that are constantly updated, like page visits and view counts. This is to reduce the overhead of the database’s write performance.&lt;/p&gt;

&lt;p&gt;Only index fields that you actively use in your &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, or &lt;code&gt;ORDER BY&lt;/code&gt; clauses.&lt;/p&gt;

</description>
      <category>database</category>
      <category>systemdesign</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
