This query looks ordinary:
SELECT *
FROM service
WHERE branch_id = 2
AND status = 'FINISHED'
AND service_date BETWEEN '2026-01-01' AND '2026-01-31'
ORDER BY service_date DESC;
With a small dataset, a query like this may appear completely fine.
Lab 02 does not rely on assumptions. The query is tested using EXPLAIN (ANALYZE, BUFFERS) against the same dataset.
Dataset
Table: service
Total rows:
500,000
Status distribution:
| Status | Fraction | Rows |
|---|---|---|
| FINISHED | 70.00% | 350,000 |
| CANCELLED | 20.00% | 100,000 |
| IN_PROGRESS | 5.00% | 25,000 |
| WAITING | 4.90% | 24,500 |
| PENDING_REFUND | 0.10% | 500 |
Branch 2 is the busiest branch, containing 25.00% of the data.
Baseline
The baseline only has constraint-backed indexes:
service_pkey
service_invoice_no_key
There is no secondary index on:
branch_id
status
service_date
Baseline query:
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM service
WHERE branch_id = 2
AND status = 'FINISHED'
AND service_date BETWEEN '2026-01-01' AND '2026-01-31'
ORDER BY service_date DESC;
What should be inspected:
- plan node;
- actual rows;
- rows removed by filter;
- explicit
Sort; - shared read / shared hit;
- planning time;
- execution time.
Add the Candidate Index
CREATE INDEX idx_service_branch_status_date
ON service(branch_id, status, service_date DESC);
This index follows the query shape:
branch_id equality
→ status equality
→ service_date range/order
It is a candidate for this specific query, not a universal index.
Run the Same Query Again
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM service
WHERE branch_id = 2
AND status = 'FINISHED'
AND service_date BETWEEN '2026-01-01' AND '2026-01-31'
ORDER BY service_date DESC;
Compare:
| Condition | What to inspect |
|---|---|
| Before index | Seq Scan, Sort, buffers, rows, time |
| After index | Index usage, Index Cond, Sort presence, buffers, rows, time |
The lab does not store fixed execution-time numbers. Those values should come from the local run.
Cardinality Is Not Match Fraction
status has 5 distinct values. That is its cardinality.
Match fraction is the proportion of rows that match a predicate.
| Predicate | Match fraction | Expected / likely plan |
|---|---|---|
status = 'FINISHED' |
70.0% | Seq Scan likely cheaper |
status = 'PENDING_REFUND' |
0.1% | index-based plan likely cheaper |
The lesson is that low cardinality does not automatically make an index useless.
Column Order Matters
The lab compares:
CREATE INDEX idx_service_a_branch_status_date
ON service(branch_id, status, service_date);
CREATE INDEX idx_service_b_date_branch_status
ON service(service_date, branch_id, status);
CREATE INDEX idx_service_c_status_date_branch
ON service(status, service_date, branch_id);
For the main query, the first index becomes a strong candidate because the leading equality predicates can narrow the scan range more effectively.
ORDER BY + LIMIT
Dashboard query:
SELECT *
FROM service
WHERE branch_id = 2
AND status = 'FINISHED'
ORDER BY service_date DESC
LIMIT 20;
Index:
CREATE INDEX idx_service_branch_status_date_desc
ON service(branch_id, status, service_date DESC);
What to inspect:
- whether
Sortis absent; - whether the plan uses
Index ScanorIndex Only Scan; - how many rows are examined before
LIMITis satisfied.
Index Is Not Free
The lab also tests:
- inserting 1,000 rows without secondary indexes;
- inserting 1,000 rows with 1 composite index;
- inserting 1,000 rows with 4 secondary indexes;
- updating indexed vs non-indexed columns;
- storage cost using
pg_relation_size,pg_indexes_size, andpg_total_relation_size.
An index can improve specific reads, but it also adds write and storage cost.
Key Takeaways
- Do not optimize based on guesses.
- Use
EXPLAIN (ANALYZE, BUFFERS). - A composite index should follow the query shape.
- Column order matters.
- Cardinality is different from match fraction.
- A Seq Scan can be the correct plan.
- Indexes have write and storage costs.
Hands-on Lab
Software Engineering Lab #02 — Database Index
Author: Lukman
GitHub: lukman-ss
Repository:
https://github.com/lukman-ss/software-engineering-lab/tree/main/labs/02-database-index
Top comments (0)