Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
DatabasesJune 22, 20265 min read

Covering indexes: Speed up read queries by eliminating bookmark lookups

Master covering indexes to eliminate bookmark lookups. Improve database query performance by keeping data retrieval inside the B-tree index structure.

databasesindexingsqlperformancepostgresqlbackendMySQLRedisDatabase

Last month, our primary dashboard API started creeping toward a 400ms latency threshold. It wasn't a complex join or a missing index; it was a simple SELECT statement hitting a table with roughly 4 million rows. After pulling the EXPLAIN ANALYZE output, I saw the culprit: a massive number of "Bookmark Lookups" (or "RID Lookups" in SQL Server terminology).

The database was finding the rows via an index, but then it had to jump back to the heap or the clustered index to fetch additional columns not present in the original index. This round-trip per row is a silent performance killer. If you're struggling with similar bottlenecks, you need to understand how covering indexes can fundamentally reshape your read efficiency.

The anatomy of a bookmark lookup

When you execute a query, the database engine uses an index to locate the specific records you’ve requested. If your index doesn't contain every piece of data requested in the SELECT clause, the engine performs a lookup to the main table storage to fetch the missing columns.

Think of it like using a library index card. If the card only gives you the shelf location, you have to walk over to the shelf to read the book. If the card actually contained the entire text of the book, you wouldn't need to move at all. That’s the core of database optimization through covering.

Crafting a covering index

A covering index is an index that includes all the columns referenced in your query, whether they are in the WHERE, JOIN, or SELECT clauses. By including these columns, the engine can satisfy the query entirely from the B-tree nodes without touching the base table.

Let’s look at a common scenario. We had a query like this:

SQL
SELECT user_id, status, created_at 
FROM audit_logs 
WHERE user_id = 12345 
ORDER BY created_at DESC;

We already had an index on (user_id). However, the query was still slow because it was fetching status and created_at from the base table. My first attempt was to add status and created_at to the existing index. I created a composite index:

SQL
CREATE INDEX idx_user_status_created 
ON audit_logs (user_id, created_at, status);

By shifting the index definition to include the payload columns, I transformed the operation from an Index Seek + Bookmark Lookup into a pure Index Seek. The latency dropped from about 280ms to under 15ms.

When covering indexes go wrong

While this sounds like a silver bullet, you have to be careful. Every index you add increases the overhead on write operations. If your table has a high write-to-read ratio, adding massive covering indexes will slow down your INSERT and UPDATE statements because the engine must update the B-tree nodes every time a record changes.

I once tried to "cover" a query on a high-traffic transactional table by including five extra columns in the index. The read speed improved, but our write latency spiked by nearly 50ms. We had to backtrack and use partial indexes for high-cardinality filtering: A deep dive instead, which kept the index size manageable while still optimizing the specific read path we cared about.

Practical steps for implementation

If you're hunting for these optimizations, follow this checklist:

  1. Analyze the plan: Run EXPLAIN or EXPLAIN ANALYZE on your slowest queries. Look specifically for "Bookmark Lookup," "RID Lookup," or "Key Lookup."
  2. Identify missing columns: Note which columns are being fetched from the table that aren't in your existing index.
  3. Evaluate index size: Can you include these columns without making the index so large that it no longer fits in memory? If the index size explodes, you might be better off using indexing strategy for app developers: Stop slow queries to refine your schema design first.
  4. Test the trade-off: Always check your write performance after deploying a new index. An index that speeds up a read by 10ms but costs 20ms on every write is a net loss for the system.

The B-tree index reality

Remember that b-tree indexing is about balance. You're trying to minimize I/O by keeping as much data as possible in the index pages. If you find yourself adding too many columns to an index, you might actually be hitting the limits of your current schema. Sometimes, the right move isn't a wider index, but moving to materialized views for database performance in complex analytical queries to decouple your read-heavy reports from your transactional tables.

I’m still experimenting with how many columns are "too many" for a covering index. There’s no hard rule, but once I start hitting 4 or 5 included columns, I start looking for architectural alternatives. It’s a constant tug-of-war between read latency and write throughput, and the only way to know for sure is to profile your specific workload.

Frequently Asked Questions

Does a covering index always improve performance? No. If the index becomes too large, it may not fit in the database's buffer cache, leading to slower performance than a smaller index that requires a bookmark lookup.

How do I know if an index is "covering"? Check your execution plan. If you see an "Index Seek" or "Index Scan" without a corresponding "Bookmark/Key/RID Lookup," the index is covering that query.

Can I include non-indexed columns in an index? Yes, most modern databases (like PostgreSQL or SQL Server) allow you to use INCLUDE clauses, which store extra data in the leaf nodes of the B-tree without participating in the index sorting, minimizing the index footprint.

Improving query performance isn't about finding one perfect index; it's about understanding the cost of your data retrieval patterns. Start by eliminating those bookmark lookups, but keep an eye on your write traffic to ensure you're not trading one problem for another.

Back to Blog

Similar Posts

Close-up of a smartphone showing Python code on the display, showcasing coding and technology.
DatabasesJune 20, 20264 min read

Indexing Strategy for App Developers: Stop Slow Queries

Master an indexing strategy for app developers to fix slow production queries. Learn how to read EXPLAIN plans, pick the right columns, and avoid overhead.

Read more
Scrabble tiles spelling 'DATA' on a wooden table with a blurred plant background.
Databases
June 20, 2026
4 min read

When to denormalize your database for production performance

Denormalize your database only when read latency becomes a bottleneck. Learn to evaluate the trade-offs between schema complexity and query speed.

Read more
DatabasesJune 21, 20264 min read

Database Sharding for High-Concurrency: A Practical Scaling Guide

Database sharding is the final frontier for high-concurrency apps. Learn how to implement horizontal scaling, choose partition keys, and manage routing.

Read more