Indexes
The primary index gives you constant-time access to any record by key. It is always present, always in memory, and costs 64 bytes per record. Every other index type in Aerospike is optional, carries additional memory cost, and serves a specific query pattern. Use indexes deliberately — not as defaults.
Primary index recap
The primary index (PI) maps every record’s 20-byte digest to a storage location. It supports one access pattern: look up a record by its full key. If you can derive the key from the data you already have, the PI gives you the fastest path to any record in the cluster.
Most Aerospike data models are designed so that the primary access path is a key lookup or a bounded batch read of known keys. Indexes beyond the PI are for the access patterns where the key is not known in advance.
Secondary indexes
A secondary index (SI) lets you query records by bin value across the entire namespace or a specific set. The server maintains a B-tree index on the indexed bin values and evaluates queries by scattering the request to all nodes — each node searches its local partition of the index.
When to use a secondary index
- You need to find records by a bin value that you cannot derive from the record key.
- The query pattern is frequent enough to justify the memory cost.
- The result set is a bounded subset of the namespace, not a full traversal.
When not to use a secondary index
- The key is derivable. If you can construct the record key from
the query parameters, a direct
getor batch read is faster and cheaper. - Unique ID resolution. If the query returns exactly one record (for example, “find the user with email X”), a lookup table is more efficient than an SI (see Lookup tables for unique ID resolution).
- Every query pattern. Do not create an SI for every bin. Many access patterns are better served by key design and batch reads. Each SI adds memory cost per indexed entry.
Memory cost
Each SI entry costs approximately 14 bytes (8 bytes for the value or value hash, plus 6 bytes for the PI reference). The B-tree structure uses 4 KiB nodes with an initial pre-allocation of 16 MiB per node.
For large indexes, the server pre-allocates memory in stages controlled
by the sindex-stage-size
configuration parameter. The default is 1 GiB per stage, with a minimum
of 128 MiB. Indexes exceeding 2 TiB require larger stage sizes.
List and Map indexes
When you create an SI on a bin that contains a List or Map, the index creates one entry per element in the collection, not one entry per record. A Map bin with 100 entries in each of 1 million records produces 100 million SI entries. Factor this into memory planning.
List indexing and Map indexing (on keys or values) are supported at any nesting depth starting with Aerospike Database 6.1. Earlier versions support top-level indexing only. A known limitation: range queries on collection-indexed bins can return duplicate records when a single collection contains multiple values that fall within the query range. Your application must handle deduplication of query results when using range queries on List or Map indexes.
Set indexes
A set index lets you query “all records in set S” without scanning the entire namespace. It costs approximately 16 bytes per record in the set, multiplied by the replication factor.
Set indexes are useful when the set is small relative to the namespace. If a namespace has 100 million records and a set has 50,000, a set index avoids scanning 99.95% of the namespace. If the set contains a large fraction of the namespace, the set index adds memory cost without meaningful performance gain.
You can enable and disable set indexes dynamically without restarting the server.
Expression indexes
Starting with Aerospike Database 8.1, expression indexes let you index a computed value derived from an expression rather than a raw bin value. This gives you two capabilities that standard SIs do not:
Sparse indexing
Use cond(..., unknown()) in the index expression to exclude records
that do not match a condition. Only the records where the expression
evaluates to a definite value are indexed; the rest are skipped.
For example, to index only records where status == "active":
cond( eq(bin("status"), "active"), bin("region"), unknown())This creates an index on the region bin, but only for active records.
Inactive records produce unknown(), which excludes them from the index.
The result is a smaller, more memory-efficient index that covers only the
records your queries target.
Computed value indexing
Index a value that does not exist in any single bin — for example, a hash of two bins, a derived category, or a transformed date component. The server evaluates the expression at write time and indexes the result.
Indexing nested CDT fields with path expressions
Starting with Aerospike Database 8.1.2, you can combine expression
indexes with path expressions to index
values extracted from nested collection data type structures. Define a
selectByPath expression that extracts the target values into a list,
then create an SI with collection type LIST so each extracted value
becomes a separate index entry.
For example, if each record has a vehicles bin holding a List of Maps
(each with make, color, and license fields), you can build an
expression index that extracts all license values and indexes them
individually. A query for a specific license plate then uses the index
to find the matching record without scanning.
Once the expression index exists, queries can reference it by name instead of passing the expression again. Querying by index name avoids rebuilding the expression on the client side and is the simpler production pattern.
For worked multi-language examples of expression index creation and query-by-name, see Working with nested CDTs.
Lookup tables for unique ID resolution
When the application needs to resolve an external unique identifier (an email address, an external system ID, a social handle) to an Aerospike record key, a dedicated lookup set is usually better than a secondary index.
Create a set where the record key is the external ID and a single bin holds the corresponding Aerospike key or digest:
Set: email_lookupKey: "alice@example.com"Bin "user_key": "user:alice"Resolving the email to a user record is two sequential get calls:
one to the lookup set, one to the user set. Both are key lookups —
constant time, single node.
An SI query for the same purpose scatters to all nodes and searches the index on every node, returning one result. For a unique-value lookup, the SI approach is slower and more resource-intensive than the two-hop key lookup.
Use lookup tables when:
- The external ID is unique and maps to exactly one record.
- The lookup is on the hot path and latency matters.
- You want to avoid the management overhead associated with an SI.
Namespace-wide secondary indexes
When multiple sets share a bin with the same name and the application queries ranges on that bin across sets, a single SI over the namespace shares the pre-allocation across all sets. This is more memory-efficient than creating a separate SI per set.
If only a subset of sets needs the index, combine a namespace-wide SI
with an expression index that uses cond to skip irrelevant sets. The
expression evaluates the set name and returns unknown() for sets that
do not participate, producing a sparse index that covers only the
relevant records.
Two-phase execution for filter expressions
When you apply a filter expression to a query, scan, or batch read, the server evaluates it in two phases:
- Metadata phase. The server checks conditions that use only record metadata — generation, TTL, last-update time, set name, record size (Database 7.0+). If the metadata condition eliminates the record, the server skips the storage read entirely.
- Storage phase. If the record passes the metadata phase, the server reads the record from storage and evaluates the remaining conditions against bin values.
Place metadata conditions first in your filter expression to maximize
the number of records eliminated before storage I/O. For example, a
filter that checks since_update_time < 3600000 (less than one hour
since last update) before checking a bin value avoids reading stale
records from disk.
Index strategy summary
| Tool | Cost per entry | Query pattern | Use when |
|---|---|---|---|
| Primary index | ~64 bytes/record (always present) | Key lookup | Key is known or derivable |
| Secondary index | ~14 bytes/entry + B-tree overhead | Bin value query (scatter to all nodes) | Key is not derivable, result set is bounded |
| Set index | ~16 bytes/record × RF | All records in a set | Set is small relative to namespace |
| Expression index | Same as SI, but sparse | Computed or conditional bin value query | Subset of records, derived values |
| Lookup table | 64 bytes PI + small record | Unique ID resolution (two key lookups) | External ID maps to one record, hot path |