Index Selection

A retail analytics database serves two workloads. Analysts run ad-hoc range queries over `sales(date, store_id, amount)`. Meanwhile, a checkout system inserts thousands of new sale rows per minute. Every index that speeds up the analysts' queries also slows down every insert. This happens because the database must maintain that index's structure on each write. The administrator must decide which indexes to build. This decision comes from a huge combinatorial space of candidate B+-Tree Indexes and Hash Indexes. Build too few, and range scans crawl over millions of rows. Build too many, and inserts stall under write amplification. Meanwhile, disk fills with rarely-used structures. This is the index selection problem: given a workload of queries and updates, choose a subset of indexes that minimizes total cost. That cost is query execution time plus index maintenance overhead. The choice is subject to a storage budget. It sits at the intersection of query optimization and physical database design. It is squarely a combinatorial optimization problem. The space of candidate indexes is exponential. Evaluating each candidate's benefit requires invoking a full query optimizer. The objective trades off conflicting goals for different workload components. In real systems this is not an academic curiosity. Every major database vendor ships an "index advisor" or "database tuning advisor." These tools automate exactly this decision. Manual index design does not scale past a handful of tables. Oracle's SQL Tuning Advisor, Microsoft's Database Engine Tuning Advisor, and IBM's Db2 Index Advisor all implement variants of the algorithms described here. These include workload capture, candidate generation, "what-if" cost estimation. They also include greedy or knapsack-style selection under a storage constraint. This article develops the index selection problem formally. It presents the standard greedy and knapsack-based algorithms used in practice. It proves why greedy achieves a provable approximation guarantee under submodularity. It works through a full numeric trace. That trace shows how an advisor would choose indexes for a small workload. It closes with the pitfalls that make naive index selection perform worse than no selection at all.

Key takeaways
  • Index selection chooses a subset of candidate indexes from a query+update workload to minimize total cost (query time + maintenance overhead) under a storage budget.
  • The candidate space is exponential; practical systems restrict it to "syntactically relevant" indexes appearing in query predicates, joins, and sort/group-by clauses.
  • Benefit of an index is estimated via what-if optimization: ask the query optimizer to cost each query as if the index existed, without building it.
  • The problem is NP-hard in general (reduces to weighted set cover / knapsack); greedy algorithms exploit diminishing returns (submodularity) for a provable (1−1/e)(1 - 1/e) approximation guarantee.
  • Updates impose a maintenance penalty per index, so the true objective is: minimize (query cost saved) − (update cost added), not maximize benefit alone.
  • Real advisors (Db2 Index Advisor, SQL Server DTA, AutoAdmin) combine candidate pruning, what-if costing, and knapsack/greedy search to make this tractable at production scale.
The core insight

Index selection is a constrained combinatorial optimization: pick the index subset maximizing workload-wide net benefit (query speedup minus update overhead) under a storage budget, solved practically with greedy or knapsack heuristics driven by the query optimizer's own cost model.

The idea

Formally, let a workload W={Q1,…,Qn}W = \{Q_1, \dots, Q_n\} be a set of representative queries and updates, each with an associated frequency or weight wiw_i reflecting how often it runs. Let C={I1,…,Im}\mathcal{C} = \{I_1, \dots, I_m\} be a set of candidate indexes — physical structures (B+-tree or hash) that could be built on some table's column or column combination. For any subset S⊆CS \subseteq \mathcal{C} of indexes actually materialized, define cost(Qi,S)\text{cost}(Q_i, S) as the estimated execution cost of query QiQ_i when exactly the indexes in SS exist. The index selection problem is:

min⁡S⊆C, size(S)≤B∑i=1nwi⋅cost(Qi,S)\min_{S \subseteq \mathcal{C},\ \text{size}(S) \le B} \sum_{i=1}^{n} w_i \cdot \text{cost}(Q_i, S)

