Hook: Interviewers love selectivity because it explains why the same index can be blazing fast for one query and useless for another.
Question: What does selectivity mean in SQL, especially for indexes and performance?
Answer: Selectivity is the fraction of rows that match a filter. High selectivity means only a small part of the table matches, so an index is usually helpful; low selectivity means many rows match, so a full scan may be cheaper. The optimizer uses table statistics to estimate this before it picks a plan.
Interview-Ready Answer: In SQL, selectivity means how many rows a predicate keeps. I think of it as how picky the WHERE clause is. A filter like email = 'a@x.com' is highly selective because it usually finds one row, so an index is a great fit. A filter like status = 'open' is often low selectivity because it may match a big chunk of the table, so the optimizer may prefer a sequential scan or a different index strategy.
Selectivity is the fraction of rows that survive a filter. If a table has 1,000,000 rows and 1,000 match, the selectivity is 0.1%. In planner terms, this is part of cardinality estimation, which means the database's guess about how many rows a step will output.
WHERE customer_email = 'amy@example.com' or WHERE status = 'open'.| Pattern | Selectivity | Index helpful? | Typical outcome |
|---|---|---|---|
| Unique email | High | Yes | Index scan |
| Status flag | Low | Often no | Seq scan |
| Status + date range | Medium | Maybe | Depends on stats |
A B-tree index is a sorted structure. It is very good when the query can jump to a tiny slice of the data, then read only a few matching rows. The tree walk itself is roughly O(log n), but the real cost includes fetching the matching rows. That is why selectivity matters more than the word index alone.
For a table with 10 million rows, a lookup that returns one row may touch only a few index pages plus one table row. A lookup that returns 3 million rows can become expensive because the engine must visit many rows, and random I/O can dominate. In that case, a sequential scan may win because it reads pages in order. There is no universal cutoff like always use an index below 10%; the right answer depends on table size, cache warmth, row clustering, and whether the query is covered by the index.
status or is_active often have low selectivity, so a plain index on them may not help much.LOWER(email) can hide the normal index because the engine cannot use the raw stored values directly.Memory rule: ask yourself whether the query is looking for one needle or a whole haystack. One needle favors an index; a haystack often does not.
Real-World Example: In an e-commerce checkout service, the team had an orders table with millions of rows. A dashboard query filtered by status = 'PAID' and expected to be fast because there was an index on status. The problem was that most recent orders were already paid, so the predicate was low selectivity. The database had to bounce through lots of matching index entries and then fetch many table rows, which made p95 latency jump from tens of milliseconds to nearly a second.
The outage symptoms were easy to miss at first: CPU stayed moderate, but disk reads spiked, slow query logs showed many rows examined, and support agents saw the dashboard timing out during peak traffic. The fix was to use a more selective access pattern: a composite index for the common dashboard filter, plus a summary table for reporting. The lesson was simple: an index is not magic; it only helps when the filter narrows the data enough.
-- Demonstrates high vs low selectivity on the same table.
-- The data is tiny here, but the pattern is what matters on a real production table.
DROP TABLE IF EXISTS tickets;
CREATE TABLE tickets (
ticket_id INTEGER PRIMARY KEY,
customer_email VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at DATE NOT NULL
);
INSERT INTO tickets (ticket_id, customer_email, status, created_at) VALUES
(1, 'amy@example.com', 'open', DATE '2026-01-01'),
(2, 'bob@example.com', 'open', DATE '2026-01-02'),
(3, 'carla@example.com', 'open', DATE '2026-01-03'),
(4, 'dave@example.com', 'closed', DATE '2026-01-03'),
(5, 'erin@example.com', 'open', DATE '2026-01-04'),
(6, 'frank@example.com', 'open', DATE '2026-01-05'),
(7, 'gina@example.com', 'closed', DATE '2026-01-05'),
(8, 'hugo@example.com', 'open', DATE '2026-01-06'),
(9, 'ivy@example.com', 'open', DATE '2026-01-07'),
(10, 'joel@example.com', 'open', DATE '2026-01-08'),
(11, 'kate@example.com', 'open', DATE '2026-01-09'),
(12, 'leo@example.com', 'closed', DATE '2026-01-10');
-- A unique-ish lookup like email is highly selective: the index can jump to one row quickly.
CREATE INDEX idx_tickets_customer_email ON tickets(customer_email);
-- Status has much lower selectivity here: many rows share the same value, so this index is weaker.
CREATE INDEX idx_tickets_status ON tickets(status);
-- High-selectivity query: usually one row.
SELECT ticket_id, customer_email, status, created_at
FROM tickets
WHERE customer_email = 'amy@example.com';
-- Low-selectivity query: many rows match, so the planner may prefer a scan on a large table.
SELECT ticket_id, customer_email, status, created_at
FROM tickets
WHERE status = 'open'
ORDER BY created_at;
-- Edge case: wrapping the column in a function changes the search key.
-- A plain index on customer_email cannot directly match LOWER(customer_email) on many engines.
SELECT ticket_id, customer_email, status, created_at
FROM tickets
WHERE LOWER(customer_email) = 'amy@example.com';
-- Sanity check: compare how many rows each filter matches.
SELECT 'customer_email=amy@example.com' AS predicate, COUNT(*) AS matched_rows
FROM tickets
WHERE customer_email = 'amy@example.com'
UNION ALL
SELECT 'status=open', COUNT(*)
FROM tickets
WHERE status = 'open';Follow-up & Tricky Questions:
WHERE LOWER(col) = ... use the normal index? Usually not. The function changes the value being searched, so you often need an expression index or normalized data.Common Mistakes:
Memory Hook: Think of a phonebook: if you want one person's name, the index is perfect; if you want half the city, just read the list.
Cheat Sheet:
Practice Tasks: