Chapter 11 · Index-Aware Logical and Physical Design

Selectivity, Cardinality, and Index Candidates

Use selectivity, cardinality, data distribution, predicate shape, and workload frequency to identify promising index candidates rather than indexing columns by habit.

Beginner65–90 minutesSelectivity + candidate analysisLast reviewed: August 2026

Learning outcomes

Not every frequently filtered column deserves an index. A useful index candidate depends on how many rows exist, how values are distributed, how selective the predicate is, how often the query runs, and how expensive writes are. Cardinality and selectivity provide the vocabulary for reasoning about this.

01

Distinguish row count, distinct-value cardinality, and predicate selectivity.

02

Recognize when low-selectivity indexes are weak or still useful.

03

Account for skewed distributions rather than assuming uniform data.

04

Rank index candidates from real workload evidence.

Table cardinality

In database discussions, cardinality can mean several related things. In query planning, it often refers to the estimated number of rows produced by an operation. In data modeling, it also describes relationship multiplicity. Here we focus on row counts and distinct values.

Distinct-value cardinality

Suppose WorkOrder has 10 million rows:

model · example
status_code       6 distinct valuesasset_id          2.5 million distinct valueswork_order_id    10 million distinct values

These columns have very different value cardinalities.

Selectivity

Predicate selectivity is roughly the fraction of table rows that match a condition.

If:

model · example
WHERE work_order_id = 84217

matches one of 10 million rows, it is extremely selective.

If:

model · example
WHERE status_code = 'closed'

matches 7 million rows, it is not selective.

Why selectivity matters

An index lookup is useful when it avoids reading large amounts of irrelevant data. If a condition matches most of the table, the optimizer may prefer a sequential scan because following millions of index entries back to table rows can cost more.

Low-cardinality does not mean “never index”

A boolean or status column may still be useful when the filtered subset is small. Example:

model · example
is_deleted = false

is weak if 99.9% of rows are false. But:

model · example
is_deleted = true

may identify a tiny subset. A partial index may target exactly that subset.

Distribution matters

Suppose WorkOrder status distribution is:

model · example
closed        80%cancelled      8%in_progress    5%scheduled      4%open           2%blocked        1%

An index on status_code may be valuable for rare statuses but unattractive for closed.

Skew breaks uniform assumptions

Statistics help the optimizer understand whether some values are much more common than others. Without good statistics, it may estimate poorly and choose an inefficient plan.

Frequency matters too

A highly selective query run once per month may not justify an index that slows every write. A moderately selective query executed thousands of times per second may justify significant indexing effort.

Candidate rule

Prioritize by workload impact: frequency × cost saved, balanced against write and storage cost.

Predicate shape matters

These are not equivalent from an indexing perspective:

model · example
WHERE customer_id = ?WHERE lower(email) = ?WHERE opened_at BETWEEN ? AND ?WHERE description LIKE '%pump%'WHERE status_code IN ('open','blocked')

Different predicates may need ordinary, expression, range-friendly, or specialized indexes.

Sargability

A predicate is often called sargable when the database can use an index search argument effectively. For example:

model · example
WHERE opened_at >= :start  AND opened_at < :end

is generally friendlier than wrapping the indexed column in a function that prevents direct lookup.

Expression indexes can rescue intentional expressions

If business semantics require case-insensitive lookup:

model · example
WHERE lower(email) = lower(:email)

an expression index on lower(email) may align the physical structure with the predicate.

Join selectivity

Index candidates are not limited to WHERE columns. Join keys matter because they determine how efficiently related rows can be located.

WorkshopHub workload examples

QueryLikely selectivityCandidate
Part by exact SKUVery highUNIQUE(sku)
WorkOrders for one AssetHigh/mediumwork_order(asset_id)
All closed WorkOrdersLowStandalone status index may be weak
Open WorkOrders for one CustomerPotentially highComposite/indexed join path
Assignments for one Technician this weekHigh(technician_id, started_at)

Estimate rows before indexing

For a candidate query, estimate:

  • table row count;
  • rows matching equality predicates;
  • rows matching range predicates;
  • rows after joins;
  • rows ultimately returned to the application.

Index candidates from access patterns

Create an access-pattern inventory:

model · example
Q1: get Part by SKUQ2: list WorkOrders by Asset ordered newest-firstQ3: list open WorkOrders by CustomerQ4: list active assignments by TechnicianQ5: search parts by description text

Then design indexes to support the highest-impact queries.

Do not index every foreign key blindly

Foreign keys are strong candidates, especially for joins and parent modification checks, but tiny tables or rarely queried relationships may not need dedicated indexes. Measure and inspect plans.

Statistics can become stale

Large data changes can invalidate optimizer assumptions. Maintenance processes such as ANALYZE/statistics refresh are part of physical design operations.

Practice: rank index candidates

Candidate ranking

A 20-million-row WorkOrder table receives:

  1. 50,000 lookups/day by work_order_number, unique.
  2. 5 reports/day filtering status='closed', matching 75% of rows.
  3. 100,000 lookups/day by asset_id, average 12 rows returned.

Rank the likely index value.

Review answer

Unique work_order_number and asset_id are strong candidates because they are selective and frequent. A standalone closed-status index is much weaker because the predicate matches most rows and the report is infrequent; a partial/composite design for a rarer operational subset may be more useful.

Summary and next lesson

Selectivity, cardinality, skew, predicate shape, and workload frequency determine whether an index is worth its cost. The next lesson moves from single-column candidates to composite indexes, where column order determines which query shapes can use the index efficiently.

References

  • PostgreSQL documentation on planner statistics and indexes.
  • Markus Winand, SQL Performance Explained.
  • Use The Index, Luke! documentation and examples.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.