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! 🚀
One thought on “Rails 8 App: Setup Test DB in PostgreSQL | Covering Index | BRIN Indexes | Hash Indexes | Create super fast indexes”