Let’s look into some of the features of sql data indexing. This will be super helpful while developing our Rails 8 Application.
π Part 1: What is a Covering Index?
Normally when you query:
SELECT * FROM users WHERE username = 'bob';
- Database searches username index (secondary).
- Finds a pointer (TID or PK).
- Then fetches full row from table (heap or clustered B-tree).
Problem:
- Heap fetch = extra disk read.
- Clustered B-tree fetch = extra traversal.
π Covering Index idea:
β
If the index already contains all the columns you need,
β
Then the database does not need to fetch the full row!
It can answer the query purely by scanning the index! β‘
Boom β one disk read, no extra hop!
βοΈ Example in PostgreSQL:
Suppose your query is:
SELECT username FROM users WHERE username = 'bob';
- You only need
username. - But by default, PostgreSQL indexes only store the index column (here,
username) + TID.
β So in this case β already covering!
No heap fetch needed!
βοΈ Example in MySQL InnoDB:
Suppose your query is:
SELECT username FROM users WHERE username = 'bob';
- Secondary index (
username) contains:username(indexed column)user_id(because secondary indexes in InnoDB always store PK)
β¦οΈ So again, already covering!
No need to jump to the clustered index!
π― Key point:
If your query only asks for columns already inside the index,
then only the index is touched β no second lookup β super fast!
π Part 2: Real SQL Examples
β¨ PostgreSQL
Create a covering index for common query:
CREATE INDEX idx_users_username_email ON users (username, email);
Now if you run:
SELECT email FROM users WHERE username = 'bob';
Postgres can:
- Search index on
username - Already have
emailin index - β No heap fetch!
(And Postgres is smart: it checks index-only scan automatically.)
β¨ MySQL InnoDB
Create a covering index:
CREATE INDEX idx_users_username_email ON users (username, email);
β Now query:
SELECT email FROM users WHERE username = 'bob';
Same behavior:
- Only secondary index read.
- No need to touch primary clustered B-tree.
π Part 3: Tips to design smart Covering Indexes
β
If your query uses WHERE on col1 and SELECT col2,
β
Best to create index: (col1, col2).
β
Keep indexes small β don’t add 10 columns unless needed.
β
Avoid huge TEXT or BLOB columns in covering indexes β they make indexes heavy.
β Composite indexes are powerful:
CREATE INDEX idx_users_username_email ON users (username, email);
β Can be used for:
WHERE username = ?WHERE username = ? AND email = ?- etc.
β Monitor index usage:
- PostgreSQL:
EXPLAIN ANALYZE - MySQL:
EXPLAIN
β
Always check if Index Only Scan or Using Index appears in EXPLAIN plan!
π Quick Summary Table
| Database | Normal Query | With Covering Index |
|---|---|---|
| PostgreSQL | B-tree β Heap fetch (unless TID optimization) | B-tree scan only |
| MySQL InnoDB | Secondary B-tree β Primary B-tree | Secondary B-tree only |
| Result | 2 steps | 1 step |
| Speed | Slower | Faster |
π Great! β Now We Know:
π§ How heap fetch works!
π§ How block lookup is O(1)!
π§ How covering indexes skip heap fetch!
π§ How to create super fast indexes for PostgreSQL and MySQL!
π¦Ύ Advanced Indexing Tricks (Real Production Tips)
Now it’s time to look into super heavy functionalities that Postgres supports for making our sql data search/fetch super fast and efficient.
1. π― Partial Indexes (PostgreSQL ONLY)
β
Instead of indexing the whole table,
β
You can index only the rows you care about!
Example:
Suppose 95% of users have status = 'inactive', but you only search active users:
SELECT * FROM users WHERE status = 'active' AND email = 'bob@example.com';
π Instead of indexing the whole table:
CREATE INDEX idx_active_users_email ON users (email) WHERE status = 'active';
β¦οΈ PostgreSQL will only store rows with status = 'active' in this index!
Advantages:
- Smaller index = Faster scans
- Less space on disk
- Faster index maintenance (less updates/inserts)
Important:
- MySQL (InnoDB) does NOT support partial indexes π β only PostgreSQL has this superpower.
2. π― INCLUDE Indexes (PostgreSQL 11+)
β
Normally, a composite index uses all columns for sorting/searching.
β
With INCLUDE, extra columns are just stored in index, not used for ordering.
Example:
CREATE INDEX idx_username_include_email ON users (username) INCLUDE (email);
Meaning:
usernameis indexed and ordered.emailis only stored alongside.
Now query:
SELECT email FROM users WHERE username = 'bob';
β Index-only scan β no heap fetch.
Advantages:
- Smaller & faster than normal composite indexes.
- Helps to create very efficient covering indexes.
Important:
- MySQL 8.0 added something similar with
INVISIBLEcolumns but it’s still different.
3. π― Composite Index Optimization
β Always order columns inside index smartly based on query pattern.
Golden Rules:
βοΈ Equality columns first (WHERE col = ?)
βοΈ Range columns second (WHERE col BETWEEN ?)
βοΈ SELECT columns last (for covering)
Example:
If query is:
SELECT email FROM users WHERE status = 'active' AND created_at > '2024-01-01';
Best index:
CREATE INDEX idx_users_status_created_at ON users (status, created_at) INCLUDE (email);
β¦οΈ status first (equality match)
β¦οΈ created_at second (range)
β¦οΈ email included (covering)
Bad Index: (wrong order)
CREATE INDEX idx_created_at_status ON users (created_at, status);
β Will not be efficient!
4. π― BRIN Indexes (PostgreSQL ONLY, super special!)
β
When your table is very huge (millions/billions of rows),
β
And rows are naturally ordered (like timestamp, id increasing),
β
You can create a BRIN (Block Range Index).
Example:
CREATE INDEX idx_users_created_at_brin ON users USING BRIN (created_at);
β¦οΈ BRIN stores summaries of large ranges of pages (e.g., min/max timestamp per 128 pages).
β¦οΈ Ultra small index size.
β¦οΈ Very fast for large range queries like:
SELECT * FROM users WHERE created_at BETWEEN '2024-01-01' AND '2024-04-01';
Important:
- BRIN β B-tree
- BRIN is approximate, B-tree is precise.
- Only useful if data is naturally correlated with physical storage order.
MySQL?
- MySQL does not have BRIN natively. PostgreSQL has a big advantage here.
5. π― Hash Indexes (special case)
β
If your query is always exact equality (not range),
β
You can use hash indexes.
Example:
CREATE INDEX idx_users_username_hash ON users USING HASH (username);
Useful for:
- Simple
WHERE username = 'bob' - Never ranges (
BETWEEN,LIKE, etc.)
β οΈ Warning:
- Hash indexes used to be “lossy” before Postgres 10.
- Now they are safe, but usually B-tree is still better unless you have very heavy point lookups.
π PRO-TIP: Which Index Type to Use?
| Use case | Index type |
|---|---|
| Search small ranges or equality | B-tree |
| Search on huge tables with natural order (timestamps, IDs) | BRIN |
| Only exact match, super heavy lookup | Hash |
| Search only small part of table (active users, special conditions) | Partial index |
| Need to skip heap fetch | INCLUDE / Covering Index |
πΊοΈ Quick Visual Mindmap:
Your Query
β
βββ Need Equality + Range? β B-tree
β
βββ Need Huge Time Range Query? β BRIN
β
βββ Exact equality only? β Hash
β
βββ Want Smaller Index (filtered)? β Partial Index
β
βββ Want to avoid Heap Fetch? β INCLUDE columns (Postgres) or Covering Index
π Now we Know:
π§ Partial Indexes
π§ INCLUDE Indexes
π§ Composite Index order tricks
π§ BRIN Indexes
π§ Hash Indexes
π§ How to choose best Index
β This is serious pro-level database knowledge.
Enjoy SQL! π