Most of the time, you don’t need to think about which index SQLite uses to run your query — the query planner is genuinely good at figuring that out on its own. But every so often, you’ll hit a situation where SQLite picks an index you weren’t expecting, or you’re debugging a performance problem and need to test how a query behaves with a specific index forced into use. That’s exactly what the INDEXED BY clause is for. It’s a niche feature, but when you need it, it’s invaluable.
In this article, I’ll explain what INDEXED BY does, how to use it correctly, when it’s actually useful, and — just as important — when you should stay away from it.
What Is the INDEXED BY Clause?
INDEXED BY is a SQLite-specific clause that lets you explicitly tell the query planner which index to use when accessing a particular table in a query, overriding SQLite’s own automatic index selection.
SELECT * FROM employees INDEXED BY idx_department
WHERE department = 'Engineering';
Here, instead of letting SQLite decide on its own whether to use idx_department, a different index, or a full table scan, you’re explicitly forcing it to use idx_department.
There’s also a companion clause, NOT INDEXED, which does the opposite — it forces SQLite to avoid using any index at all for that table and fall back to a full table scan.
SELECT * FROM employees NOT INDEXED
WHERE department = 'Engineering';
Why Does SQLite Need This?
SQLite’s query planner uses a cost-based optimizer that looks at the structure of your query, the indexes available, and (if you’ve run ANALYZE) statistics about your data distribution, to decide the most efficient way to execute a query. Most of the time, this works well. But the query planner isn’t perfect, and there are situations — often involving complex queries, unusual data distributions, or missing statistics — where it might choose a less efficient index, or skip using an index altogether when one would actually help.
INDEXED BY exists as an escape hatch for exactly those situations: a way to say “I know better than the planner here, use this specific index.”
Basic Syntax
The general syntax pattern is:
SELECT columns
FROM table_name INDEXED BY index_name
WHERE condition;
It can also be used in UPDATE and DELETE statements, since those also need to identify rows to act on:
UPDATE employees INDEXED BY idx_department
SET salary = salary * 1.05
WHERE department = 'Engineering';
DELETE FROM employees INDEXED BY idx_department
WHERE department = 'Engineering';
A Full Working Example
Let’s set up a table and a couple of indexes to see INDEXED BY in action.
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT,
department TEXT,
salary REAL,
hire_date TEXT
);
CREATE INDEX idx_department ON employees(department);
CREATE INDEX idx_salary ON employees(salary);
Now, a query that filters on both columns could theoretically use either index:
SELECT * FROM employees
WHERE department = 'Engineering' AND salary > 80000;
By default, SQLite’s query planner picks whichever index it estimates will be more selective — likely idx_department if there are relatively few engineering employees, or idx_salary if high earners are rare. If you wanted to force it to specifically use the department index, regardless of what the planner would otherwise choose:
SELECT * FROM employees INDEXED BY idx_department
WHERE department = 'Engineering' AND salary > 80000;
Important Behavior: INDEXED BY Can Cause Errors
Here’s a detail that catches people off guard and is worth understanding clearly: if you specify an index with INDEXED BY that SQLite determines cannot actually be used to satisfy the query — for instance, because the WHERE clause doesn’t reference the indexed column in a way that the index could help with — SQLite will throw an error rather than silently falling back to a table scan.
-- If idx_salary can't help answer this WHERE clause, this raises an error
SELECT * FROM employees INDEXED BY idx_salary
WHERE department = 'Engineering';
This is actually a deliberate design decision. Unlike a query hint in some other databases (which might be silently ignored if inapplicable), SQLite’s INDEXED BY is a hard constraint: either the specified index can be used, or the query fails outright. This makes INDEXED BY somewhat risky to leave in production code, since a schema change that drops or renames the index will cause every query using it to start failing immediately, rather than degrading gracefully to a slower query.
Using NOT INDEXED
Sometimes you want the opposite: to confirm what a query looks like or performs like without any index assistance, forcing a full table scan.
SELECT * FROM employees NOT INDEXED
WHERE department = 'Engineering';
This is particularly useful for benchmarking — comparing the performance of an indexed lookup against a full scan lets you actually measure how much benefit an index provides, rather than just assuming.
Using EXPLAIN QUERY PLAN to See What SQLite Would Choose
Before reaching for INDEXED BY, it’s worth first understanding what SQLite would do on its own. The EXPLAIN QUERY PLAN statement shows you exactly which index (if any) SQLite intends to use for a given query.
EXPLAIN QUERY PLAN
SELECT * FROM employees WHERE department = 'Engineering' AND salary > 80000;
This returns a human-readable description of the plan, something like showing whether it’s doing a SEARCH using a particular index, or a SCAN of the whole table. Running this before and after adding an INDEXED BY clause lets you directly compare what changes.
When INDEXED BY Is Actually Useful
To be candid: for the vast majority of applications, you will never need INDEXED BY. It’s a specialized tool, and SQLite’s own documentation is fairly explicit that it should be used sparingly. That said, here are legitimate scenarios where it earns its place:
- Debugging and diagnosing query planner decisions. If you suspect the planner is making a poor choice, INDEXED BY lets you force an alternative and directly compare performance using
EXPLAIN QUERY PLANor timing measurements. - Testing index effectiveness during development. Before deciding whether an index is worth keeping, you can force its use and measure the actual performance difference versus NOT INDEXED.
- Working around a query planner limitation in a specific SQLite version. Occasionally, particularly complex queries expose edge cases where the planner’s heuristics genuinely pick a suboptimal plan, and a hint is the pragmatic fix while waiting on a fix or restructuring the query.
- Writing deterministic test suites. In some testing scenarios, you might want to guarantee a query always executes via the same access path, regardless of how table statistics might shift, to keep test behavior consistent.
When to Avoid INDEXED BY
- In general production application code, as a default habit. SQLite’s planner is usually better informed than a hard-coded assumption, especially as data grows and changes shape over time.
- When you haven’t first tried running
ANALYZE. A lot of “the planner picked the wrong index” problems are actually solved by runningANALYZE, which gives the planner better statistics about your data distribution, rather than by forcing a specific index.
ANALYZE;
- When the schema might change. Since INDEXED BY causes a hard failure if the named index doesn’t exist or can’t be used, it introduces a brittle dependency between your query text and your exact index names. Renaming or restructuring an index elsewhere in your codebase can silently break queries that reference it via INDEXED BY.
- As a substitute for proper indexing strategy. If a query is slow, the right first step is almost always to look at whether you have the right indexes defined at all, not to force the use of an existing, possibly suboptimal one.
INDEXED BY vs. Just Adding a Better Index
It’s worth emphasizing this distinction clearly: INDEXED BY doesn’t create anything or improve anything on its own — it only controls which existing index SQLite uses. If your real problem is “there’s no good index for this query,” the fix is to create one, not to force the use of a mediocre existing index.
-- The actual fix, if no good index exists yet
CREATE INDEX idx_department_salary ON employees(department, salary);
A well-designed composite index like this one often eliminates the need for INDEXED BY entirely, because the planner will naturally choose it once it’s the clearly superior option.
Common Use Cases
- Query plan debugging sessions where you’re actively comparing different access paths for a slow query.
- Performance regression testing, ensuring a specific index is genuinely being used as part of an automated test.
- Working through a rare planner edge case in a specific SQLite version where the automatic choice is demonstrably worse than a manual one, confirmed via benchmarking.
- Educational and exploratory contexts, like this very kind of deep-dive analysis, where understanding exactly how indexes affect query execution matters more than production robustness.
Best Practices
- Reach for
EXPLAIN QUERY PLANbefore INDEXED BY. Understand what SQLite is already doing before trying to override it. - Run
ANALYZEbefore assuming the planner is wrong. Often the planner just needs better statistics, not a manual override. - Use INDEXED BY primarily for diagnostics, not as permanent production logic, given the risk of hard failures if the index changes or disappears.
- If you must use INDEXED BY in production, document why. A future maintainer (including future you) needs to understand this isn’t an accidental leftover from debugging.
- Prefer fixing the actual indexing strategy — adding, removing, or restructuring indexes — over forcing SQLite’s hand with a hint.
- Test both indexed and NOT INDEXED versions of a query when you’re genuinely trying to measure an index’s real-world benefit.
- Re-verify INDEXED BY queries after schema migrations, since renamed or dropped indexes will cause immediate query failures rather than a graceful fallback.
Wrapping Up
The INDEXED BY clause is one of those SQLite features that most developers will go their entire career without needing, and that’s by design — SQLite’s query planner is generally trustworthy, and manually overriding it should be the exception, not the rule. But when you’re debugging a genuinely puzzling performance issue, or you need deterministic, testable control over exactly how a query executes, INDEXED BY (and its counterpart, NOT INDEXED) gives you that precision. Just go in with your eyes open about the tradeoff: what you gain in explicit control, you give up in graceful degradation, since an invalid or missing index reference will cause the query to fail outright rather than quietly falling back to a scan.
