Query Optimization — Cost Models & Hints Advanced
ধীরে চলা কোয়েরিকে ১০০× দ্রুত করা — বাস্তবে
1. The Optimizer in One Sentence
The previous module showed how the database executes a query. This one is about why it picks one plan over another. A modern cost-based optimizer is a tiny chess engine: given many legal plans, it estimates the cost of each and picks the cheapest.
2. Selectivity & Cardinality — The Two Numbers That Decide Everything
Two estimates drive virtually every cost-based decision:
- Cardinality — how many rows a step is expected to produce.
- Selectivity — for a predicate, the fraction of input rows that survive it. Cardinality after a filter ≈ input cardinality × selectivity.
orders has 1,000,000 rows. The predicate
city = 'Dhaka' has selectivity 0.40 (40% of rows). Estimated rows after the filter ≈ 400,000.
Selectivity comes from statistics the database keeps about your data — number of
distinct values per column, min/max, and (in advanced systems) histograms. They are refreshed by an
ANALYZE command (Postgres, SQLite) or automatically (MySQL, SQL Server).
CREATE TABLE orders(id INTEGER, city TEXT, amount INTEGER);
INSERT INTO orders VALUES
(1,'Dhaka',3200),(2,'Dhaka',1500),(3,'Chittagong',2800),
(4,'Dhaka',3150),(5,'Sylhet',2600),(6,'Dhaka',3350);
-- Tell SQLite to gather statistics about row counts and distributions.
ANALYZE;
-- The query plan can now use those stats.
EXPLAIN QUERY PLAN
SELECT SUM(amount) FROM orders WHERE city = 'Dhaka';
ANALYZE, the optimizer is making
decisions on outdated reality. A fast query may suddenly take 100× longer overnight after a bulk load.
Re-run ANALYZE after large data changes.
3. Inside the Cost Model
Every operator (sequential scan, index lookup, hash join, sort, ...) has a cost formula. A simplified model looks like:
cost = cpu_per_row × rows + io_per_page × pages
The optimizer builds a tree of operators, totals the costs, and the cheapest tree wins.
Example: filter on indexed vs unindexed column
| Plan | What it does | Approx. cost |
|---|---|---|
| Sequential scan | Read every page; check predicate per row. | io × N_pages + cpu × N_rows |
| Index range scan | Use B-tree to jump to matching rows. | O(log N) seeks + io × matched_pages |
For a predicate that selects 0.1% of rows (selectivity 0.001), the index plan typically wins by orders of magnitude. For one that selects 80% of rows, the seq scan often wins (because index lookups become random I/O all over the disk).
4. Join Order & Algorithms
For three or more tables, the optimizer must decide both which order to join them and which physical algorithm to use for each join.
| Algorithm | Best when | Cost |
|---|---|---|
| Nested-loop join | Inner side has a useful index; one side is small. | O(outer × log(inner)) |
| Hash join | Both sides fit in memory; equality predicate. | O(outer + inner) |
| Merge join | Both sides already sorted on the join key. | O(outer + inner) after sort |
WHERE conditions into each base table before any
join — sometimes called predicate push-down. Smaller intermediate results = cheaper joins.
5. Case Study — 10 Seconds → 10 Milliseconds
A real e-commerce dashboard runs:
-- The slow query: top customers by spend in Dhaka in the last 7 days.
EXPLAIN QUERY PLAN
SELECT c.name, SUM(o.amount) AS total
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE c.city = 'Dhaka'
AND o.placed_at >= '2026-05-01'
GROUP BY c.id, c.name
ORDER BY total DESC;
What the plan exposes (in production, on millions of rows):
- Sequential scan on
customers— there is no index oncity. - Sequential scan on
orders— no index onplaced_atorcustomer_id. - Nested-loop join over both unfiltered tables. Disaster.
-- Add the right indexes.
CREATE INDEX idx_customers_city ON customers(city);
CREATE INDEX idx_orders_cust_date ON orders(customer_id, placed_at);
ANALYZE;
EXPLAIN QUERY PLAN
SELECT c.name, SUM(o.amount) AS total
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE c.city = 'Dhaka'
AND o.placed_at >= '2026-05-01'
GROUP BY c.id, c.name
ORDER BY total DESC;
Now the plan uses the city index to find Dhaka customers, then for each one uses the composite index
on (customer_id, placed_at) as a clean range probe. From a 10-second sequential disaster
to a 10-millisecond targeted lookup — same query, smarter indexes.
6. Hints — Use Sparingly, If at All
Most major databases let you force a particular plan with a "hint":
- Oracle — comment-style hints:
SELECT /*+ INDEX(orders idx_date) */ ... - SQL Server —
WITH (INDEX(idx_date)) - MySQL —
USE INDEX(idx_date)/FORCE INDEX - PostgreSQL — no built-in hints; use the
pg_hint_planextension. - SQLite — no plan hints. The optimizer takes
ANALYZEdata and decides.
WHERE func(col) = x can't use an index on col). Use hints only as a
documented last resort with a follow-up ticket to remove them.
7. The Slow-Query Checklist
- Run
EXPLAIN/EXPLAIN ANALYZE/EXPLAIN QUERY PLAN. Read it. - Look for sequential scans on big tables — usually a missing index.
- Check estimated vs actual row counts. Big mismatch ⇒ run
ANALYZE. - Check that filters use indexable expressions.
WHERE date(placed_at) = '2026-05-01'disables the index;WHERE placed_at >= '2026-05-01' AND placed_at < '2026-05-02'uses it. - For multi-column predicates, ensure the index column order matches.
- Avoid
SELECT *when only a few columns are needed — covering indexes can serve index-only scans when the index has all the columns. - For hot read paths, consider materialized views or caches.
- Only after all the above: consider a hint, and document why.
EXPLAIN পড়ুন। ৯০% সমস্যা index, statistics বা
non-sargable predicate-এর কারণে হয়।
8. Glossary (শব্দকোষ)
| Term | Meaning | বাংলায় |
|---|---|---|
| Cardinality | Estimated number of rows produced by a step. | একটি ধাপের প্রত্যাশিত row সংখ্যা। |
| Selectivity | Fraction of input rows surviving a predicate. | একটি predicate-এ যত fraction row বেঁচে যাবে। |
| Statistics | Summary info about column distributions, refreshed by ANALYZE. | Column বিতরণ-এর সারাংশ; ANALYZE দিয়ে আপডেট। |
| Cost-based optimizer | Picks the cheapest plan based on estimated cost. | আনুমানিক খরচের ভিত্তিতে সবচেয়ে সস্তা plan বেছে নেয়। |
| Sargable predicate | One that can use an index — e.g. col >= ?. | Index ব্যবহার-উপযোগী predicate। |
| Hint | Comment / clause that forces a particular plan. | Optimizer-কে নির্দিষ্ট plan ব্যবহার করতে বাধ্য করার নির্দেশ। |
9. Practice Problems
-
Show the query plan for
SELECT * FROM orders WHERE city='Dhaka';with no index, then add an index oncityand show the plan again.Index যোগ করার আগে ও পরে plan তুলনা করুন।✨ Show Answer
ans1.sqlEXPLAIN QUERY PLAN SELECT * FROM orders WHERE city='Dhaka'; CREATE INDEX idx_city ON orders(city); ANALYZE; EXPLAIN QUERY PLAN SELECT * FROM orders WHERE city='Dhaka'; -
Why does
WHERE date(placed_at) = '2026-05-01'defeat an index onplaced_at? Rewrite it to use the index.কেন function-wrapped predicate index-কে কাজ করতে দেয় না?✨ Show Answer
Answer: The index stores the raw
placed_atvalues;date(placed_at)is a function-wrapped expression, and the optimizer cannot translatedate(col) = ?into a B-tree range. Rewrite as a sargable range:WHERE placed_at >= '2026-05-01' AND placed_at < '2026-05-02' -
If
customershas 10,000 rows andordershas 10,000,000, which side should the optimizer scan first when joining? Why?কোন side-কে আগে scan করা ভালো?✨ Show Answer
Answer: Apply filters first. After
city='Dhaka'on customers (selectivity ~0.4) → ~4,000 customers. Then for each, probe an index onorders(customer_id). We do 4,000 index probes instead of scanning 10M rows. The smaller-after-filter side drives the join. -
In one sentence, explain why a query that was fast yesterday became slow overnight after a bulk insert.কেন overnight কোয়েরি ধীর হয়ে যায়?
✨ Show Answer
Answer: The optimizer's statistics are stale: it still believes the table is small, picks a nested-loop plan, and that plan blows up against the new row count. Re-run
ANALYZEto refresh statistics. -
When is a sequential scan actually the right plan?কখন seq scan ভালো?
✨ Show Answer
Answer: When selectivity is high (the predicate keeps a large fraction of rows) and/or the table is small enough to fit in memory. Random I/O of an index lookup over millions of rows often costs more than a streaming sequential read.
-
Apply the slow-query checklist to:
SELECT * FROM users WHERE LOWER(email) = '...'. What's wrong, and how do you fix it?কোয়েরিটি optimize করুন।✨ Show Answer
Answer:
LOWER(email)defeats a normal index onemail. Two fixes:- Store emails in lowercase at insert time and search with
WHERE email = '...'. Index works. - Create a functional index:
CREATE INDEX idx_email_lower ON users(LOWER(email));(Postgres / SQLite). The optimizer can now use it for the original query.
- Store emails in lowercase at insert time and search with
Summary — Module 35
The optimizer picks the cheapest plan from many candidates, guided by statistics
(refreshed by ANALYZE) and a cost model. Selectivity and cardinality
estimates drive everything; small mistakes amplify across joins. Most slow queries are caused by
missing indexes, stale statistics, or non-sargable predicates — fix the cause first, hint as a last
resort, and always use EXPLAIN to verify your fix actually changed the plan.