Skip to content

Secondary indexes

Secondary indexes enable efficient queries on bin values beyond the primary key.

What is a secondary index?

By default, Aerospike retrieves records by their primary key (digest). A secondary index lets you query records by the value of a specific bin.

Query typeBehavior
Single primary key lookupkey → record (O(1), always fast)
Multiple primary key lookupskeys → records (done in parallel across servers, usually fast)
Secondary index querybin value → records (requires index)
Primary index query(scans all records in parallel, O(N), can be slow)

Index types

TypeBin Data TypeQuery Operations
IndexType.STRING / IndexTypeEnum.STRINGStringEquality (==)
IndexType.INTEGER / IndexTypeEnum.NUMERICIntegerEquality and range comparisons (<, <=, >, >=)

When to use secondary indexes

Good use cases:

  • Filtering by status, category, or type fields
  • Range queries on timestamps or numeric IDs
  • Geospatial queries (find nearby)

Avoid when:

  • High cardinality (millions of unique values)
  • Zero or one result will be returned (eg alternate id for a record)
  • Frequently updated bins
  • Can use primary key lookup instead

Creating an index

Note: Creating indexes in code in production environments is often an anti-pattern due to resource consumption. Use tools like asadm to manage indexes in higher environments instead.

import com.aerospike.client.sdk.DataSet;
import com.aerospike.client.sdk.query.IndexCollectionType;
import com.aerospike.client.sdk.query.IndexType;
DataSet users = DataSet.of("test", "users");
// Create an integer index on the "age" bin
session.createIndex(users, "age_idx", "age", IndexType.INTEGER, IndexCollectionType.DEFAULT)
.waitTillComplete();
// Create a string index on the "status" bin
session.createIndex(users, "status_idx", "status", IndexType.STRING, IndexCollectionType.DEFAULT)
.waitTillComplete();

Querying with indexes

Once an index exists, matching AEL predicates can use it:

import com.aerospike.client.sdk.Record;
import com.aerospike.client.sdk.RecordResult;
import com.aerospike.client.sdk.RecordStream;
// This query uses the "age_idx" secondary index
RecordStream stream = session.query(users)
.where("$.age > 21")
.execute();
stream.forEach((RecordResult result) -> {
if (result.isOk()) {
Record row = result.recordOrThrow();
if (row != null) {
// Process row (for example, row.getString("name"))
}
}
});
stream.forEach(result -> {
Record row = result.recordOrThrow();
// Process row (for example, row.getString("name"))
// The row will not be null (empty rows not returned by default)
});

Complete example

import com.aerospike.client.sdk.AerospikeException;
import com.aerospike.client.sdk.Cluster;
import com.aerospike.client.sdk.ClusterDefinition;
import com.aerospike.client.sdk.DataSet;
import com.aerospike.client.sdk.Record;
import com.aerospike.client.sdk.RecordResult;
import com.aerospike.client.sdk.RecordStream;
import com.aerospike.client.sdk.Session;
import com.aerospike.client.sdk.policy.Behavior;
import com.aerospike.client.sdk.query.IndexCollectionType;
import com.aerospike.client.sdk.query.IndexType;
public class SecondaryIndexExample {
public static void main(String[] args) {
try (Cluster cluster = new ClusterDefinition("localhost", 3000).connect()) {
Session session = cluster.createSession(Behavior.DEFAULT);
DataSet users = DataSet.of("test", "users");
String k1 = "sidx-example-1";
String k2 = "sidx-example-2";
String k3 = "sidx-example-3";
String ageIndex = "age_idx_demo";
String statusIndex = "status_idx_demo";
// Cleanup so the example is repeatable.
session.delete(users.ids(k1, k2, k3)).execute().close();
try {
session.dropIndex(users, ageIndex).waitTillComplete();
} catch (AerospikeException ignored) {
// Index may not exist yet.
}
try {
session.dropIndex(users, statusIndex).waitTillComplete();
} catch (AerospikeException ignored) {
// Index may not exist yet.
}
// Seed sample data.
session.insert(users)
.bins("name", "age", "status")
.id(k1).values("Alice", 28, "sidx_active")
.id(k2).values("Bob", 42, "sidx_active")
.id(k3).values("Carol", 19, "sidx_inactive")
.execute();
// Create secondary indexes.
session.createIndex(users, ageIndex, "age", IndexType.INTEGER, IndexCollectionType.DEFAULT)
.waitTillComplete();
session.createIndex(users, statusIndex, "status", IndexType.STRING, IndexCollectionType.DEFAULT)
.waitTillComplete();
// Query using age index.
System.out.println("sidx_active users age >= 30:");
RecordStream ageQuery = session.query(users)
.where("$.status == 'sidx_active' and $.age >= 30")
.readingOnlyBins("name", "age")
.execute();
ageQuery.forEach(result -> {
Record user = result.recordOrThrow();
System.out.println(" - " + user.getString("name") + " (" + user.getInt("age") + ")");
});
// Query using status index.
System.out.println("\nsidx_active users:");
RecordStream statusQuery = session.query(users)
.where("$.status == 'sidx_active'")
.readingOnlyBins("name", "status")
.execute();
statusQuery.forEach(result -> {
Record user = result.recordOrThrow();
System.out.println(" - " + user.getString("name"));
});
}
}
}

Performance considerations

  • Indexes consume memory on every node
  • Index updates add write latency
  • Queries without indexes scan entire set (slow)
  • Monitor index memory usage in production

Next steps

Feedback

Was this page helpful?

What type of feedback are you giving?

What would you like us to know?

+Capture screenshot

Can we reach out to you?