subject to a storage budget BB on total index size. Notice that cost(Qi,S)\text{cost}(Q_i, S) already folds in update overhead: if QiQ_i is an `INSERT`/`UPDATE`/`DELETE`, its cost includes maintaining every index in SS that covers the modified table, so the objective automatically penalizes over-indexing for write-heavy workloads. This is why index selection cannot be solved by looking at read queries alone — the objective function must be workload-wide, spanning both queries and updates, weighted by relative frequency. The distinctive difficulty of this problem, compared to a textbook Cost Comparison of File Organizations exercise, is that cost(Qi,S)\text{cost}(Q_i, S) is not a closed-form function — it is whatever the query optimizer's cost model says the optimal plan for QiQ_i costs, given that the indexes in SS are available access paths. Evaluating it requires either building the index (too expensive to do for every candidate) or asking the optimizer to simulate having it. This simulation capability is called what-if analysis: the optimizer is given hypothetical index metadata (cardinality, clustering factor, height) without the index existing on disk, and it returns the plan and cost it would choose. What-if analysis is what makes index selection computationally feasible: it decouples "does this index help" from "actually building it and measuring."

How it works

A practical index selection pipeline has four stages: workload capture, candidate generation, cost evaluation (what-if), and search over subsets. Each stage prunes or estimates so the final combinatorial search operates on a small, well-scored candidate set rather than the astronomically large space of all possible indexes.

1. Workload capture. The system logs a representative sample of queries and updates over some observation window, along with their execution frequency (or the DBA supplies a synthetic workload). Each query is parsed to identify which columns appear in `WHERE` predicates, join conditions, `ORDER BY`, and `GROUP BY` clauses — these are the only columns for which an index can plausibly help, drastically narrowing the search.

2. Candidate generation. For each query, generate candidate indexes on:

  • single columns used in equality or range predicates,
  • column combinations used in multi-column predicates or joins (as composite indexes, in the order that maximizes prefix matching),
  • covering indexes that include all columns referenced by a query, avoiding a table lookup entirely.

This yields a candidate set C\mathcal{C} typically of size linear in the workload size times a small constant, not the exponential set of all column subsets.

3. What-if cost evaluation. For each candidate index IjI_j and each query QiQ_i, invoke the optimizer's cost estimator with IjI_j's statistics hypothetically injected into the catalog. The optimizer returns cost(Qi,{Ij})\text{cost}(Q_i, \{I_j\}), the cost of the best plan assuming only IjI_j exists (in addition to indexes already present). Define the benefit of IjI_j for query QiQ_i as

benefit(Ij,Qi)=cost(Qi,∅)−cost(Qi,{Ij})\text{benefit}(I_j, Q_i) = \text{cost}(Q_i, \emptyset) - \text{cost}(Q_i, \{I_j\})

and weight by frequency to get workload-level benefit ∑iwi⋅benefit(Ij,Qi)\sum_i w_i \cdot \text{benefit}(I_j, Q_i).

4. Search over subsets. Given benefits (and interaction effects among indexes — an index can also make other indexes redundant, or a composite index can subsume a single-column one), search for the subset SS maximizing net benefit under the storage budget BB. This is the step where the combinatorics bite, and where the greedy algorithm below is the workhorse.

The greedy algorithm

The key structural fact exploited by every practical index selection algorithm is that the benefit function exhibits diminishing returns: adding an index to an already-large set of indexes helps less than adding it to a small set, because some queries are already well-served. Formally, the total benefit function

f(S)=∑iwi(cost(Qi,∅)−cost(Qi,S))f(S) = \sum_i w_i \big(\text{cost}(Q_i, \emptyset) - \text{cost}(Q_i, S)\big)

