{"post":{"slug":"query-planner-explained-2026","title":"Query Planners: Why Your Index Is Being Ignored","excerpt":"You added the index and the query got no faster. The planner looked at it, estimated it would be slower, and chose a sequential scan. It is often right.","category":"code","date":"August 17, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">Y</span>ou profiled the slow endpoint, found the query, added exactly the index it should need, and deployed. Nothing changed. You check, and the database is still doing a sequential scan across four million rows while your beautiful index sits unused.\n\nThe planner is not broken and it is not ignoring you. It considered your index, estimated what using it would cost, estimated what scanning would cost, and picked the cheaper one. Whether it picked correctly depends almost entirely on whether its estimates were any good.\n\n## What changed in 2026\n\n- **Managed databases hid the tuning knobs.** Fewer teams adjust planner settings directly, which makes understanding the estimates more important, not less.\n- **Statistics collection got more automatic.** Better defaults reduced the frequency of stale-statistics disasters without eliminating them, particularly after bulk loads.\n- **Plan regression monitoring spread.** Tracking when a query's plan changes — rather than only when it gets slow — became a standard part of database observability.\n- **Query plan visualisation tools improved.** Reading nested plan output stopped requiring quite so much practice.\n\n## How the decision is made\n\nThe planner enumerates ways to execute your query — which index to use or none, what order to join tables, which join algorithm — and assigns each a cost. Cost is an abstract number combining estimated disk pages read and CPU work. Lowest cost wins.\n\nEvery one of those costs depends on an estimate of how many rows each step will produce, and those estimates come from statistics: sampled summaries of your data's distribution, collected periodically.\n\nThe chain is short and brittle. Statistics feed row estimates, row estimates feed costs, costs pick the plan. Bad statistics produce bad estimates produce a bad plan — even though the planner reasoned perfectly from what it was told.\n\n## Why a sequential scan is often right\n\nThe instinct that an index is always faster is wrong, and understanding why explains most \"ignored index\" cases.\n\nAn index scan finds matching entries, then fetches each row from the table — a separate, effectively random read per row. A sequential scan reads the table straight through in large sequential chunks, which storage handles far better.\n\nSo when a query matches a large fraction of the table, the index scan does nearly as many row fetches as a sequential scan does, but randomly, plus the index traversal on top. The crossover is lower than people expect — often somewhere in the single-digit percentages of the table.\n\n| Situation | Likely plan | Why |\n| --- | --- | --- |\n| Matches 0.1% of rows | Index scan | Few random fetches |\n| Matches 30% of rows | Sequential scan | Random fetches exceed a straight read |\n| Table is small | Sequential scan | Whole table fits in a few pages |\n| Index covers all needed columns | Index-only scan | No table fetch at all |\n| Statistics are stale | Anyone's guess | Estimates are wrong |\n\nThe index-only scan row is worth noting. If every column the query needs is in the index, the table is never touched and the random-fetch cost disappears. This is why adding a column to an index sometimes produces a dramatic speedup that adding a whole new index does not — see [database indexing explained](/blog/database-indexing-explained-2026).\n\n## Reading EXPLAIN ANALYZE\n\n`EXPLAIN` shows the plan and the estimates. `EXPLAIN ANALYZE` actually runs the query and shows estimates *and* actual numbers. The second is what you want, because the single most valuable diagnostic is the gap between them.\n\nLook for a node where estimated rows and actual rows differ by a large factor. That is where the planner was misled, and every decision above that node was made on a wrong assumption. A step estimating 10 rows and returning 400,000 explains a bad plan completely — the planner chose a strategy appropriate for 10 rows.\n\nCommon causes of that gap:\n\n**Stale statistics.** The most frequent, and the first thing to check. After a bulk load, a large delete, or any big data shift, statistics describe a table that no longer exists. Refresh them and re-check before doing anything else.\n\n**Correlated columns.** The planner generally assumes conditions are independent. If `city` and `postcode` are filtered together, it multiplies their selectivities and estimates far fewer rows than reality, because those columns are not independent at all. Extended statistics on the column group fix this where supported.\n\n**Expressions the planner cannot see through.** A condition wrapping a column in a function is opaque to column statistics, so the planner falls back to a generic guess.\n\n**Skewed distributions.** A column where one value covers most rows breaks average-based estimates unless the sample captured it.\n\n## Common mistakes\n\n- **Adding an index without checking whether it gets used.** Unused indexes cost write performance and space for nothing.\n- **Using EXPLAIN without ANALYZE.** You see estimates and no way to know they were wrong.\n- **Forcing plans with hints before checking statistics.** Freezes a plan that may be wrong later.\n- **Not refreshing statistics after a bulk load.** The single most common cause of a sudden bad plan.\n- **Wrapping indexed columns in functions.** Prevents index use unless you built a matching expression index.\n- **Reading only the top of the plan.** The problem is usually at a leaf node where estimates diverged.\n- **Optimising against an empty test database.** Plans on a thousand rows tell you nothing about four million.\n\n## FAQ\n\n**How do I make it use my index?**\nFirst find out why it is not. Run `EXPLAIN ANALYZE`, find the estimate-versus-actual gap, and refresh statistics. If estimates are accurate and it still declines the index, the planner is probably right and your index does not suit that query — often because it matches too many rows or lacks a needed column.\n\n**Why did a query get slow with no code change?**\nData changed and the plan flipped. A query fast at 10,000 rows can pick a different plan at 10 million, and the tipping point arrives without warning. Plan-change monitoring catches this before users do.\n\n**Are query hints a bad idea?**\nA reasonable last resort, and a bad first one. They pin a decision that was correct for today's data shape. Where the planner is persistently wrong for structural reasons and you have exhausted statistics fixes, they earn their place — with a comment explaining why.\n\n**Does this apply to NoSQL databases?**\nThose with query planners, yes, in the same shape. Simpler key-value stores have no planner because there is nothing to choose between, which trades flexibility for predictability.\n\n**Does connection pooling affect plans?**\nIndirectly. Prepared statements reused across a pool can retain a plan chosen for different parameters — the generic-plan problem. See [connection pooling explained](/blog/connection-pooling-explained-2026).\n\n## Where to go next\n\nFor the indexes the planner chooses between, read [database indexing explained](/blog/database-indexing-explained-2026). For why those indexes degrade over time, [index bloat](/blog/index-bloat-explained-2026), and for the version churn underneath your row counts, [MVCC explained](/blog/mvcc-explained-2026).","hue":310,"metaTitle":"Query Planners: Why Your Index Is Being Ignored","metaDescription":"How database query planners choose execution plans from cost estimates, why stale statistics cause bad plans, and how to read EXPLAIN ANALYZE output.","keywords":["query planner","explain analyze","index not used","query optimizer","database statistics","sequential scan vs index scan","slow query debugging","sql performance tuning 2026"],"tldr":["**The planner picks a plan from cost estimates, not rules** — it is not ignoring your index, it estimated using it would cost more.","**Bad estimates are the usual root cause** — stale statistics make the planner confidently wrong about how many rows it will see.","**A sequential scan is often correct** — reading a whole small table beats random index lookups plus fetching most of the rows anyway.","**EXPLAIN ANALYZE beats EXPLAIN** — you need estimated versus actual row counts, and only the analyze form gives you both.","**Skip:** forcing the planner's hand with hints before checking statistics. You are usually fixing a symptom of stale data."],"readTime":"5 min read","wordCount":1166},"related":[{"slug":"how-to-optimize-sql-queries-2026","title":"How to optimize SQL queries in 2026: indexes, plans, and real fixes","excerpt":"Slow SQL is rarely a mystery — the query plan tells you exactly what is wrong. This 2026 guide covers the tools and techniques to find and fix slow queries fast.","category":"code","date":"April 1, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":309},{"slug":"how-to-optimize-database-queries-2026","title":"How to optimize database queries in 2026: a measured approach","excerpt":"Most slow queries come from missing indexes and bad query shapes, not the database engine. Here is how to measure, fix, and verify query performance.","category":"code","date":"May 22, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":222},{"slug":"index-only-scans-explained-2026","title":"Index-Only Scans: When the Table Is Never Touched","excerpt":"If every column a query needs is in the index, the database can answer without reading the table at all. That is frequently the difference between a slow query and an instant one.","category":"code","date":"January 4, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":303}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"planner-executor-agents-2026","title":"Planner-Executor Agents: Separating Thinking From Doing","excerpt":"Splitting an agent into a planner and an executor makes long tasks tractable and adds failure modes. Here is when the separation helps and how to handle replanning.","category":"ai","date":"August 17, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":210},"older":{"slug":"remote-work-state-taxes-2026","title":"Remote Work State Taxes: Where You Actually Owe","excerpt":"Working from a different state than your employer creates filing obligations most people discover late. Here is how residency, sourcing, and credits interact.","category":"finance","date":"August 17, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":52}}