---
title: "Secondary index queries"
description: "Learn Aerospike secondary index queries: types, creation, CDT contexts, expressions, and background operations."
---

# Secondary index queries

> For the complete documentation index see: [llms.txt](https://aerospike.com/docs/llms.txt)
> 
> All documentation pages available in markdown.

Queries matching a (relatively) small subset of records within a large Aerospike namespace or set can be accelerated by optionally indexing often-queried values with a [secondary index](https://aerospike.com/docs/database/learn/architecture/data-storage/secondary-index). Such secondary index (SI) queries run faster than equivalent [primary index (PI) queries](https://aerospike.com/docs/develop/learn/queries/primary-index/). This is a trade-off between (typically in-memory) index storage and performance.

Secondary indexes can index the value of a specified record bin or the computed value of an expression applied to the record.

PI and SI queries are served by the same subsystem and share the same policies and features. The SI query is the same as a PI query with the additional ability to use an index filter - a simple predicate leveraging the secondary index. The SI query can then apply a filter expression to the records first matched by the index filter predicate.

Read-only SI queries use the same [projection](https://aerospike.com/docs/develop/learn/queries/projection) options as primary index queries (bin projection; operation projection on Database 8.1.2 and later).

::: note
-   Querying expression indexes was introduced by Database 8.1.0.
-   Prior to Aerospike Database 6 primary index (PI) queries were called scans and secondary index (SI) queries were called queries. See the [Queries](https://aerospike.com/docs/develop/learn/queries) feature guide.
:::

## Types of SI queries

There are four types of SI queries:

-   [Foreground](https://aerospike.com/docs/develop/learn/queries/secondary-index#foreground-queries) - read-only, returns records to the client.
-   [Aggregation](https://aerospike.com/docs/database/learn/architecture/aggregation) - read-only, returns aggregated results to the client. Aggregations use stream UDFs with map/reduce functions to perform aggregation.
-   Background UDF - write-only, returns nothing to client. Uses a [Record UDF](https://aerospike.com/docs/database/learn/architecture/udf/#record-udfs) to modify records.
-   Background [ops](https://aerospike.com/docs/develop/learn/bin-operations/) - write-only, returns nothing to client.

All types use an index filter to define a predicate used directly against a secondary index:

-   _Equals_ comparison against indexed string, blob or integer values.
-   _Range_ comparisons against indexed integer values. Range query results are _inclusive_, that is both low end and high end boundary values are included in the results.
-   [_Point-In-Region or Region-Contain-Point_](https://aerospike.com/docs/develop/data-types/geospatial/#geospatial-index) comparisons against geospatial indexes.

A [record filter expression](https://aerospike.com/docs/develop/expressions/#record-filtering-with-expressions) can be added to apply further predicates to records matched by the index filter. With the filter expressions, you can:

-   Filter out records based on record metadata, such as last update time or storage size.
-   Filter out records based on bin data, such as integer size thresholds or regular expressions on string bins.
-   Note: aggregation queries do not support filter expressions. Stream UDFs have their own filter function mechanism.

You can fine-tune the Aerospike query system for your use case based on:

-   Query selectivity - the average number of records returned. When multiple secondary indexes are available, the application should query the most selective index (meaning the index with the lowest [`entries_per_bval`](https://aerospike.com/docs/database/reference/info#sindex-stat)). The purpose of the secondary index is to reduce, as much as possible, the number of records read from namespace data storage. A filter expression can be applied to these records, in order to further exclude records from being streamed back to the client waiting for the query results.
-   Throughput and latency.
-   Query load vs. read/write load — the frequency that the secondary index updates.
-   SSD parallelism — the IOPS required to support query throughput.

### Key data type

One part of a secondary index definition is the expected data type of the value(s) being indexed. This is termed the key type, or ktype. Secondary indexes key on bin values (RDBMS _columns_). Secondary index keys can only be created on the following primitive data types:

-   [Integer](https://aerospike.com/docs/develop/data-types/scalar/#integer)
-   [String](https://aerospike.com/docs/develop/data-types/scalar/#string)
-   [Blob](https://aerospike.com/docs/develop/data-types/blob)
-   [Geospatial](https://aerospike.com/docs/develop/data-types/geospatial/#geospatial-data)

In Aerospike, distinct secondary indexes are created on a combination of namespace, set name, bin name, key data type, and index type. You can also create a secondary index on a [CDT context](https://aerospike.com/docs/develop/data-types/collections/context/) of a map (document) or list collection data type (CDT) bin to identify the path to the value that should be indexed.

### Index type

Aerospike supports four index types (AKA itype):

-   [Default](https://aerospike.com/docs/develop/data-types/scalar/) where a single value of the given ktype is expected.
-   [List](https://aerospike.com/docs/develop/data-types/collections/list) where a list of values of the given ktype is expected.
-   [Map keys](https://aerospike.com/docs/develop/data-types/collections/map/) where the ktype-matching keys of a map CDT are indexed.
-   [Map values](https://aerospike.com/docs/develop/data-types/collections/map/) where the ktype-matching values of a map CDT are indexed.

### Caveats

Secondary indexes are built independently on each cluster node, a structure for each data partition, which means that a secondary index query hits all the nodes. For better performance, secondary indexes should not be used when key-based commands are an alternative.

For example, secondary indexes should not be built on a “primary key”. The record already has an entry in the primary index for direct, low latency access with read, upsert and delete commands.

Similarly, secondary indexes are not the most efficient way to implement a unique index query (where the [`entries_per_bval`](https://aerospike.com/docs/database/reference/info#sindex-stat) index statistic is equal to 1). Instead, a lookup table provides significantly better performance and throughput. A distinct set can be used to insert all the unique identifiers as record keys with a digest stored in a record bin. A single read operation against the lookup table retrieves the digest of the actual record, which is then used to access that record directly in a subsequent read command. This two-phase lookup pattern is one or two orders of magnitude faster than a secondary index query to find zero or one record.

### Limitations

-   Aerospike supports up to 256 secondary indexes per namespace.
-   String and blob/bytes data types are limited to less than 2048 bytes - larger values are silently excluded from the index on write, and queries with an oversized predicate value fail with error code 4 (`AEROSPIKE_ERR_REQUEST_INVALID`). See [secondary index byte-size limits](https://aerospike.com/docs/database/reference/limitations#secondary-index).
-   Migration-tolerant queries were introduced in Aerospike Database 6 and require [fully compatible clients](https://aerospike.com/docs/develop/client-matrix#full-clientserver-feature-compatibility).

## Foreground queries

[Foreground queries](https://aerospike.com/docs/develop/learn/queries/#foreground-queries) are read-only. To use a secondary index query in your application:

1.  Create secondary indexes on a bin value (in the case of List or Map bins it can be a nested CDT context) or computed value of an expression.
2.  Insert/update records. These write operations will trigger synchronous indexing.
3.  Construct a query with a secondary index filter predicate, optionally adding more predicates with a filter expression. Execute the SI query and process the records streaming back to the client. Use [operation projection](https://aerospike.com/docs/develop/learn/queries/projection#supported-projection-operations) to project read operations or read expressions in the result.

### Create an index

You create and manage secondary indexes with the [`manage sindex`](https://aerospike.com/docs/database/tools/asadm/live-mode/#sindex) commands of the Aerospike admin tool ([`asadm`](https://aerospike.com/docs/database/tools/asadm)). Alternatively, use the [`sindex-create`](https://aerospike.com/docs/database/reference/info/#sindex-create) and other `sindex-*` info commands. If you are creating a database administration application, a secondary index can also be created programmatically with the `createIndex` and `dropIndex` equivalent client API calls in each language client.

::: note
Creating an index requires the `sindex-admin` [privilege](https://aerospike.com/docs/database/manage/security/rbac/#privileges) if security is turned on.
:::

When you create a secondary index:

-   If a set name is specified, the index covers only records in that set.
-   If no set name is specified:
    -   **Database 6.1.0 and later:** the index covers all records in the namespace, regardless of set membership. In `show sindex` output, these indexes display `NULL` in the Set column.
    -   **Prior to Database 6.1.0:** the index covers only records not belonging to any set (the _null set_).

Create a bin index

```bash
Admin+> manage sindex create numeric dnr-age-idx ns test set donor bin age

Admin+> show sindex

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Secondary Indexes (2026-04-27 16:18:57 UTC)~~~~~~~~~~~~~~

                   Index Name|Namespace|       Set|       Bin|    Bin|  Index|State|     Expression

                             |         |          |          |   Type|   Type|     |

dnr-age-idx                  |test     |donor     |       age|numeric|default|RW   |--

Number of rows: 1
```

Indexing a value embedded inside a list or map requires a CDT context describing the path to the element.

Create a bin index with a CDT context

```bash
Admin+> manage sindex create numeric dnr-zip-idx ns test set donor bin address ctx map_key(zip)

Admin+> show sindex

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Secondary Indexes (2026-04-27 16:21:06 UTC)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                   Index Name|Namespace|       Set|       Bin|    Bin|  Index|State|              Context|      Expression

                             |         |          |          |   Type|   Type|     |                     |

dnr-zip-idx                  |test     |donor     |   address|numeric|default|RW   |[map_key(<string#3>)]|--

Number of rows: 1
```

Alternatively, this same index can be created with the base64-encoded CDT context

Terminal window

```bash
Admin+> manage sindex create numeric dnr-zip-idx ns test set donor bin address ctx_base64 kiKkA3ppcA==
```

To create an expression index you don’t specify a bin name or context; instead you provide a base64-encoded expression. The computed value must adhere to the specified ktype and itype. To skip indexing the record send back an `unknown()` from the expression.

-   [Java](#tab-panel-2880)
-   [Python](#tab-panel-2881)
-   [Go](#tab-panel-2882)
-   [C](#tab-panel-2883)
-   [C#](#tab-panel-2884)
-   [Node.js](#tab-panel-2885)

Assemble a base64-encoded expression

```java
Expression campaignTotal = Exp.build(

    Exp.add(Exp.intBin("campaign1"), Exp.intBin("campaign2"), Exp.intBin("campaign3"))

);

System.out.println(campaignTotal.getBase64());

// lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=
```

Assemble a base64-encoded expression

```python
sum_expr = exp.Add(

    exp.IntBin("campaign1"),

    exp.IntBin("campaign2"),

    exp.IntBin("campaign3"),

).compile()

b64 = client.get_expression_base64(sum_expr)

print(b64)

# lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=
```

Assemble a base64-encoded expression

```go
campaignTotal := as.ExpNumAdd(

    as.ExpIntBin("campaign1"),

    as.ExpIntBin("campaign2"),

    as.ExpIntBin("campaign3"),

)

b64, _ := campaignTotal.Base64()

fmt.Println(b64)

// lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=
```

Assemble a base64-encoded expression

```c
as_exp_build(campaign_total,

    as_exp_add(

        as_exp_bin_int("campaign1"),

        as_exp_bin_int("campaign2"),

        as_exp_bin_int("campaign3")));

char* b64 = as_exp_to_base64(campaign_total);

printf("%s\n", b64);

// lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=

as_exp_destroy_base64(b64);

as_exp_destroy(campaign_total);
```

Assemble a base64-encoded expression

```csharp
Expression campaignTotal = Exp.Build(

    Exp.Add(Exp.IntBin("campaign1"), Exp.IntBin("campaign2"), Exp.IntBin("campaign3"))

);

Console.WriteLine(campaignTotal.GetBase64());

// lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=
```

Assemble a base64-encoded expression

```js
const sumExp = exp.add(

  exp.binInt("campaign1"),

  exp.binInt("campaign2"),

  exp.binInt("campaign3")

);

const b64 = client.expressionToBase64(sumExp);

console.log(b64);

// lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=
```

The base64-encoded expression is used to create the expression index

Create an expression index

```bash
Admin+> manage sindex create numeric dnr-sum-idx ns test set donor exp_base64 lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=

Use 'show sindex' to confirm dnr-sum-idx was created successfully.
```

After the secondary index creation command reaches one node in the cluster, the cluster propagates the request to all cluster nodes. Each node begin building the secondary index on its local data.

In asadm, [`show sindex`](https://aerospike.com/docs/database/tools/asadm/live-mode/#show-sindex) describes all the existing secondary indexes assembled on the namespace and its sets.

Terminal window

```bash
Admin+> show sindex

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Secondary Indexes (2026-04-08 22:51:37 UTC)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 Index Name|Namespace|  Set|    Bin|    Bin|  Index|State|              Context|                                                           Expression

           |         |     |       |   Type|   Type|     |                     |

dnr-age-idx|test     |donor|    age|numeric|default|RW   |--                   |--

dnr-sum-idx|test     |donor|   null|numeric|default|RW   |--                   |add(bin_int("campaign1"), bin_int("campaign2"), bin_int("campaign3"))

dnr-zip-idx|test     |donor|address|numeric|default|RW   |[map_key(<string#3>)]|--

Number of rows: 3
```

::: note
Nodes can only be queried when the index is in read-write state (`RW`). If all nodes are not in the `RW` state and a secondary index query is made against the cluster, an error with no data is returned to the client.
:::

### Insert data

Insert data programmatically using one of the Aerospike client libraries. The following examples insert a record into the `donor` set with this structure:

Example record in the set 'donor'

```json
{

  name: "Austin Albertson",

  age: 57,

  campaign1: 150,

  campaign2: 100,

  campaign3: 75,

  address: {

    state: "CA",

    city: "Los Altos",

    zip: 94023,

  }

}
```

-   [Java](#tab-panel-2886)
-   [Python](#tab-panel-2887)
-   [Go](#tab-panel-2888)
-   [C](#tab-panel-2889)
-   [C#](#tab-panel-2890)
-   [Node.js](#tab-panel-2891)

```java
Key key = new Key("test", "donor", "donor1");

Map<String, Object> address = new HashMap<>();

address.put("state", "CA");

address.put("city", "Los Altos");

address.put("zip", 94023);

client.put(null, key,

    new Bin("name", "Austin Albertson"),

    new Bin("age", 57),

    new Bin("campaign1", 150),

    new Bin("campaign2", 100),

    new Bin("campaign3", 75),

    new Bin("address", address));
```

```python
key = ("test", "donor", "donor1")

address = {"state": "CA", "city": "Los Altos", "zip": 94023}

bins = {

    "name": "Austin Albertson",

    "age": 57,

    "campaign1": 150,

    "campaign2": 100,

    "campaign3": 75,

    "address": address,

}

client.put(key, bins)
```

```go
key, _ := as.NewKey("test", "donor", "donor1")

address := map[interface{}]interface{}{

    "state": "CA",

    "city":  "Los Altos",

    "zip":   94023,

}

client.PutBins(nil, key,

    as.NewBin("name", "Austin Albertson"),

    as.NewBin("age", 57),

    as.NewBin("campaign1", 150),

    as.NewBin("campaign2", 100),

    as.NewBin("campaign3", 75),

    as.NewBin("address", address),

)
```

```c
as_key key;

as_key_init_str(&key, "test", "donor", "donor1");

as_hashmap address;

as_hashmap_init(&address, 3);

as_stringmap_set_str((as_map*)&address, "state", "CA");

as_stringmap_set_str((as_map*)&address, "city", "Los Altos");

as_stringmap_set_int64((as_map*)&address, "zip", 94023);

as_record rec;

as_record_inita(&rec, 6);

as_record_set_str(&rec, "name", "Austin Albertson");

as_record_set_int64(&rec, "age", 57);

as_record_set_int64(&rec, "campaign1", 150);

as_record_set_int64(&rec, "campaign2", 100);

as_record_set_int64(&rec, "campaign3", 75);

as_record_set_map(&rec, "address", (as_map*)&address);

aerospike_key_put(&as, &err, NULL, &key, &rec);

as_record_destroy(&rec);
```

```csharp
Key key = new Key("test", "donor", "donor1");

Dictionary<string, object> address = new()

{

    ["state"] = "CA",

    ["city"] = "Los Altos",

    ["zip"] = 94023,

};

client.Put(null, key,

    new Bin("name", "Austin Albertson"),

    new Bin("age", 57),

    new Bin("campaign1", 150),

    new Bin("campaign2", 100),

    new Bin("campaign3", 75),

    new Bin("address", address));
```

```js
const key = new Aerospike.Key("test", "donor", "donor1");

const address = { state: "CA", city: "Los Altos", zip: 94023 };

await client.put(key, {

  name: "Austin Albertson",

  age: 57,

  campaign1: 150,

  campaign2: 100,

  campaign3: 75,

  address,

});
```

### Query a bin index

The bin index `dnr-age-idx` allows you to query for an integer age range.

**index filter:** age >= 18

**filter expression:** campaign1 > 200

-   [Java](#tab-panel-2892)
-   [Python](#tab-panel-2893)
-   [Go](#tab-panel-2894)
-   [C](#tab-panel-2895)
-   [C#](#tab-panel-2896)
-   [Node.js](#tab-panel-2897)

```java
Statement stmt = new Statement();

stmt.setNamespace("test");

stmt.setSetName("donor");

stmt.setFilter(Filter.range("age", 18, Integer.MAX_VALUE));

QueryPolicy queryPolicy = new QueryPolicy();

queryPolicy.filterExp = Exp.build(

    Exp.gt(Exp.intBin("campaign1"), Exp.val(200))

);

RecordSet recordSet = client.query(queryPolicy, stmt);
```

```python
query = client.query("test", "donor")

query.where(p.between("age", 18, 2**31 - 1))

expr = exp.GT(exp.IntBin("campaign1"), exp.Val(200)).compile()

policy = {"expressions": expr}

results = query.results(policy)
```

```go
stmt := as.NewStatement("test", "donor")

stmt.SetFilter(as.NewRangeFilter("age", 18, math.MaxInt64))

queryPolicy := as.NewQueryPolicy()

queryPolicy.FilterExpression = as.ExpGreater(

    as.ExpIntBin("campaign1"),

    as.ExpIntVal(200),

)

rs, _ := client.Query(queryPolicy, stmt)
```

```c
as_query q;

as_query_init(&q, "test", "donor");

as_query_where_inita(&q, 1);

as_query_where(&q, "age", as_integer_range(18, INT64_MAX));

as_exp_build(filter,

    as_exp_cmp_gt(as_exp_bin_int("campaign1"), as_exp_int(200)));

as_policy_query p;

as_policy_query_init(&p);

p.base.filter_exp = filter;

aerospike_query_foreach(&as, &err, &p, &q, query_cb, NULL);

as_exp_destroy(filter);

as_query_destroy(&q);
```

```csharp
Statement stmt = new();

stmt.SetNamespace("test");

stmt.SetSetName("donor");

stmt.SetFilter(Filter.Range("age", 18, long.MaxValue));

QueryPolicy queryPolicy = new()

{

    filterExp = Exp.Build(

        Exp.GT(Exp.IntBin("campaign1"), Exp.Val(200)))

};

RecordSet recordSet = client.Query(queryPolicy, stmt);
```

```js
const query = client.query("test", "donor");

query.where(Aerospike.filter.range("age", 18, Number.MAX_SAFE_INTEGER));

const queryPolicy = new Aerospike.QueryPolicy({

  filterExpression: exp.gt(exp.binInt("campaign1"), exp.int(200)),

});

const stream = query.foreach(queryPolicy);
```

In this case, you have a secondary index on `age` but no index on `campaign1`. The index filter predicate finds records matching the age criteria, and then the filter expression is applied to each of them.

### Query a namespace-wide index (no set)

When you create an index without specifying a set, it covers all records in the namespace. This is useful for bins that exist across multiple sets, such as a `created` timestamp.

Create a namespace-wide index

```bash
Admin+> manage sindex create numeric ns-created-idx ns test bin created
```

Use `show sindex` to confirm `ns-created-idx` was created successfully.

Verify the index: Set column shows NULL

```bash
Admin+> show sindex

~~~~~~~~~~~~~~~~~~~~~~Secondary Indexes~~~~~~~~~~~~~~~~~~~~~~

      Index Name|Namespace|  Set|     Bin|    Bin|  Index|State

                |         |     |        |   Type|   Type|

ns-created-idx  |test     |NULL |created |numeric|default|RW
```

To query this index, omit the set from the statement. The query returns matching records from all sets and from records with no set.

**index filter:** created BETWEEN one\_week\_ago AND now

-   [Java](#tab-panel-2898)
-   [Python](#tab-panel-2899)
-   [Go](#tab-panel-2900)
-   [C](#tab-panel-2901)
-   [C#](#tab-panel-2902)
-   [Node.js](#tab-panel-2903)

```java
Statement stmt = new Statement();

stmt.setNamespace("test");

stmt.setFilter(Filter.range("created", oneWeekAgo, now));

RecordSet recordSet = client.query(null, stmt);
```

```python
query = client.query("test")

query.where(p.between("created", one_week_ago, now))

results = query.results()
```

```go
stmt := as.NewStatement("test", "")

stmt.SetFilter(as.NewRangeFilter("created", oneWeekAgo, now))

rs, _ := client.Query(nil, stmt)
```

```c
as_query q;

as_query_init(&q, "test", NULL);

as_query_where_inita(&q, 1);

as_query_where(&q, "created", as_integer_range(one_week_ago, now));

aerospike_query_foreach(&as, &err, NULL, &q, query_cb, NULL);

as_query_destroy(&q);
```

```csharp
Statement stmt = new();

stmt.SetNamespace("test");

stmt.SetFilter(Filter.Range("created", oneWeekAgo, now));

RecordSet recordSet = client.Query(null, stmt);
```

```js
const query = client.query("test");

query.where(Aerospike.filter.range("created", oneWeekAgo, now));

const stream = query.foreach();
```

### Query a bin index using CDT context

The bin index `dnr-zip-idx` indexes the integer value of an embedded field `zip` of Map bin `address`.

**index filter:** address.zip = 94023

**filter expression:** age >= 18

-   [Java](#tab-panel-2904)
-   [Python](#tab-panel-2905)
-   [Go](#tab-panel-2906)
-   [C](#tab-panel-2907)
-   [C#](#tab-panel-2908)
-   [Node.js](#tab-panel-2909)

```java
Statement stmt = new Statement();

stmt.setNamespace("test");

stmt.setSetName("donor");

stmt.setFilter(Filter.equal("address", 94023,

    CTX.mapKey(Value.get("zip"))));

QueryPolicy queryPolicy = new QueryPolicy();

queryPolicy.filterExp = Exp.build(

    Exp.ge(Exp.intBin("age"), Exp.val(18))

);

RecordSet recordSet = client.query(queryPolicy, stmt);
```

```python
from aerospike_helpers import cdt_ctx

query = client.query("test", "donor")

ctx = [cdt_ctx.cdt_ctx_map_key("zip")]

query.where(p.equals("address", 94023), ctx)

expr = exp.GE(exp.IntBin("age"), exp.Val(18)).compile()

policy = {"expressions": expr}

results = query.results(policy)
```

```go
stmt := as.NewStatement("test", "donor")

stmt.SetFilter(as.NewEqualFilter("address", 94023,

    as.CtxMapKey(as.NewStringValue("zip"))))

qp := as.NewQueryPolicy()

qp.FilterExpression = as.ExpGreaterEq(

    as.ExpIntBin("age"),

    as.ExpIntVal(18),

)

rs, _ := client.Query(qp, stmt)
```

```c
as_string zip_key;

as_string_init(&zip_key, "zip", false);

as_cdt_ctx ctx;

as_cdt_ctx_init(&ctx, 1);

as_cdt_ctx_add_map_key(&ctx, (as_val *)&zip_key);

as_query q;

as_query_init(&q, "test", "donor");

as_query_where_inita(&q, 1);

as_query_where_with_ctx(&q, "address", &ctx,

    as_integer_equals(94023));

as_exp_build(filter,

    as_exp_cmp_ge(as_exp_bin_int("age"), as_exp_int(18)));

as_policy_query p;

as_policy_query_init(&p);

p.base.filter_exp = filter;

aerospike_query_foreach(&as, &err, &p, &q, query_cb, NULL);

as_exp_destroy(filter);

as_cdt_ctx_destroy(&ctx);

as_query_destroy(&q);
```

```csharp
Statement stmt = new();

stmt.SetNamespace("test");

stmt.SetSetName("donor");

stmt.SetFilter(Filter.Equal("address", 94023,

    CTX.MapKey(Value.Get("zip"))));

QueryPolicy queryPolicy = new()

{

    filterExp = Exp.Build(

        Exp.GE(Exp.IntBin("age"), Exp.Val(18)))

};

RecordSet recordSet = client.Query(queryPolicy, stmt);
```

```js
const query = client.query("test", "donor");

query.where(Aerospike.filter.contains("address", 94023,

  Aerospike.indexType.DEFAULT,

  new Aerospike.cdt.Context().addMapKey("zip")));

const queryPolicy = new Aerospike.QueryPolicy({

  filterExpression: exp.ge(exp.binInt("age"), exp.int(18)),

});

const stream = query.foreach(queryPolicy);
```

You now have two potential secondary indexes to use. Using `entries_per_bval` in the `Avg Per Bin Val` column of asadm’s `info sindex` or the [`sindex-stat`](https://aerospike.com/docs/database/reference/info#sindex-stat) info command, you can compare how many records share the same indexed value on average. The index with the lowest `entries_per_bval` is the most selective — use it for the index filter. In a production dataset with many donors, `dnr-zip-idx` would typically be more selective than `dnr-age-idx`, so you would choose it for the index filter and apply the age condition through a filter expression onto the records matched by the secondary index.

Terminal window

```bash
Admin+> info sindex

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Secondary Index Information (2026-04-08 22:51:43 UTC)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

 Index Name|Namespace|  Set|                       Node|    Bin|    Bin|     State|~~~~~~~~Entries~~~~~~~~|~~~~Storage~~~~|              Context|                                                           Expression

           |         |     |                           |       |   Type|          |  Total|Avg Per|Avg Per| Type|     Used|                     |

           |         |     |                           |       |       |          |       |    Rec|Bin Val|     |         |                     |

dnr-age-idx|test     |donor|1.0.0.127:3000|age    |numeric|Read-Write|1.000  |0.000  |0.000  |shmem|16.000 MB|--                   |--

dnr-sum-idx|test     |donor|1.0.0.127:3000|null   |numeric|Read-Write|1.000  |0.000  |0.000  |shmem|16.000 MB|--                   |add(bin_int("campaign1"), bin_int("campaign2"), bin_int("campaign3"))

dnr-zip-idx|test     |donor|1.0.0.127:3000|address|numeric|Read-Write|1.000  |0.000  |0.000  |shmem|16.000 MB|[map_key(<string#3>)]|--

           |test     |donor|                           |       |       |          |3.000  |       |0.000  |     |48.000 MB|                     |

Number of rows: 3

Admin+> asinfo -v 'sindex-stat:namespace=test;indexname=dnr-zip-idx'

1.0.0.127:3000 (127.0.0.1) returned:

entries=1;used_bytes=16777216;entries_per_bval=0;entries_per_rec=0;load_pct=100;load_time=0;stat_gc_recs=0

Admin+> asinfo -v 'sindex-stat:namespace=test;indexname=dnr-age-idx'

1.0.0.127:3000 (127.0.0.1) returned:

entries=1;used_bytes=16777216;entries_per_bval=0;entries_per_rec=0;load_pct=100;load_time=0;stat_gc_recs=0
```

### Query an expression index by index name

The expression index `dnr-sum-idx` can be queried using its index name.

**index filter:** dnr-sum-idx BETWEEN 200 AND 2000

**filter expression:** age >= 18

-   [Java](#tab-panel-2910)
-   [Python](#tab-panel-2911)
-   [Go](#tab-panel-2912)
-   [C](#tab-panel-2913)
-   [C#](#tab-panel-2914)
-   [Node.js](#tab-panel-2915)

```java
Statement stmt = new Statement();

stmt.setNamespace("test");

stmt.setSetName("donor");

stmt.setFilter(Filter.rangeByIndex("dnr-sum-idx", 200, 2000));

QueryPolicy queryPolicy = new QueryPolicy();

queryPolicy.filterExp = Exp.build(

    Exp.ge(Exp.intBin("age"), Exp.val(18))

);

RecordSet recordSet = client.query(queryPolicy, stmt);
```

```python
query = client.query("test", "donor")

query.where_with_index_name("dnr-sum-idx", p.between(None, 200, 2000))

expr = exp.GE(exp.IntBin("age"), exp.Val(18)).compile()

policy = {"expressions": expr}

results = query.results(policy)
```

```go
stmt := as.NewStatement("test", "donor")

stmt.SetFilter(as.NewRangeWithIndexNameFilter("dnr-sum-idx", 200, 2000))

queryPolicy := as.NewQueryPolicy()

queryPolicy.FilterExpression = as.ExpGreaterEq(

    as.ExpIntBin("age"),

    as.ExpIntVal(18),

)

rs, _ := client.Query(queryPolicy, stmt)
```

```c
as_query q;

as_query_init(&q, "test", "donor");

as_query_where_inita(&q, 1);

as_query_where_with_index_name(&q, "dnr-sum-idx",

    as_integer_range(200, 2000));

as_exp_build(filter,

    as_exp_cmp_ge(as_exp_bin_int("age"), as_exp_int(18)));

as_policy_query p;

as_policy_query_init(&p);

p.base.filter_exp = filter;

aerospike_query_foreach(&as, &err, &p, &q, query_cb, NULL);

as_exp_destroy(filter);

as_query_destroy(&q);
```

```csharp
Statement stmt = new();

stmt.SetNamespace("test");

stmt.SetSetName("donor");

stmt.SetFilter(Filter.RangeByIndex("dnr-sum-idx", 200, 2000));

QueryPolicy queryPolicy = new()

{

    filterExp = Exp.Build(

        Exp.GE(Exp.IntBin("age"), Exp.Val(18)))

};

RecordSet recordSet = client.Query(queryPolicy, stmt);
```

```js
const query = client.query("test", "donor");

query.whereWithIndexName(

  Aerospike.filter.range(null, 200, 2000),

  "dnr-sum-idx"

);

const queryPolicy = new Aerospike.QueryPolicy({

  filterExpression: exp.ge(exp.binInt("age"), exp.int(18)),

});

const stream = query.foreach(queryPolicy);
```

### Query an expression index using the expression

The expression index `dnr-sum-idx` can be queried using a matching expression.

**index filter:** add(bin\_int(“campaign1”), bin\_int(“campaign2”), bin\_int(“campaign3”)) BETWEEN 200 AND 2000

**filter expression:** age >= 18

-   [Java](#tab-panel-2916)
-   [Python](#tab-panel-2917)
-   [Go](#tab-panel-2918)
-   [C](#tab-panel-2919)
-   [C#](#tab-panel-2920)
-   [Node.js](#tab-panel-2921)

```java
Statement stmt = new Statement();

stmt.setNamespace("test");

stmt.setSetName("donor");

Expression sumExp = Exp.build(Exp.add(Exp.intBin("campaign1"),

    Exp.intBin("campaign2"), Exp.intBin("campaign3")));

stmt.setFilter(Filter.range(sumExp, 200, 2000));

QueryPolicy queryPolicy = new QueryPolicy();

queryPolicy.filterExp = Exp.build(

    Exp.ge(Exp.intBin("age"), Exp.val(18))

);

RecordSet recordSet = client.query(queryPolicy, stmt);
```

```python
sum_expr = exp.Add(

    exp.IntBin("campaign1"),

    exp.IntBin("campaign2"),

    exp.IntBin("campaign3"),

).compile()

query = client.query("test", "donor")

query.where_with_expr(sum_expr, p.between(None, 200, 2000))

age_filter = exp.GE(exp.IntBin("age"), exp.Val(18)).compile()

policy = {"expressions": age_filter}

results = query.results(policy)
```

```go
sumExp := as.ExpNumAdd(

    as.ExpIntBin("campaign1"),

    as.ExpIntBin("campaign2"),

    as.ExpIntBin("campaign3"),

)

stmt := as.NewStatement("test", "donor")

stmt.SetFilter(as.NewRangeWithExpressionFilter(sumExp, 200, 2000))

queryPolicy := as.NewQueryPolicy()

queryPolicy.FilterExpression = as.ExpGreaterEq(

    as.ExpIntBin("age"),

    as.ExpIntVal(18),

)

rs, _ := client.Query(queryPolicy, stmt)
```

```c
as_exp_build(sum_exp,

    as_exp_add(

        as_exp_bin_int("campaign1"),

        as_exp_bin_int("campaign2"),

        as_exp_bin_int("campaign3")));

as_query q;

as_query_init(&q, "test", "donor");

as_query_where_inita(&q, 1);

as_query_where_with_exp(&q, sum_exp, as_integer_range(200, 2000));

as_exp_build(filter,

    as_exp_cmp_ge(as_exp_bin_int("age"), as_exp_int(18)));

as_policy_query p;

as_policy_query_init(&p);

p.base.filter_exp = filter;

aerospike_query_foreach(&as, &err, &p, &q, query_cb, NULL);

as_exp_destroy(filter);

as_exp_destroy(sum_exp);

as_query_destroy(&q);
```

```csharp
Statement stmt = new();

stmt.SetNamespace("test");

stmt.SetSetName("donor");

Expression sumExp = Exp.Build(Exp.Add(Exp.IntBin("campaign1"),

    Exp.IntBin("campaign2"), Exp.IntBin("campaign3")));

stmt.SetFilter(Filter.Range(sumExp, 200, 2000));

QueryPolicy queryPolicy = new()

{

    filterExp = Exp.Build(

        Exp.GE(Exp.IntBin("age"), Exp.Val(18)))

};

RecordSet recordSet = client.Query(queryPolicy, stmt);
```

```js
const sumExp = exp.add(

  exp.binInt("campaign1"),

  exp.binInt("campaign2"),

  exp.binInt("campaign3")

);

const query = client.query("test", "donor");

query.whereWithExp(Aerospike.filter.range(null, 200, 2000), sumExp);

const queryPolicy = new Aerospike.QueryPolicy({

  filterExpression: exp.ge(exp.binInt("age"), exp.int(18)),

});

const stream = query.foreach(queryPolicy);
```

You can get the expression used for a secondary index through asadm’s `show sindex` command, and get either the human-readable expression or its base64-encoded format using the [`sindex-list`](https://aerospike.com/docs/database/reference/info#sindex-list) info command.

## Background queries

[Background queries](https://aerospike.com/docs/develop/learn/queries/#background-queries) modify records in place on the server. The example below uses the `dnr-age-idx` secondary index to target donors aged 30-65 and award a campaign bonus by incrementing `campaign1` by 50, throttled to 1000 records per second per node.

A filter expression checks that the `update_pass` bin is less than 5 (or doesn’t exist), and the operations both increment `campaign1` and set `update_pass` to 5. This makes the query idempotent — re-running it skips records that were already updated.

**index filter:** dnr-age-idx BETWEEN 30 AND 65

**filter expression:** update\_pass < 5 (or bin does not exist)

**operations:** increment campaign1 by 50; set update\_pass to 5

**RPS limit:** 1000

-   [Java](#tab-panel-2922)
-   [Python](#tab-panel-2923)
-   [Go](#tab-panel-2924)
-   [C](#tab-panel-2925)
-   [C#](#tab-panel-2926)
-   [Node.js](#tab-panel-2927)

```java
Statement stmt = new Statement();

stmt.setNamespace("test");

stmt.setSetName("donor");

stmt.setFilter(Filter.range("age", 30, 65));

stmt.setRecordsPerSecond(1000);

WritePolicy writePolicy = new WritePolicy();

writePolicy.filterExp = Exp.build(

    Exp.or(

        Exp.not(Exp.binExists("update_pass")),

        Exp.lt(Exp.intBin("update_pass"), Exp.val(5))));

ExecuteTask task = client.execute(writePolicy, stmt,

    Operation.add(new Bin("campaign1", 50)),

    Operation.put(new Bin("update_pass", 5)));

task.waitTillComplete();
```

```python
from aerospike_helpers.operations import operations

from aerospike_helpers.expressions import base as exp

query = client.query("test", "donor")

query.where(p.between("age", 30, 65))

query.records_per_second = 1000

expr = exp.Or(

    exp.Not(exp.BinExists("update_pass")),

    exp.LT(exp.IntBin("update_pass"), exp.Val(5)),

).compile()

policy = {"expressions": expr}

ops = [

    operations.increment("campaign1", 50),

    operations.write("update_pass", 5),

]

query.add_ops(ops)

query.execute_background(policy)
```

```go
stmt := as.NewStatement("test", "donor")

stmt.SetFilter(as.NewRangeFilter("age", 30, 65))

qp := as.NewQueryPolicy()

qp.RecordsPerSecond = 1000

qp.FilterExpression = as.ExpOr(

    as.ExpNot(as.ExpBinExists("update_pass")),

    as.ExpLess(as.ExpIntBin("update_pass"), as.ExpIntVal(5)))

task, err := client.QueryExecute(qp, nil, stmt,

    as.AddOp(as.NewBin("campaign1", 50)),

    as.PutOp(as.NewBin("update_pass", 5)))

if err != nil {

    panic(err)

}

<-task.OnComplete()
```

```c
as_query q;

as_query_init(&q, "test", "donor");

as_query_where_inita(&q, 1);

as_query_where(&q, "age", as_integer_range(30, 65));

q.records_per_second = 1000;

as_exp_build(filter,

    as_exp_or(

        as_exp_not(as_exp_bin_exists("update_pass")),

        as_exp_cmp_lt(as_exp_bin_int("update_pass"), as_exp_int(5))));

as_operations ops;

as_operations_inita(&ops, 2);

as_operations_add_incr(&ops, "campaign1", 50);

as_operations_add_write_int64(&ops, "update_pass", 5);

q.ops = &ops;

as_policy_write wp;

as_policy_write_init(&wp);

wp.base.filter_exp = filter;

uint64_t query_id = 0;

aerospike_query_background(&as, &err, &wp, &q, &query_id);

aerospike_query_wait(&as, &err, NULL, &q, query_id, 0);

as_exp_destroy(filter);

as_query_destroy(&q);
```

```csharp
Statement stmt = new();

stmt.SetNamespace("test");

stmt.SetSetName("donor");

stmt.SetFilter(Filter.Range("age", 30, 65));

stmt.RecordsPerSecond = 1000;

WritePolicy writePolicy = new()

{

    filterExp = Exp.Build(

        Exp.Or(

            Exp.Not(Exp.BinExists("update_pass")),

            Exp.LT(Exp.IntBin("update_pass"), Exp.Val(5))))

};

ExecuteTask task = client.Execute(writePolicy, stmt,

    Operation.Add(new Bin("campaign1", 50)),

    Operation.Put(new Bin("update_pass", 5)));

task.Wait();
```

```javascript
const query = client.query("test", "donor");

query.where(Aerospike.filter.range("age", 30, 65));

// recordsPerSecond is not available for query operations

// in the Node.js client. Use the server config

// background-query-max-rps to limit throughput.

const writePolicy = new Aerospike.WritePolicy({

  filterExpression: exp.or(

    exp.not(exp.binExists("update_pass")),

    exp.lt(exp.binInt("update_pass"), exp.int(5))

  ),

});

const ops = [

  Aerospike.operations.incr("campaign1", 50),

  Aerospike.operations.write("update_pass", 5),

];

const job = await query.operate(ops, writePolicy);

await job.waitUntilDone();
```

## Related information

-   [Projection](https://aerospike.com/docs/develop/learn/queries/projection) for read-only query results (bin projection and, on Database 8.1.2+, operation projection).
-   [Secondary index capacity planning](https://aerospike.com/docs/database/manage/planning/capacity/secondary-indexes) describes how to plan and schedule index creation on production systems. Background index creation can take a substantial amount of resources because an index consumes RAM for every index entry.
-   See [Manage queries](https://aerospike.com/docs/database/manage/cluster/queries/).
-   Language-specific examples
    -   [Java](https://aerospike.com/docs/develop/client/java/usage/multi/queries/secondary/)
    -   [C# .NET](https://aerospike.com/docs/develop/client/csharp/usage/multi/queries/secondary/)
    -   [Node.js](https://aerospike.com/docs/develop/client/node/usage/multi/queries/secondary/)
    -   [Go](https://aerospike.com/docs/develop/client/go/usage/multi/queries/secondary/)
    -   [Python](https://aerospike.com/docs/develop/client/python/usage/multi/queries/secondary/)
    -   [C](https://aerospike.com/docs/develop/client/c/usage/multi/queries/secondary/)