is (approximately) monotone and submodular: f(S∪{I})−f(S)≥f(T∪{I})−f(T)f(S \cup \{I\}) - f(S) \geq f(T \cup \{I\}) - f(T) whenever S⊆TS \subseteq T. This licenses a greedy strategy: repeatedly add the index with the highest marginal benefit per unit storage, until the budget is exhausted. ``` ALGORITHM GreedyIndexSelection(candidates C, workload W, budget B) S ← ∅ // selected index set remainingBudget ← B while remainingBudget > 0 and C ≠ ∅: best ← null bestRatio ← -∞ for each candidate I in C: if size(I) > remainingBudget: continue // cannot afford it // marginal benefit of adding I to current set S gain ← 0 for each (query Qi, weight wi) in W: gain ← gain + wi ( cost(Qi, S) - cost(Qi, S ∪ {I}) ) ratio ← gain / size(I) // benefit per unit storage if gain > 0 and ratio > bestRatio: bestRatio ← ratio best ← I if best = null: break // no beneficial candidate left S ← S ∪ {best} remainingBudget ← remainingBudget − size(best) C ← C − {best} return S ``` Each iteration of the outer loop calls the what-if cost estimator once per remaining candidate per query — this is the expensive step, since `cost(Qi, S)` invokes the query optimizer. Systems cache these calls aggressively and often approximate `cost(Qi, S ∪ {I})` using only II's marginal* effect rather than re-optimizing the whole query from scratch, using atomic configurations: precompute cost(Qi,{I})\text{cost}(Q_i, \{I\}) once for every single candidate II in isolation, then approximate the cost under a larger set SS by taking, for each query, the best single index in SS that helps it (a reasonable approximation because most queries are dominated by one index, though it can miss multi-index plans like index intersection).

Knapsack framing

