Indexes
Use CREATE INDEX to speed up equality lookups and supported numeric range queries. Vector search uses cosine indexes.
KalamDB has two different index kinds. They look similar in SQL, but they answer different questions.
| Kind | SQL shape | What it speeds up |
|---|---|---|
| Scalar (this page) | CREATE INDEX name ON table (columns) | Equality lookups and prefix matches on USER and SHARED tables |
| Vector | ALTER TABLE … CREATE INDEX col USING COSINE | Nearest-neighbor search on EMBEDDING(n) columns |
Scalar indexes support equality prefix scans and numeric range seeks. They do not replace the primary key, they do not apply to STREAM tables, and they do not answer ORDER BY COSINE_DISTANCE(...).
For embeddings, see Vector Search.
When to create one
Create a scalar index when a query, policy, or live subscription repeatedly filters on the same column (or a stable leading pair of columns):
- Chat:
WHERE conversation_id = $1(and oftenORDER BY created_at) - Membership:
WHERE user_id = CURRENT_USERinsideCREATE POLICY - Lookups:
WHERE email = $1,WHERE status = 'open'
The primary key already gives you O(1) identity lookups. Index everything else you filter on in a hot path.
CREATE INDEX
The parentheses are required. That is how KalamDB distinguishes a scalar index from a vector index.
The same statement works as ALTER TABLE:
CREATE INDEX after rows already exist is valid. Hot rows are indexed immediately. Flushed (cold) rows stay queryable; equality still returns the right count even if some matches come from a cold scan.
What the planner uses
A scalar index is a prefix index:
- Leading equality predicates (
col = value) narrow an index prefix. Ordered integer keys also support numeric range predicates. - Extra predicates after that prefix can still run on the matching rows.
EXPLAIN ANALYZEshows a smallhot_rows_scannedwhen the seek works. Without the index, the same query scans the whole hot set.
Confirm it with:
After flush, cold Parquet files are pruned with bloom / min-max on indexed columns instead of opening every segment.
UNIQUE indexes
CREATE UNIQUE INDEX is accepted and stored on the table catalog (indexes[].unique). The primary key is still the enforced unique live-row identity. Treat a secondary UNIQUE index as a documented uniqueness contract among live MVCC winners (for example a login email), not as a second primary key. Duplicate primary keys still fail independently of secondary indexes.
DROP INDEX
Queries keep working after a drop; they just scan more. In schema.sql, do not write DROP INDEX. Remove the CREATE INDEX instead and let the migration diff emit ALTER TABLE … DROP INDEX.
Rules
- Tables:
USERandSHAREDonly.STREAMtables reject scalarCREATE INDEX. - Columns: at most four real table columns, no duplicates in the list, no expressions, no
INCLUDE, noWHERE(partial) clauses. - Reserved columns: you cannot index
_seq,_commit_seq, or_deleted. - Names:
IF NOT EXISTSis a no-op when the index already exists; without it, a duplicate name errors. - Vector mix-up:
CREATE INDEX idx (embedding) USING COSINEis rejected. Vector indexes use the no-parenthesesUSING COSINEform. - Not supported:
CREATE INDEX CONCURRENTLY, expression indexes, GiST/GIN, or using a scalar index as a vector search path.
Inspecting indexes
Latest catalog JSON lives on system.schemas:
indexes is a JSON array of { "name", "columns", "unique" }.
Chat membership pattern
Index both sides of a membership policy so reads do not scan every member row:
idx_conversation_members_user covers WHERE user_id = CURRENT_USER. idx_messages_conversation covers WHERE conversation_id = … after the policy has chosen rooms.
Related
Numeric ranges and replica backfill
Integer keys sort numerically. Use EXPLAIN ANALYZE SELECT * FROM app.products WHERE id >= 100 AND id < 200 to inspect a numeric range plan. Creating or changing a scalar index backfills each replica from its own hot rows. Repeated indexed equality updates reuse a plan; staged transaction updates remain point lookups. Performance still depends on selectivity, hot/cold data, and the measured workload.