Database Indexing
Optimize query performance with single-field, unique, compound, and lookup indexes
Database Indexing
Optimize query performance with indexes:
indexes: [
// Single field index
{ fields: ['name'], unique: false },
// Unique index — one holder per organization
{ fields: ['email'], unique: 'organization' },
// Unique index — one holder across the whole installation
{ fields: ['hostname'], unique: 'global' },
// Compound index
{ fields: ['type', 'is_active'], unique: false },
// Lookup field index
{ fields: ['owner'], unique: false },
]Two ways to say "unique" — one vocabulary for both
Uniqueness can be declared in two places: on a field, or as a declared index. Both use the same word to say which boundary the value must be unique within (ADR-0120):
| Scope | Meaning | Materializes as |
|---|---|---|
unique: 'organization' | One holder per organization | (COALESCE(organization_id, '__global__'), …fields) |
unique: 'global' | One holder across the whole installation | exactly the listed column(s) |
// Field level — the scope is the same word in both places
email: Field.email({ unique: 'organization' }), // one contact per org
hostname: Field.text({ unique: 'global' }), // one holder, platform-wide
indexes: [
// per organization — you do NOT list organization_id yourself
{ fields: ['department', 'code'], unique: 'organization' },
// platform-wide — exactly the columns you list
{ fields: ['source', 'dedup_key'], unique: 'global' },
]Two rules make the choice safe to write and safe to deploy:
- You state a business boundary, never a deployment shape. The same app package runs unmodified under every tenancy posture — no index shape reads the posture, so moving a deployment between postures has no schema consequence.
'organization'is NULL-safe. Rows with no organization (platform rows, and every row on a single-organization deployment) collapse into one platform bucket and are unique among themselves. Without this a plain(organization_id, …)composite enforces nothing on those rows, because SQLUNIQUEtreats everyNULLas distinct.
Bare unique: true. On a field it means 'organization' and stays
valid indefinitely — email: Field.email({ unique: true }) is correct, and the
explicit spelling is simply preferred in new code.
On a declared index it is the deprecated spelling of 'global': it
materializes over exactly the listed columns. Because that reads like the
field-level meaning but does the opposite, os lint / os build / os validate
report it as unique/unscoped-declared-index, and it is rejected outright at
protocol 18. State the scope.
Do not declare both on the same column. The installation-wide one wins physically, so the per-organization constraint can never be reached — one of the two intents you wrote is silently discarded:
// ⚠️ contradictory — the global index wins, the per-organization scope is dead
email: Field.email({ unique: 'organization' }),
indexes: [{ fields: ['email'], unique: 'global' }],os lint / os build report this as unique/double-declaration. Pick one:
keep unique: 'global' on the field and drop the index for platform-wide
uniqueness, or drop the index and keep unique: 'organization' on the field.
Never put a 'global' unique index on an autonumber field. The autonumber
sequence is per organization — every organization counts from 1 — so an
installation-wide unique index rejects the second organization's CASE-00001
on insert. Use unique: 'organization' so the constraint matches the sequence
that feeds it.
Already have a hand-written organization composite?
Metadata written before the vocabulary existed spells the per-organization constraint by listing the column itself:
// the legacy spelling — still valid, still materialized exactly as written
indexes: [{ fields: ['name', 'organization_id'], unique: true }]This keeps working forever and forces no migration, so os lint only suggests
the change (unique/legacy-organization-composite). Opting in is worth it
because it closes the NULL hole above — keep fields exactly as they are and
change the scope word:
indexes: [{ fields: ['name', 'organization_id'], unique: 'organization' }]The listed organization column becomes NULL-safe in place; no second
organization key part is added. Because this genuinely tightens the constraint,
it surfaces as a recreate_index migration guarded by a duplicate pre-flight
probe — if rows the old index wrongly admitted are still there, os migrate plan
reports them instead of failing a boot. See
CLI · migrations.
Installing an app into an isolated deployment
Under the isolated posture, organizations are separate customers. A
'global' unique on an app's own object therefore constrains across
customers — right for a DNS hostname or an external provider id, almost never
right for a business rule. Installing such an app stops and lists each index so
the installer can confirm it or rewrite it to 'organization'; the answer is
recorded in the install manifest and never asked again. os doctor and
os migrate plan report the same finding for apps installed before that check
existed, or for environments whose posture changed afterwards.
When to Add Indexes
✅ Add indexes for:
- Lookup/reference fields
- Fields used in filters
- Fields used for sorting
- Fields in WHERE clauses
- High-cardinality fields
❌ Avoid indexes for:
- Low-cardinality fields (e.g., boolean)
- Fields rarely queried
- Frequently updated fields