SQLite Indexes – CREATE INDEX, EXPLAIN QUERY PLAN, and Tuning Tips | DbSchema

Table of Contents
- What a SQLite index does
- Sample table and query patterns
- SQLite index types
- Create and inspect indexes in sqlite3
- Use EXPLAIN QUERY PLAN
- Index design and performance tips
- Create and manage indexes in DbSchema
- FAQ
- Conclusion
- References
SQLite indexes improve lookup speed by giving the query planner a faster path to matching rows. They are especially helpful for WHERE, JOIN, and ORDER BY queries, but they also add write overhead because SQLite must keep the index structure updated on every relevant insert, update, and delete.
This guide covers practical SQLite indexing: when to add an index, which kind to use, how to read EXPLAIN QUERY PLAN, and how DbSchema helps you manage indexes visually.
What a SQLite index does
Think of an index like the index in a book: instead of reading every page to find a topic, you jump directly to the relevant pages.
In SQLite, an index can help:
- equality filters such as
WHERE email = ? - range searches such as
WHERE created_at >= ? - join lookups such as
orders.customer_id = customers.customer_id - sorted reads such as
ORDER BY created_at DESC
An index does not help every query. If a table is tiny, or if the query reads most rows anyway, a full scan may still be the best plan.
Sample table and query patterns
Assume this table:
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
status TEXT NOT NULL,
created_at TEXT NOT NULL,
total_cents INTEGER NOT NULL
);
Common queries might be:
SELECT * FROM orders WHERE customer_id = 42;
SELECT * FROM orders
WHERE status = 'open'
ORDER BY created_at DESC;
SELECT * FROM orders
WHERE customer_id = 42
AND status = 'open';
Those patterns tell you far more about indexing needs than a generic rule like "index every foreign key and every text column."
SQLite index types
| Index type | Best use |
|---|---|
| Single-column index | one frequent filter column |
| Composite index | multi-column filter or filter + sort pattern |
| Unique index | enforce uniqueness and speed lookups |
| Partial index | index only rows that match a condition |
| Implicit index | created automatically for some PRIMARY KEY and UNIQUE constraints |
Single-column index
CREATE INDEX idx_orders_customer_id
ON orders(customer_id);
Composite index
CREATE INDEX idx_orders_customer_status
ON orders(customer_id, status);
Column order matters. The index above can help:
WHERE customer_id = ?WHERE customer_id = ? AND status = ?
but it is less useful for queries filtering only on status.
Unique index
CREATE UNIQUE INDEX idx_customers_email
ON customers(email);
Partial index
CREATE INDEX idx_orders_open_created_at
ON orders(created_at)
WHERE status = 'open';
Partial indexes are a strong SQLite feature when you repeatedly query a focused subset of rows.
Create and inspect indexes in sqlite3
Open the database:
sqlite3 shop.db
Create indexes:
CREATE INDEX idx_orders_customer_id
ON orders(customer_id);
CREATE INDEX idx_orders_status_created_at
ON orders(status, created_at);
List indexes:
.indices orders
Inspect them with pragmas:
PRAGMA index_list('orders');
PRAGMA index_info('idx_orders_status_created_at');
When deciding what to index, also review the table definition and constraints:
Apply indexes from Python
If your schema is managed from application code or migrations, you can create indexes programmatically:
import sqlite3
with sqlite3.connect("shop.db") as connection:
connection.execute("""
CREATE INDEX IF NOT EXISTS idx_orders_customer_id
ON orders(customer_id)
""")
This is useful when index creation needs to stay in versioned setup scripts instead of being run manually.
Use EXPLAIN QUERY PLAN
Many articles mention indexes without showing whether SQLite will actually use them. EXPLAIN QUERY PLAN is the missing piece.
Before adding an index
EXPLAIN QUERY PLAN
SELECT order_id, created_at
FROM orders
WHERE customer_id = 42;
Typical output will indicate a SCAN orders, which means SQLite is reading the table broadly.
After adding the index
CREATE INDEX idx_orders_customer_id
ON orders(customer_id);
EXPLAIN QUERY PLAN
SELECT order_id, created_at
FROM orders
WHERE customer_id = 42;
Now the output often shows SEARCH orders USING INDEX idx_orders_customer_id, which is what you want to see.
Filter plus sort example
EXPLAIN QUERY PLAN
SELECT order_id, created_at
FROM orders
WHERE status = 'open'
ORDER BY created_at DESC;
If this query is common, an index like the following may help:
CREATE INDEX idx_orders_status_created_at
ON orders(status, created_at DESC);
For more plan-reading examples, see SQLite EXPLAIN PLAN.
Index design and performance tips
Index the workload, not the schema diagram
Only add indexes for queries you actually run often.
Put the most useful leading columns first
Composite indexes are ordered structures. The leftmost columns influence which queries can use the index efficiently.
Avoid redundant indexes
If you already have:
CREATE INDEX idx_orders_customer_status
ON orders(customer_id, status);
then a second index on only (customer_id) may be redundant, depending on your workload.
Expect write overhead
Every extra index can slow down:
INSERTUPDATEDELETE
This matters most on write-heavy tables.
Index foreign key columns used in joins
SQLite does not automatically index every foreign key column. If you join or cascade through that column often, add the index explicitly.
Create and manage indexes in DbSchema
DbSchema is useful when you want to see indexes as part of the whole schema rather than as isolated SQL statements.
Typical DbSchema index workflow
- Open the SQLite database or model in DbSchema.
- Double-click the table or open it in the diagram.
- Go to the Indexes / Foreign Keys area.
- Add a normal, unique, or multi-column index.
- Generate the SQL and synchronize the model with the database.
Why DbSchema helps:
- you can compare indexes with table constraints in one place
- you can review relationships before indexing join paths
- you can keep the design documented for the next developer
Useful docs:
FAQ
When should I create a SQLite index?
When a query on a non-trivial table is filtered, joined, or ordered often enough that repeated scans are expensive.
Does a PRIMARY KEY create an index automatically?
Often yes, either explicitly or implicitly depending on the table design. But not every useful lookup path is covered by the primary key.
How do I know whether SQLite is using my index?
Run EXPLAIN QUERY PLAN and look for SEARCH ... USING INDEX ... instead of a broad table scan.
Can too many indexes hurt performance?
Yes. Too many indexes increase disk usage and make inserts, updates, and deletes slower.
Conclusion
SQLite indexes are powerful because small, well-chosen indexes can dramatically improve common reads. The trick is to add them based on real query patterns, validate them with EXPLAIN QUERY PLAN, and avoid indexing every column by default.
Use the sqlite3 shell when you need quick control, and use DbSchema when you want visual schema review, easier index management, and better documentation around how the database actually performs.