{"post":{"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","content":"<span class=\"dropcap\">A</span>n ordinary index scan does two things: find matching entries in the index, then fetch each matching row from the table to get the columns you actually selected. That second step is a random read per row, and it is usually the expensive part.\n\nIf the index already contains every column the query needs, the second step is unnecessary. The database reads the index and returns results without touching the table.\n\n## What changed in 2026\n\n- **Include-column support spread.** More engines gained the ability to store extra columns in an index without making them part of the sort key.\n- **Covering index design became routine.** Adding selected columns to an index moved from an advanced technique to a standard tuning step.\n- **Visibility interaction got better documented.** The dependency on cleanup state became widely understood rather than a mysterious inconsistency.\n- **Write cost scrutiny increased.** Teams became more careful about index width as write volumes grew.\n\n## What makes an index covering\n\nThe index must contain every column the query references — in the filter, in the select list, in any sort, and in any join condition.\n\nFiltering on `status` and selecting `id, name` requires an index containing all three. An index on `status` alone can find the rows and must then fetch each one for `name`.\n\nTwo ways to include the extra columns:\n\n**As key columns**, part of the index's sort order. Makes them usable for filtering and sorting, and enlarges every level of the index structure.\n\n**As included columns**, stored only at the leaf level. Available for returning, not usable for filtering or ordering, and cheaper — the index's internal structure stays narrow.\n\nIncluded columns are the right choice for anything you only need to return. Key columns for anything you filter or sort on.\n\n| Query need | Where the column belongs |\n| --- | --- |\n| Filtered on | Key column |\n| Sorted by | Key column |\n| Joined on | Key column |\n| Only returned | Included column |\n| Not referenced | Not in the index |\n\n## The visibility catch\n\nUnder multi-version concurrency control, a row may exist in the index while being invisible to your transaction — deleted by a committed transaction, or created by one not yet committed. The index alone does not know.\n\nDatabases handle this with a structure tracking which pages contain only rows visible to everyone. If a page is marked all-visible, the index entry can be trusted without checking. If not, the table must be consulted after all, and the scan is no longer index-only.\n\nThe practical consequence: **whether an index-only scan actually happens depends on cleanup state**. A recently-updated table has many pages not marked all-visible, so the scan degrades toward a normal index scan.\n\nWhich means an index-only scan that worked in testing may not in production if the table is write-heavy and cleanup is behind. Rising update volume silently removes the optimisation — see [MVCC explained](/blog/mvcc-explained-2026) and [autovacuum tuning](/blog/autovacuum-tuning-2026).\n\n## The write cost\n\nEvery column in an index is maintained on every write touching that row. A wide covering index makes reads fast and writes slower, permanently.\n\nThat trade is worth it for a query running constantly against a table written to occasionally. It is not worth it for a report running weekly against a hot table.\n\nThe discipline: add columns to an index because a specific frequent query needs them, and verify from the plan that the index-only scan actually happens. An index widened speculatively is pure write cost.\n\n## Common mistakes\n\n- **Including only filtered columns.** Selected columns must be there too.\n- **Key columns where included would do.** Enlarges the index structure unnecessarily.\n- **Assuming it works without checking the plan.** Visibility state may prevent it.\n- **Widening indexes speculatively.** Write cost with no read benefit.\n- **`SELECT *` with a covering index.** Guarantees a table fetch.\n- **Ignoring cleanup on write-heavy tables.** Silently disables the optimisation.\n\n## FAQ\n\n**How do I know if I got one?**\nThe query plan names it explicitly. It will also report how many table fetches were still required — a non-zero count means visibility state is preventing a full index-only scan.\n\n**Does this work with partial indexes?**\nYes, and it composes well — a partial covering index over a small subset of rows is both small and fast. See [partial indexes](/blog/partial-indexes-explained-2026).\n\n**Why did it stop working?**\nAlmost always cleanup falling behind on a table that became write-heavy, leaving pages not marked all-visible.\n\n**Is there a limit on index width?**\nEngines have limits, and the practical constraint arrives first: a very wide index is large, slow to write, and consumes cache that other queries need.\n\n## Where to go next\n\nFor index fundamentals, read [database indexing explained](/blog/database-indexing-explained-2026). For the visibility mechanism, [MVCC explained](/blog/mvcc-explained-2026), and for keeping cleanup current, [autovacuum tuning](/blog/autovacuum-tuning-2026).","hue":303,"metaTitle":"Index-Only Scans: When the Table Is Never Touched","metaDescription":"How covering indexes let queries be answered from the index alone, why visibility information matters, and the write cost of wider indexes.","keywords":["index only scan","covering index","include columns index","visibility map","query performance tuning","random io reduction","index design","database performance 2026"],"tldr":["**Covering the query means skipping the table entirely** — no random row fetches at all.","**Include columns you select, not just ones you filter on** — that is what makes an index covering.","**Visibility information still has to come from somewhere** — which is why cleanup affects whether it works.","**Wider indexes cost writes and space** — do not add columns speculatively.","**Skip:** adding columns to an index for a query that runs rarely. The write cost applies constantly."],"readTime":"4 min read","wordCount":811},"related":[{"slug":"what-is-a-covering-index-2026","title":"What is a covering index? The 2026 guide to index-only scans","excerpt":"A covering index holds every column a query needs, so the database never has to fetch the underlying row. Here is how that shortcut actually works.","category":"code","date":"July 17, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":240},{"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","readTime":"5 min read","hue":310},{"slug":"clustered-indexes-explained-2026","title":"Clustered Indexes: When Physical Order Matters","excerpt":"Storing rows in index order makes range scans dramatically faster and makes the choice of key a decision you live with, because inserting in the wrong order fragments everything.","category":"code","date":"January 8, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":317}],"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":"best-ai-presentation-makers-2026","title":"Best AI presentation makers in 2026: Gamma vs Beautiful.ai vs Tome","excerpt":"AI presentation makers are now genuinely faster than PowerPoint. Here's the 2026 ranking by output quality, customization, and price.","category":"ai","date":"January 4, 2026","author":"ByteLedger Team","readTime":"2 min read","hue":195},"older":{"slug":"lock-timeouts-explained-2026","title":"Lock Timeouts: Failing Fast Instead of Queueing Forever","excerpt":"A migration waiting for a lock does not just wait. Everything arriving behind it queues too, and a brief schema change becomes a full outage.","category":"code","date":"January 4, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":300}}