By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
(B-tree, Bitmap, Covering Index, Partial Index)
You’re analyzing a 500M-row sales table in PostgreSQL. A simple WHERE customer_id = 12345 query takes 12 seconds—unacceptable for a dashboard. Your boss asks, "Why is this slow? Can we fix it without rewriting the app?"
WHERE customer_id = 12345
This is where indexing strategies come in.Indexes are pre-sorted lookup structures that let the database skip scanning the entire table. Think of them like a book’s index: instead of reading every page to find "SQL tuning," you flip to the index, see "page 42," and jump straight there.
Why this matters in production:- Performance: A missing or poorly chosen index can turn a 100ms query into a 10-second disaster.- Cost: Slow queries burn CPU, memory, and cloud bills (e.g., AWS RDS charges by the hour).- Scalability: A table with 1M rows today might have 100M tomorrow. Indexes keep queries fast as data grows.- Legacy systems: You’ll inherit databases with no indexes, or worse—too many indexes (which slow down writes).
Real-world scenario:You’re optimizing a reporting system for an e-commerce company. The orders table has 200M rows, and analysts run queries like:
orders
SELECT customer_id, SUM(amount) FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31' AND status = 'completed' GROUP BY customer_id;
Without the right indexes, this query could take minutes. With the right ones, it finishes in <100ms.
=
>
<
BETWEEN
LIKE 'prefix%'
LIKE '%suffix'
customer_id
WHERE customer_id = 123
gender
status
user_id
['pending', 'completed', 'cancelled']
-- Covering index: CREATE INDEX idx_orders_covering ON orders(customer_id) INCLUDE (order_date); ```
WHERE status = 'active'
WHERE is_active = true
sql -- Only index "active" users: CREATE INDEX idx_active_users ON users(email) WHERE is_active = true;
(last_name, first_name)
(A, B, C)
(A)
(A, B)
(B, C)
sql -- Good for: WHERE last_name = 'Smith' AND first_name = 'John' CREATE INDEX idx_name ON users(last_name, first_name);
sql -- Index a timestamp column in a 500M-row table: CREATE INDEX idx_orders_brin ON orders USING BRIN(order_date);
WHERE tags @> ARRAY['urgent']
WHERE json_data->>'key' = 'value'
sql -- Index a JSON column: CREATE INDEX idx_orders_gin ON orders USING GIN(json_metadata);
generate_series
SELECT
EXPLAIN ANALYZE
You have a sales table with 10M rows:
sales
CREATE TABLE sales ( id SERIAL PRIMARY KEY, customer_id INT NOT NULL, product_id INT NOT NULL, sale_date TIMESTAMP NOT NULL, amount DECIMAL(10, 2) NOT NULL, status VARCHAR(20) NOT NULL -- 'pending', 'completed', 'cancelled' );
A query is slow:
-- Takes 8 seconds on 10M rows EXPLAIN ANALYZE SELECT customer_id, SUM(amount) FROM sales WHERE sale_date BETWEEN '2023-01-01' AND '2023-12-31' AND status = 'completed' GROUP BY customer_id;
Run EXPLAIN ANALYZE to see the query plan:
EXPLAIN ANALYZE SELECT customer_id, SUM(amount) FROM sales WHERE sale_date BETWEEN '2023-01-01' AND '2023-12-31' AND status = 'completed' GROUP BY customer_id;
Output (simplified):
Seq Scan on sales (cost=0.00..350000.00 rows=1000000 width=12) (actual time=0.123..8123.456 rows=123456 loops=1) Filter: ((sale_date >= '2023-01-01'::timestamp) AND (sale_date <= '2023-12-31'::timestamp) AND (status = 'completed'::text)) Rows Removed by Filter: 9876543
Key takeaways:- Seq Scan (full table scan): The database reads every row (10M!) because there’s no index.- Rows Removed by Filter: 9.8M rows were discarded after scanning—wasteful.
Create an index on (sale_date, status) to cover the WHERE clause:
(sale_date, status)
WHERE
CREATE INDEX idx_sales_date_status ON sales(sale_date, status);
Why this order?- sale_date is range-filtered (BETWEEN), so it goes first.- status is equality-filtered (=), so it goes second.
sale_date
Run EXPLAIN ANALYZE again:
Index Scan using idx_sales_date_status on sales (cost=0.42..12345.67 rows=123456 width=12) (actual time=0.045..123.456 rows=123456 loops=1) Index Cond: ((sale_date >= '2023-01-01'::timestamp) AND (sale_date <= '2023-12-31'::timestamp) AND (status = 'completed'::text))
Key improvements:- Index Scan: Now uses the index (no full table scan).- Time: Dropped from 8s → 123ms (65x faster!).
The query still needs to read the table to get customer_id and amount. Add them to the index:
amount
CREATE INDEX idx_sales_covering ON sales(sale_date, status) INCLUDE (customer_id, amount);
Verify:
Output:
Index Only Scan using idx_sales_covering on sales (cost=0.42..12345.67 rows=123456 width=12) (actual time=0.034..87.654 rows=123456 loops=1)
Key improvements:- Index Only Scan: Never touches the table (all data comes from the index).- Time: Dropped from 123ms → 87ms (30% faster).
If most queries filter on status = 'completed', create a partial index:
status = 'completed'
CREATE INDEX idx_sales_completed ON sales(sale_date) INCLUDE (customer_id, amount) WHERE status = 'completed';
Index Only Scan using idx_sales_completed on sales (cost=0.28..8765.43 rows=123456 width=12) (actual time=0.023..65.432 rows=123456 loops=1)
Key improvements:- Smaller index: Only includes status = 'completed' rows.- Time: Dropped from 87ms → 65ms (25% faster).
JOIN
ORDER BY
GROUP BY
is_active
(status, sale_date)
WHERE sale_date BETWEEN ... AND status = 'completed'
WHERE status = 'completed' AND sale_date BETWEEN ...
INSERT
UPDATE
DELETE
pg_stat_user_indexes
sys.dm_db_index_usage_stats
idx_<table>_<column(s)>
idx_orders_customer_id
sql COMMENT ON INDEX idx_sales_covering IS 'Covers 90% of reporting queries. Do not drop without checking dashboard performance.';
pg_repack
ONLINE INDEX REBUILD
sql SELECT schemaname, relname, indexrelname, idx_scan FROM pg_stat_user_indexes ORDER BY idx_scan ASC;
sql SELECT OBJECT_NAME(i.object_id) AS table_name, i.name AS index_name, ius.user_seeks + ius.user_scans + ius.user_lookups AS reads, ius.user_updates AS writes FROM sys.indexes i JOIN sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id ORDER BY reads ASC;
seq_scan
pg_stat_activity
EXPLAIN
INCLUDE
❌ B-tree (wastes space).
"You have a query WHERE last_name = 'Smith' AND first_name = 'John'. Which composite index is best?"
WHERE last_name = 'Smith' AND first_name = 'John'
❌ (first_name, last_name) (won’t be used for last_name alone).
(first_name, last_name)
last_name
"A query uses an index but is still slow. What’s likely missing?"
❌ More RAM (not the root cause).
"When should you use a partial index?"
WHERE last_name = 'Smith'
id
"You have a 1B-row table with a timestamp column. Most queries filter on timestamp and status. The table is write-heavy. What’s the best indexing strategy?"- ✅ Composite index on (timestamp, status) (covers most queries).- ✅ Partial index on timestamp WHERE status = 'active' (if most queries filter on status = 'active').- ❌ Bitmap index on status (PostgreSQL doesn’t support it; use BRIN instead).- ❌ Index on timestamp alone (won’t help with status filters).
timestamp
(timestamp, status)
timestamp WHERE status = 'active'
status = 'active'
You have a users table:
users
CREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL, is_active BOOLEAN NOT NULL DEFAULT true, last_login TIMESTAMP, signup_date DATE NOT NULL );
Query 1: SELECT email FROM users WHERE is_active = true AND signup_date > '2023-01-01'; Query 2: `SELECT email, last_login FROM users
SELECT email FROM users WHERE is_active = true AND signup_date > '2023-01-01';
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.