When index interactions are ignored entirely (each index's benefit is independent of which other indexes exist), index selection reduces exactly to the 0/1 knapsack problem: each candidate IjI_j has a "weight" size(Ij)\text{size}(I_j) and a "value" benefit(Ij)=∑iwi⋅benefit(Ij,Qi)\text{benefit}(I_j) = \sum_i w_i \cdot \text{benefit}(I_j, Q_i), and we want to maximize total value subject to total weight ≤B\le B. This connection matters because it tells us precisely how hard the problem is (0/1 knapsack is NP-hard, but has a pseudo-polynomial dynamic program and excellent approximation schemes) and licenses borrowing knapsack solvers — dynamic programming for small budgets, or greedy-by-ratio with a known worst-case bound — whenever interaction effects are weak enough to ignore.

Figure
Index selection pipeline: from workload to selected index set
Workload queries + updates with frequencies wᵢ Candidate Generation columns in WHERE/JOIN/GROUP BY What-if Cost Estimation optimizer simulates each index Greedy / Knapsack Search pick highest benefit/size ratio until storage budget B exhausted Selected Index Set S materialized as B+-tree / hash size(S) ≤ B re-cost with S so far

Complexity & correctness

Hardness. The decision version of index selection — "does there exist S⊆CS \subseteq \mathcal{C} with size(S)≤B\text{size}(S) \le B and total cost ≤K\le K?" — is NP-hard in general. This follows by reduction from weighted set cover: treat each query as an element to be "covered" and each candidate index as covering the set of queries it speeds up past some threshold; choosing a minimum-cost covering set of indexes is exactly weighted set cover, a canonical NP-hard problem. Even the simplified independent-benefit version reduces to 0/1 knapsack (also NP-hard, though only weakly so — it has a pseudo-polynomial DP). So no polynomial-time algorithm is known to find the exact optimum for large instances, and none is expected to exist unless P = NP.

Why greedy is used anyway — the submodularity argument. Although exact optimization is intractable, the benefit function f(S)f(S) is monotone (f(S)≤f(T)f(S) \le f(T) whenever S⊆TS \subseteq T, since more indexes never hurt query cost — ignoring, for the moment, update overhead) and, when index effects on queries don't strongly interact, submodular: the marginal gain of adding any index shrinks as the current set grows. This is the same structural property behind Buffer Management replacement heuristics and greedy set-cover approximations. For monotone submodular maximization under a cardinality or knapsack constraint, a classical result (Nemhauser–Wolsey–Fisher, 1978) guarantees:

f(Sgreedy)≥(1−1e)f(Sopt)≈0.632⋅f(Sopt)f(S_{\text{greedy}}) \ge \left(1 - \frac{1}{e}\right) f(S_{\text{opt}}) \approx 0.632 \cdot f(S_{\text{opt}})

for the cardinality-constrained case, and a similar constant-factor guarantee holds for the budgeted (knapsack) version when the greedy ratio rule (benefit divided by size) is combined with an exhaustive check over small solutions of size 1–3. The proof sketch: let SkS_k be the greedy solution after kk steps and S∗S^* be optimal. Because S∗S^* has at most kk elements not yet in SkS_k, and by submodularity each of those elements would contribute at least as much marginally to SkS_k as it does to smaller sets, one shows f(S∗)−f(Sk)≤k⋅(f(Sk+1)−f(Sk))f(S^*) - f(S_k) \le k \cdot (f(S_{k+1}) - f(S_k)). Unrolling this recursion across all kk steps yields the (1−1/e)(1-1/e) bound. This is the same guarantee that justifies greedy algorithms for influence maximization and sensor placement — index selection is a direct application of that general theory.

Where the guarantee breaks. The bound assumes true submodularity, but real index benefit functions are only approximately submodular: two indexes can be strictly complementary (an index-only "covering" plan needs both a filter index and included columns; neither alone gives the covering benefit) which can violate the diminishing-returns assumption locally. Update overhead also violates pure monotonicity — adding an index to SS can increase total workload cost once write costs dominate, so the objective used in practice is net benefit (read benefit minus write penalty), which is not monotone, and the theoretical guarantee applies only to the read-benefit component, not the full net objective. Practical advisors therefore treat the greedy output as a strong heuristic starting point, then apply local search (swap one index for another, check if net benefit improves) to patch violations.

Complexity of the algorithm itself. With nn queries, mm candidates, and greedy selecting up to k≤mk \le m indexes:

  • Each outer iteration scans all remaining candidates: O(m)O(m).
  • For each candidate, computing marginal gain costs O(n)O(n) optimizer calls (one per query), each call assumed O(P)O(P) for a cost estimate over plan space of typical size (treated as a constant per query in practice, though real optimizers are themselves exponential in join count without pruning).
  • Total: O(k⋅m⋅n)O(k \cdot m \cdot n) what-if cost evaluations.

With workload sizes in the hundreds and candidate sets pruned to tens or low hundreds, this is entirely tractable — the real engineering cost is minimizing redundant optimizer invocations via caching atomic-configuration costs, not the asymptotic complexity itself.

Space. Each what-if index requires only its catalog statistics (estimated cardinality, distinct values, clustering factor, tree height) to be stored, not the index itself — this is the entire point of what-if analysis, and it means the memory footprint of the search is O(m)O(m) small metadata records, not O(m)O(m) full index structures.

Worked example

Consider a simplified workload on a table `orders(order_id, customer_id, order_date, amount)` with 1,000,000 rows, and a storage budget B=3B = 3 units (each candidate index below costs exactly 1 unit to store, for simplicity).

Workload (query, weight = executions per hour):

QueryWeightDescription
Q1Q_1500`SELECT * FROM orders WHERE customer_id = ?`
Q2Q_2300`SELECT * FROM orders WHERE order_date BETWEEN ? AND ?`
Q3Q_350`UPDATE orders SET amount = ? WHERE order_id = ?`
Q4Q_4200`SELECT SUM(amount) FROM orders WHERE customer_id = ? AND order_date > ?`

Candidates (generated from predicate columns):

CandidateColumnsSize (units)
I1I_1`customer_id`1
I2I_2`order_date`1
I3I_3`(customer_id, order_date)` composite1

Baseline cost with no indexes (full table scans, in abstract page-I/O units): cost(Q1,∅)=10,000\text{cost}(Q_1, \emptyset) = 10{,}000, cost(Q2,∅)=10,000\text{cost}(Q_2, \emptyset) = 10{,}000, cost(Q3,∅)=10,000\text{cost}(Q_3, \emptyset) = 10{,}000 (an update still must scan to find `order_id` without a primary-key index — assume none exists), cost(Q4,∅)=10,000\text{cost}(Q_4, \emptyset) = 10{,}000. What-if evaluation gives per-query costs under each single candidate (illustrative estimator outputs, consistent with B+-tree lookup costs of O(log⁡FN)O(\log_F N) for a fan-out FF tree on 1M rows, roughly 20–50 I/Os per lookup, versus 10,000 for a scan):

∅\emptyset{I1}\{I_1\}{I2}\{I_2\}{I3}\{I_3\}
Q1Q_110,0003010,00030
Q2Q_210,00010,000200200
Q3Q_3 (update)10,00010,050 (extra maintenance)10,00010,050
Q4Q_410,0008,0006,000150

Note Q3Q_3's cost increases slightly under I1I_1 or I3I_3 because every update must also maintain the index — this is the update-overhead term baked directly into the what-if cost, exactly as the objective requires.

Step 1 — compute weighted net benefit for each candidate alone:

benefit(I1)=500(10000−30)+300(0)+50(10000−10050)+200(10000−8000)\text{benefit}(I_1) = 500(10000-30) + 300(0) + 50(10000-10050) + 200(10000-8000)
=500(9970)+0−50(50)+200(2000)=4,985,000−2,500+400,000=5,382,500= 500(9970) + 0 - 50(50) + 200(2000) = 4{,}985{,}000 - 2{,}500 + 400{,}000 = 5{,}382{,}500
benefit(I2)=500(0)+300(9800)+50(0)+200(4000)=2,940,000+800,000=3,740,000\text{benefit}(I_2) = 500(0) + 300(9800) + 50(0) + 200(4000) = 2{,}940{,}000 + 800{,}000 = 3{,}740{,}000
benefit(I3)=500(9970)+300(9800)+50(−50)+200(9850)\text{benefit}(I_3) = 500(9970) + 300(9800) + 50(-50) + 200(9850)
=4,985,000+2,940,000−2,500+1,970,000=9,892,500= 4{,}985{,}000 + 2{,}940{,}000 - 2{,}500 + 1{,}970{,}000 = 9{,}892{,}500

Step 2 — greedy ratio (size = 1 for all, so ratio = benefit): I3>I1>I2I_3 > I_1 > I_2. Greedy picks I3I_3 first, spending 1 of 3 units.

Step 3 — recompute marginal benefit of remaining candidates given S={I3}S = \{I_3\}. Since I3I_3 already answers Q1Q_1, Q2Q_2, and Q4Q_4 nearly optimally (30, 200, 150 respectively — a composite index on `(customer_id, order_date)` serves an equality-then-range predicate and, via prefix matching, a pure `customer_id` equality too), adding I1I_1 or I2I_2 now yields almost no marginal benefit for Q1,Q2,Q4Q_1, Q_2, Q_4 — this is exactly the diminishing-returns (submodularity) effect. The only remaining potential gain is nothing, since I3I_3's composite key already gives a leading-column match for I1I_1's workload (Q1Q_1) at the same cost as I1I_1 alone (30), and a full match for Q2Q_2-style range scans is not achievable via I3I_3 if Q2Q_2's predicate doesn't touch `customer_id` — but our what-if table already shows I3I_3 costs Q2Q_2 only 200, matching I2I_2 alone, because the composite index's leading column can still be range-scanned when queries provide both columns or, in this case, the optimizer chose to scan the `order_date` range within the composite tree. Marginal benefit of adding I1I_1 given S={I3}S=\{I_3\}: ≈0\approx 0 (no query improves further). Marginal benefit of adding I2I_2: ≈0\approx 0 similarly. Greedy terminates early, returning S={I3}S = \{I_3\}, having used only 1 of 3 budget units — because a single well-chosen composite index dominates two narrower ones for this workload. Total achieved cost:

500(30)+300(200)+50(10050)+200(150)=15,000+60,000+502,500+30,000=607,500500(30) + 300(200) + 50(10050) + 200(150) = 15{,}000 + 60{,}000 + 502{,}500 + 30{,}000 = 607{,}500

versus the no-index baseline of 500(10000)+300(10000)+50(10000)+200(10000)=10,500,000500(10000)+300(10000)+50(10000)+200(10000) = 10{,}500{,}000 — roughly a 17× workload cost reduction using a single index, illustrating why identifying the right composite candidate (not just single-column ones) is often more valuable than adding more indexes.

Common pitfalls & traps

PitfallWhy it happensConsequence
Indexing every predicate column independentlyNaive per-query analysis ignores composite indexes and cross-query synergyWastes storage budget on redundant single-column indexes when one composite index dominates
Ignoring update/maintenance costBenefit computed only from `SELECT` queries in the workloadSelected indexes look great for reads but tank throughput on the write-heavy production path
Treating benefit as strictly additive across indexesAssumes independence to simplify to knapsackMisses both synergy (composite index subsumes several) and negative interaction (redundant indexes competing for buffer pool space)
Using a stale or unrepresentative workload sampleWorkload captured once during a quiet periodIndexes tuned for yesterday's query mix regress when query patterns shift seasonally
Not accounting for existing indexes when computing marginal benefitEvaluating each candidate against an empty baseline instead of current catalog stateRecommends indexes that are already redundant with what's deployed, wasting the storage budget
Assuming the (1−1/e)(1-1/e) greedy bound applies to the full net-benefit objectiveBound proven only for monotone submodular functions; net benefit is not monotone once write cost dominatesGreedy can select an index that increases total workload cost, silently violating the theoretical guarantee

Pioneers & history

Origins · 1 minSurajit Chaudhuri and how this was discovered
SC
Surajit Chaudhuri
1962–present · Microsoft Research, database systems and query optimization

Surajit Chaudhuri co-led the development of the AutoAdmin project at Microsoft Research in the 1990s, which formalized automated physical database design — including index selection — as a search problem driven by what-if calls into a production query optimizer. This work directly shaped Microsoft SQL Server's Database Engine Tuning Advisor and influenced index-advisor tooling across the industry, extending decades of query-optimization research into automated system tuning.

History
1979
— System R's optimizer (Selinger et al.) establishes cost-based plan selection, the machinery index selection later reuses via what-if calls.
1988
— Finkelstein, Schkolnick, and Tiberio formalize automatic index selection as workload-driven optimization at IBM.
1997
— Chaudhuri and Narasayya publish the AutoAdmin what-if index analysis framework at Microsoft Research.
2000
— Microsoft SQL Server ships the Index Tuning Wizard, an early production index advisor built on these ideas.
2004
— IBM Db2 Design Advisor generalizes the approach to indexes, materialized views, and partitioning jointly.

Where this matters

Every major relational database ships an automated index-tuning tool built on the pipeline described here. Microsoft SQL Server's Database Engine Tuning Advisor captures a query trace, generates candidate indexes and indexed views, and uses the optimizer's own what-if mode to score them before recommending a configuration. IBM Db2's Design Advisor extends the same machinery to jointly select indexes, materialized query tables, and partitioning schemes. Oracle's SQL Tuning Advisor and Partition Advisor apply analogous what-if costing during automatic workload analysis. Cloud data warehouses such as Amazon Redshift and Google BigQuery apply related automated physical-design logic (sort keys, distribution keys, clustering) to workloads at petabyte scale, where the storage-budget tradeoff becomes a real dollar cost, not just disk capacity. Beyond relational systems, the same submodular-greedy machinery underlies materialized-view selection in OLAP cubes, cache-placement decisions in content delivery networks, and column-store projection design — anywhere a system must choose a bounded set of auxiliary structures to minimize workload cost. Understanding index selection also deepens intuition for Clustered vs Unclustered Indexes, since the choice of which column to cluster on interacts directly with which secondary indexes are worth building, and for Buffer Management, since indexes compete with data pages for the same finite buffer pool.

Ask the tutor: How would index selection change if two candidate indexes are strongly complementary — i.e., a query only speeds up when both are present together?

Why can't we just build an index on every column that appears in a WHERE clause?

Each index adds real overhead beyond disk space: every `INSERT`, `UPDATE`, and `DELETE` on the table must also update every index covering the modified columns, which multiplies write latency and can bottleneck throughput long before it improves read latency. Indexes also compete for buffer pool pages, so an excess of rarely-used indexes can evict frequently-used data pages, hurting overall performance. Finally, many single-column indexes become redundant once a well-chosen composite index exists, wasting storage budget without added benefit. Index selection exists precisely because "index everything" is a worse strategy than a carefully chosen minimal set — the goal is net benefit, not maximum read speed alone.

How does what-if analysis estimate a query's cost without actually building the index?

The database catalog stores statistics about tables — row counts, distinct value counts, histograms — that the optimizer's cost model consumes to estimate plan costs. What-if analysis injects hypothetical catalog entries for a candidate index (estimated tree height, leaf page count, clustering factor, computed analytically from existing table statistics) without writing any actual index pages to disk. The optimizer then runs its normal cost-based plan enumeration exactly as if the index existed, returning the plan and cost it would choose. Because building a real index is orders of magnitude more expensive than estimating its statistics, what-if analysis is what makes evaluating hundreds of candidates computationally feasible.

Is the greedy algorithm ever worse than a random or manual selection?

In the worst case, yes — the (1−1/e)(1-1/e) bound is a guarantee relative to the true optimum, not an absolute guarantee of good performance, and it applies strictly to the monotone read-benefit component, not the full net-benefit objective once update costs are included. When index interactions are strongly non-submodular (heavy complementarity or heavy redundancy), greedy's locally-optimal choices can lead it away from a better global configuration. In practice, however, greedy combined with a small amount of local search (swap-based refinement) performs very well empirically, and it remains vastly more scalable than exhaustive or dynamic-programming search over realistic candidate set sizes.

How is index selection different from just running EXPLAIN on every query?

`EXPLAIN` shows the cost and plan for a given set of existing indexes — it answers "how does this query run today." Index selection instead asks a hypothetical, workload-wide question: "what set of indexes, not yet built, would minimize total cost across all queries and updates, under a storage budget." It requires the optimizer's what-if capability to cost dozens or hundreds of hypothetical configurations, and a search procedure (greedy, knapsack DP, or local search) to navigate the combinatorial space of subsets — `EXPLAIN` alone provides no mechanism for either.

Why does storage budget matter if disk is cheap?

Storage budget in index selection is really a proxy for several costs beyond raw disk price: buffer pool contention (more indexes compete for the same cache), backup and replication time (larger databases take longer to back up and ship to replicas), and maintenance cost during bulk loads or schema migrations. Even when disk itself is inexpensive, an index that is rarely used still consumes buffer pool space whenever it is touched, potentially evicting hot data pages. Bounding total index size forces the selection algorithm to prioritize indexes with genuinely high impact rather than adding marginal ones "for free."

Do NoSQL and columnar systems face the same index selection problem?

Yes, in a generalized form. Columnar warehouses face an analogous problem choosing sort keys, distribution/partition keys, and projections — auxiliary physical structures that trade storage and write cost for query speedup, exactly mirroring the read/write/storage tradeoff of relational index selection. Document and key-value stores that support secondary indexes (e.g., composite or multi-key indexes) face the identical workload-driven tradeoff: each secondary index accelerates specific query shapes but adds write amplification on every document mutation. The underlying combinatorial structure — submodular benefit, knapsack-style storage constraint — recurs across nearly every system that offers optional auxiliary access paths.

What role does the query optimizer's own accuracy play in index selection quality?

A critical one: the entire selection pipeline is only as good as the cost estimates the optimizer produces during what-if analysis, since the search algorithm treats those estimates as ground truth. If cardinality estimation is systematically biased (a well-known weak point in query optimizers, especially under correlated predicates or skewed distributions), the advisor can confidently recommend indexes that don't actually help, or miss ones that would. This is why production advisors periodically re-evaluate recommendations against actual execution feedback, closing the loop between estimated and observed benefit rather than trusting the initial what-if numbers indefinitely.

Before this