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! ๐