technical9 min read·Jul 21, 2026

Unpacking Database Indexes: Why They Matter and How They Work

Database indexes are fundamental for query performance, yet often misunderstood. This post dives into what indexes are, how common types like B-trees and hash indexes function, and when to use them effectively.

ShareLinkedInX / Twitter

You build an application, everything's fast, everything's great. Then, suddenly, as data grows, a simple SELECT query starts taking seconds. What happened? More often than not, the culprit is a missing or improperly used database index.

Indexes are one of those fundamental database concepts that seem simple on the surface but hide a lot of nuance. Getting them right can make or break your application's performance. Getting them wrong can introduce new bottlenecks. Let's dig into what they are, how they work, and how to use them effectively.

The Problem: Full Table Scans

Imagine you have a table with millions of rows. Let's say it's an attendance_records table, similar to what I'd deal with in my Real-Time Attendance Management System project, tracking every student's entry and exit. You want to find all attendance records for a specific user_id.

Without an index on user_id, the database engine has one option: it starts at the first row, checks if the user_id matches, then moves to the second, and so on, until it's checked every single row in the table. This is called a full table scan.

SELECT * FROM attendance_records WHERE user_id = 123;

If that table has 10 million rows, and the user_id you're looking for is near the end, or if you're looking for many records that are scattered, the database has to do a lot of work. This is slow. Really slow.

What is an Index?

Think of a database index like the index at the back of a textbook. You want to find all mentions of 'B-tree'. You don't read the entire book page by page. Instead, you go to the index, find 'B-tree', and it tells you exactly which pages to turn to.

A database index is a special lookup table that the database search engine can use to speed up data retrieval. It's a separate data structure that stores the values of one or more columns from the table, along with pointers to the actual rows where those values reside.

When you query an indexed column, the database can quickly scan the smaller, optimized index structure to find the row pointers, then jump directly to the relevant data rows, bypassing the need to read the entire table.

How Indexes Work: Under the Hood

There are several types of indexes, but two are most common: B-tree indexes and Hash indexes. Understanding their underlying structure helps you pick the right one.

B-Tree Indexes

B-tree (Balanced Tree) indexes are the most common type, used by default in most relational databases (PostgreSQL, MySQL, SQLite, Oracle, SQL Server). They are incredibly versatile.

A B-tree is a self-balancing tree data structure that keeps data sorted and allows searches, sequential access, insertions, and deletions in logarithmic time. Here's a simplified breakdown:

  • Nodes: The tree consists of nodes. Each node can hold a certain number of keys (column values) and pointers to child nodes.
  • Root, Internal, Leaf Nodes: There's a single root node at the top. Internal nodes point to other internal nodes or leaf nodes. Leaf nodes contain the actual indexed values and pointers to the rows in the table.
  • Sorted Data: All keys within a node are sorted. Keys in left child nodes are less than the key in the parent, and keys in right child nodes are greater.
  • Balanced: The 'B' in B-tree stands for balanced. This means all leaf nodes are at the same depth. This ensures that a search operation always takes roughly the same amount of time, regardless of where the data is located.

How a Search Works:

  1. Start at the root node.
  2. Compare your search value with the keys in the current node.
  3. Follow the appropriate pointer to a child node (left if less, right if greater, or in-between if matching an internal key).
  4. Repeat until you reach a leaf node.
  5. The leaf node contains the exact value (or range of values) and the physical location (e.g., page and offset) of the corresponding row in the table.

This structure makes B-trees excellent for:

  • Equality lookups: WHERE user_id = 123
  • Range queries: WHERE timestamp BETWEEN '2023-01-01' AND '2023-01-31'
  • ORDER BY and GROUP BY clauses: Since the data in the index is sorted, the database can use the index to fulfill sorting requirements without performing a separate sort operation.
  • LIKE searches with a leading string: WHERE name LIKE 'Ansh%' (but not '%Ansh').

Hash Indexes

Hash indexes are a different beast. They are based on hash tables, which map keys to values using a hash function. When you create a hash index on a column, the database calculates a hash value for each entry in that column and stores it in the index, along with a pointer to the original row.

How a Search Works:

  1. When you query WHERE email = 'ansh@example.com', the database calculates the hash of 'ansh@example.com'.
  2. It then uses this hash value to directly jump to the location in the hash index where that hash is stored.
  3. From there, it retrieves the pointer to the actual row.

This provides incredibly fast exact equality lookups. It's O(1) in the best case, meaning the lookup time is constant regardless of the table size. However, hash indexes have significant limitations:

  • Equality only: They can only be used for exact equality comparisons (=).
  • No range queries: You can't use them for WHERE value > 10 or WHERE value BETWEEN X AND Y because the hash values don't maintain any inherent order.
  • No ORDER BY or GROUP BY: Similarly, they can't help with sorting.
  • No partial matches: WHERE name LIKE 'Ansh%' won't work.

Given these limitations, B-tree indexes are almost always the default and generally more useful. Hash indexes are a niche optimization for very specific workloads that involve only equality checks.

Creating Indexes

Creating an index is straightforward in SQL. Let's use an example for our attendance_records table.

Raw SQL

To create a B-tree index on the user_id column:

CREATE INDEX idx_attendance_user_id ON attendance_records (user_id);

If you want to create a composite index (an index on multiple columns), which is useful for queries that filter on both columns:

CREATE INDEX idx_attendance_user_session ON attendance_records (user_id, session_id);

For a hash index (if supported by your database, e.g., PostgreSQL):

CREATE INDEX idx_attendance_email_hash ON users USING HASH (email);

SQLAlchemy

When using an ORM like SQLAlchemy (which I use in many of my projects, like the Attendance System and Dronzer AI Gateway), you can define indexes directly in your model definitions.

from sqlalchemy import Column, Integer, String, DateTime, Index
from sqlalchemy.orm import declarative_base

Base = declarative_base()

class AttendanceRecord(Base):
    __tablename__ = 'attendance_records'

    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, nullable=False, index=True) # Simple index
    session_id = Column(Integer, nullable=False)
    timestamp = Column(DateTime, nullable=False)

    # Composite index example
    __table_args__ = (
        Index('idx_attendance_user_session', 'user_id', 'session_id'),
        # You can specify other index types if your backend supports it
        # For example, for a hash index in PostgreSQL via SQLAlchemy:
        # Index('idx_users_email_hash', 'email', postgresql_using='hash')
    )

    def __repr__(self):
        return f"<AttendanceRecord(user_id={self.user_id}, session_id={self.session_id}, timestamp='{self.timestamp}')>"

# To create the tables and indexes (usually done via migrations):
# from sqlalchemy import create_engine
# engine = create_engine('sqlite:///./test.db')
# Base.metadata.create_all(engine)

The index=True argument on a Column automatically creates a single-column B-tree index. For more complex or composite indexes, you use the Index construct within __table_args__.

Trade-offs and Considerations

Indexes aren't a silver bullet. They come with their own set of costs.

  1. Storage Overhead: Indexes consume disk space. A large table with many indexes can take up significantly more storage than the table data itself.
  2. Write Performance Impact: Every time you INSERT, UPDATE, or DELETE a row in the table, the database has to update not only the table data but also all associated indexes. This adds overhead and slows down write operations. For tables with very high write traffic, too many indexes can become a bottleneck.
  3. Index Selectivity: An index is most effective when the column it's on has high selectivity, meaning a large number of unique values. Indexing a boolean column (e.g., is_active) with only two possible values is usually not very helpful, as the database might still decide to do a full table scan because filtering on it doesn't narrow down the results much.
  4. Composite Indexes: When querying on multiple columns (e.g., WHERE col1 = X AND col2 = Y), a composite index on (col1, col2) can be much more efficient than two separate single-column indexes. The order of columns in a composite index matters: (col1, col2) is useful for queries on col1 alone or col1 AND col2, but less useful for queries on col2 alone.
  5. Partial Indexes (Conditional Indexes): Some databases (like PostgreSQL) allow you to create indexes on a subset of rows. For example, CREATE INDEX idx_active_users ON users (email) WHERE is_active = TRUE;. This can reduce index size and improve performance for specific queries on filtered data.
  6. When Not to Index: Don't index small tables (the overhead of using the index might be greater than a full table scan), columns with very low cardinality, or columns that are rarely queried.

My Approach to Indexing

When I'm working on a project, especially one with a lot of data or real-time demands like the Attendance Management System, I start by identifying the critical query paths. What are the most frequent SELECT queries? What filters are always applied? What columns are used in JOIN conditions, ORDER BY, or GROUP BY clauses?

For attendance_records:

  • user_id: Almost certainly needs an index for looking up a specific student's attendance history.
  • timestamp: Likely needs an index for range queries (e.g., "attendance for this week/month"). A composite index (user_id, timestamp) would be even better for queries like "get all attendance for user X in date range Y".
  • session_id: If I'm often querying attendance for a specific class session, this needs an index.

Then, I'd use EXPLAIN ANALYZE (or similar tools provided by the database) to see the query plan. This is invaluable. It tells you exactly how the database is executing your query, whether it's using an index, performing a full table scan, or doing expensive sorts. This feedback loop is crucial for optimizing your database interactions.

Wrapping Up

Database indexes are a core tool in a software engineer's arsenal for building performant applications. They're not magic, and you need to understand their trade-offs. B-trees are your general-purpose workhorse, while hash indexes are for specific equality-only scenarios. Use them thoughtfully, monitor their impact, and always profile your queries. Your users (and your future self) will thank you for it.

Frequently Asked Questions

What is a database index and why is it important?

A database index is a data structure that improves the speed of data retrieval operations on a database table. It's crucial because without it, the database might have to scan every row (a full table scan) to find the data, which becomes very slow on large tables.

What's the difference between a B-tree and a hash index?

B-tree indexes are general-purpose, excellent for range queries (e.g., `>`, `<`, `BETWEEN`), sorting, and `LIKE` searches. Hash indexes are optimized for exact equality lookups (`=`) but cannot handle range queries or sorting efficiently due to how they store data.

When should I avoid using an index?

Avoid indexes on columns with very low cardinality (few unique values), tables that are very small, or columns that are updated extremely frequently. Indexes add overhead to write operations (INSERT, UPDATE, DELETE) and consume disk space, so their benefits must outweigh these costs.

How do indexes impact write operations (INSERT, UPDATE, DELETE)?

Indexes slow down write operations because every time data in the indexed column changes, the index itself must also be updated. This additional work means more CPU cycles and disk I/O, which can become a bottleneck if a table has many indexes and high write traffic.

AG

Written by Ansh Gautam

Full-stack engineer building production systems with FastAPI, React, and AI/LLM integrations. Currently looking for backend engineering & AI integration roles.