Secondary index queries
For the complete documentation index see: 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. Such secondary index (SI) queries run faster than equivalent primary index (PI) queries. This is a trade-off between (typically in-memory) index storage and performance.
The Java SDK and Python SDK tabs use Developer SDK AEL text where it can express the example; the other tabs build the same expression with their client’s Exp builder. See the AEL reference and Query records in the Developer SDK.
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 options as primary index queries (bin projection; operation projection on Database 8.1.2 and later).
Types of SI queries
There are four types of SI queries:
- Foreground - read-only, returns records to the client.
- 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 to modify records.
- Background ops - write-only, returns nothing to client.
For a foreground read query, the query optimizer selects the index by reading the AEL the query carries. A query that carries no AEL must name its index with an explicit index filter, as background queries and aggregations always must:
- 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 comparisons against geospatial indexes.
A record filter expression 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). 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.
On Database 8.2.0 and later, a query with no readable matching index fails with AS_ERR_SINDEX_NOT_FOUND (201), because allowScansWithWhere is false by default. Set it to true to let the query read every record in the namespace or set instead. See Primary index fallback.
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:
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 an SI on a CDT 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 (also known as itype):
- Default where a single value of the given ktype is expected.
- List where a list of values of the given ktype is expected.
- Map keys where the ktype-matching keys of a map CDT are indexed.
- Map values 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 an SI 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 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 an SI query to find zero or one record.
Limitations
- Aerospike supports up to 256 secondary indexes per namespace.
- String and blob/bytes index keys are limited to 2048 bytes. Values longer than 2048 bytes are silently excluded from the index on write. A filter whose literal is 2048 bytes or longer cannot use a secondary index. In AEL it is dropped from index selection and evaluated on each record instead, with no error. An explicit index filter carrying such a value is rejected with error code 4 (
AEROSPIKE_ERR_REQUEST_INVALID). See secondary index byte-size limits. - Migration-tolerant queries were introduced in Aerospike Database 6 and require fully compatible clients.
Foreground queries
Foreground queries are read-only. To use an SI query in your application:
-
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.
-
Insert/update records. These write operations will trigger synchronous indexing.
-
Construct a query in one of these ways:
- The query optimizer, which selects the index from the query’s AEL.
- Explicit SI filter predicate plus optional filter expression (required for background queries and legacy SI queries).
Execute the SI or PI query and process the records streaming back to the client. Use operation projection to project read operations or read expressions in the result.
Create an index
Create and manage secondary indexes with manage sindex in asadm or sindex-create through asinfo. Both accept the integer key type for integer bins in Database 8.2.0 and later. Use asadm for interactive work and asinfo for automation.
If you are creating a database administration application, you can also create a secondary index programmatically with the createIndex and dropIndex equivalent client API calls in each language client.
When you create an SI:
- 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 sindexoutput, these indexes displaynullin the Set column. - Prior to Database 6.1.0: the index covers only records not belonging to any set (the null set).
- Database 6.1.0 and later: the index covers all records in the namespace, regardless of set membership. In
Admin+> manage sindex create integer dnr-age-idx ns test set donor bin ageasinfo -v 'sindex-create:indexname=dnr-age-idx;namespace=test;set=donor;bin=age;type=integer;indextype=default'Admin+> show sindex~~~~~Secondary Indexes (2026-09-26 15:07:24 UTC)~~~~~ Index Name|Namespace| Set|Bin| Bin| Index|State | | | | Type| Type|dnr-age-idx|test |donor|age|integer|default|RWNumber of rows: 1Indexing a value embedded inside a list or map requires a CDT context describing the path to the element.
Admin+> manage sindex create integer dnr-zip-idx ns test set donor bin address ctx map_key(zip)
Admin+> show sindex~~~~~~~~~~~~~~~~~~Secondary Indexes (2026-09-26 15:07:24 UTC)~~~~~~~~~~~~~~~~~~ Index Name|Namespace| Set| Bin| Bin| Index|State| Context | | | | Type| Type| |dnr-zip-idx|test |donor|address|integer|default|RW |[map_key(<string#3>)]Number of rows: 1Alternatively, this same index can be created with the base64-encoded CDT context
Admin+> manage sindex create integer 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.
The result type of the expression decides which comparisons the index can serve. An expression that returns an integer serves both equality and range comparisons. An expression that returns a string or blob serves equality only, because string and blob keys are stored hashed and have no order to range over. If you need range queries, write the expression so it returns an integer.
Expression campaignTotal = Exp.build( Exp.add(Exp.intBin("campaign1"), Exp.intBin("campaign2"), Exp.intBin("campaign3")));System.out.println(campaignTotal.getBase64());// lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=from aerospike_sdk.exp import Exp
campaign_total = Exp.num_add([ Exp.int_bin("campaign1"), Exp.int_bin("campaign2"), Exp.int_bin("campaign3"),])print(campaign_total.base64())# lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=use aerospike::expressions::{int_bin, num_add};
let campaign_total = num_add(vec![ int_bin("campaign1".into()), int_bin("campaign2".into()), int_bin("campaign3".into()),]);println!("{}", campaign_total.base64()?);// lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=Expression campaignTotal = Exp.Build( Exp.Add(Exp.IntBin("campaign1"), Exp.IntBin("campaign2"), Exp.IntBin("campaign3")));Console.WriteLine(campaignTotal.GetBase64());// lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=// Requires: import as "github.com/aerospike/aerospike-client-go/v8"campaignTotal := as.ExpNumAdd( as.ExpIntBin("campaign1"), as.ExpIntBin("campaign2"), as.ExpIntBin("campaign3"),)
b64, _ := campaignTotal.Base64()fmt.Println(b64)// lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=const exp = Aerospike.exp;
const sumExp = exp.add( exp.binInt("campaign1"), exp.binInt("campaign2"), exp.binInt("campaign3"));
const b64 = client.expressionToBase64(sumExp);console.log(b64);// lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=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);Expression campaignTotal = Exp.build( Exp.add(Exp.intBin("campaign1"), Exp.intBin("campaign2"), Exp.intBin("campaign3")));System.out.println(campaignTotal.getBase64());// lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=from aerospike_helpers import expressions as exp
sum_expr = exp.Add( exp.IntBin("campaign1"), exp.IntBin("campaign2"), exp.IntBin("campaign3"),).compile()
b64 = client.get_expression_base64(sum_expr)print(b64)# lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=The base64-encoded expression is used to create the expression index
Admin+> manage sindex create integer dnr-sum-idx ns test set donor exp_base64 lBSTUQKpY2FtcGFpZ24xk1ECqWNhbXBhaWduMpNRAqljYW1wYWlnbjM=Use 'show sindex' to confirm dnr-sum-idx was created successfully.Alternatively, on Database 8.2.0 and later with Tools 13.1.0 (asadm 5.1.0) or later, you can create the same expression index directly from Aerospike Expression Language (AEL) source using asadm, without writing any client code. Pass the AEL text to ael, surrounded by single quotes:
Admin+> manage sindex create integer dnr-sum-ael-idx ns test set donor ael '$.campaign1:INT + $.campaign2:INT + $.campaign3:INT'Use 'show sindex' to confirm dnr-sum-ael-idx was created successfully.The :INT type pins are required here: with three unpinned bins in a + chain, and no literal to anchor the type, the compiler cannot tell whether + means numeric addition or string concatenation, and rejects the expression.
If the AEL text is awkward to quote in your shell or script, base64-encode it and pass it to ael_b64 instead. The following command creates the same index, where the encoded value is base64("$.campaign1:INT + $.campaign2:INT + $.campaign3:INT"):
Admin+> manage sindex create integer dnr-sum-ael-idx ns test set donor ael_b64 JC5jYW1wYWlnbjE6SU5UICsgJC5jYW1wYWlnbjI6SU5UICsgJC5jYW1wYWlnbjM6SU5UUse 'show sindex' to confirm dnr-sum-ael-idx was created successfully.The ael and ael_b64 modifiers require Tools 13.1.0 (asadm 5.1.0) or later.
After the SI creation command reaches one node in the cluster, the cluster propagates the request to all cluster nodes. Each node begins building the SI on its local data.
In asadm, show sindex describes all the existing secondary indexes assembled on the namespace and its sets. The Expression column shows the server’s human-readable rendering for indexes created with exp_base64. The AEL column shows the original AEL source for indexes created with ael or ael_b64, and requires Tools 13.1.0 (asadm 5.1.0) or later.
Admin+> show sindex~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Secondary Indexes (2026-09-24 22:51:37 UTC)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Index Name|Namespace| Set| Bin| Bin| Index|State| Context| Expression| AEL | | | | Type| Type| | | |dnr-age-idx |test |donor| age|integer|default|RW |-- |-- |--dnr-sum-ael-idx|test |donor| null|integer|default|RW |-- |-- |$.campaign1:INT + $.campaign2:INT + $.campaign3:INTdnr-sum-idx |test |donor| null|integer|default|RW |-- |add(bin_int("campaign1"), bin_int("campaign2"), bin_int("campaign3"))|--dnr-zip-idx |test |donor|address|integer|default|RW |[map_key(<string#3>)]|-- |--Number of rows: 4Insert data
Insert data programmatically using one of the Aerospike client libraries.
The following examples insert a record into the donor set with this structure:
{ name: "Austin Albertson", age: 57, campaign1: 150, campaign2: 100, campaign3: 75, address: { state: "CA", city: "Los Altos", zip: 94023, }}DataSet donor = DataSet.of("test", "donor");
Map<String, Object> address = new HashMap<>();address.put("state", "CA");address.put("city", "Los Altos");address.put("zip", 94023);
session.upsert(donor.id("donor1")) .bin("name").setTo("Austin Albertson") .bin("age").setTo(57) .bin("campaign1").setTo(150) .bin("campaign2").setTo(100) .bin("campaign3").setTo(75) .bin("address").setTo(address) .execute();from aerospike_sdk import DataSet
donor = DataSet.of("test", "donor")
address = {"state": "CA", "city": "Los Altos", "zip": 94023}
session.upsert(donor.id("donor1")).put({ "name": "Austin Albertson", "age": 57, "campaign1": 150, "campaign2": 100, "campaign3": 75, "address": address,}).execute()use aerospike::{as_bin, as_map, as_val, Key, WritePolicy};
let key = Key::new("test", "donor", "donor1".into())?;
let address = as_map!("state" => "CA", "city" => "Los Altos", "zip" => 94023);
client.put(&WritePolicy::default(), &key, &[ as_bin!("name", "Austin Albertson"), as_bin!("age", 57), as_bin!("campaign1", 150), as_bin!("campaign2", 100), as_bin!("campaign3", 75), as_bin!("address", address),]).await?;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));// Requires: import as "github.com/aerospike/aerospike-client-go/v8"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),)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,});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);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));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)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
DataSet donor = DataSet.of("test", "donor");
RecordStream stream = session.query(donor) .where("$.age >= 18 and $.campaign1 > 200") .execute();from aerospike_sdk import DataSet
donor = DataSet.of("test", "donor")
stream = session.query(donor).where("$.age >= 18 and $.campaign1 > 200").execute()use aerospike::expressions::{gt, int_bin, int_val};use aerospike::query::Filter;use aerospike::{Bins, PartitionFilter, QueryPolicy, Statement};
let mut stmt = Statement::new("test", "donor", Bins::All);stmt.add_filter(Filter::range("age", 18, i64::MAX));
let mut qp = QueryPolicy::default();qp.base_policy.filter_expression = Some(gt(int_bin("campaign1".into()), int_val(200)));
let rs = client.query(&qp, PartitionFilter::all(), stmt).await?;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);// Requires: import as "github.com/aerospike/aerospike-client-go/v8"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)const exp = Aerospike.exp;
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);bool query_cb(const as_val* val, void* udata) { if (!val) return false; printf("%s\n", as_record_get_str(as_record_fromval(val), "name")); return true;}
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);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);from aerospike import predicates as pfrom aerospike_helpers import expressions as exp
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)In this case, you have an SI 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.
Admin+> manage sindex create integer ns-created-idx ns test bin createdUse show sindex to confirm ns-created-idx was created successfully.
Admin+> show sindex~~~~~~~~Secondary Indexes (2026-09-26 15:07:24 UTC)~~~~~~~~ Index Name|Namespace| Set| Bin| Bin| Index|State | | | | Type| Type|ns-created-idx|test |null|created|integer|default|RWNumber of rows: 1To 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
long now = System.currentTimeMillis();long oneWeekAgo = now - 7L * 24 * 60 * 60 * 1000;
DataSet wholeNamespace = DataSet.of("test", null);
RecordStream stream = session.query(wholeNamespace) .where("$.created >= " + oneWeekAgo + " and $.created <= " + now) .execute();import time
from aerospike_sdk import DataSet
now = int(time.time() * 1000)one_week_ago = now - 7 * 24 * 60 * 60 * 1000
whole_namespace = DataSet.of("test", None)
stream = ( session.query(whole_namespace) .where(f"$.created >= {one_week_ago} and $.created <= {now}") .execute())use aerospike::query::Filter;use aerospike::{Bins, PartitionFilter, QueryPolicy, Statement};use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as i64;let one_week_ago = now - 7 * 24 * 60 * 60 * 1000;
let mut stmt = Statement::new("test", "", Bins::All);stmt.add_filter(Filter::range("created", one_week_ago, now));
let rs = client.query(&QueryPolicy::default(), PartitionFilter::all(), stmt).await?;long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();long oneWeekAgo = now - 7L * 24 * 60 * 60 * 1000;
Statement stmt = new();stmt.SetNamespace("test");stmt.SetFilter(Filter.Range("created", oneWeekAgo, now));
RecordSet recordSet = client.Query(null, stmt);// Requires: import as "github.com/aerospike/aerospike-client-go/v8"now := time.Now().UnixMilli()oneWeekAgo := now - 7*24*60*60*1000
stmt := as.NewStatement("test", "")stmt.SetFilter(as.NewRangeFilter("created", oneWeekAgo, now))
rs, _ := client.Query(nil, stmt)const now = Date.now();const oneWeekAgo = now - 7 * 24 * 60 * 60 * 1000;
const query = client.query("test");query.where(Aerospike.filter.range("created", oneWeekAgo, now));
const stream = query.foreach();bool query_cb(const as_val* val, void* udata) { if (!val) return false; printf("%s\n", as_record_get_str(as_record_fromval(val), "name")); return true;}
int64_t now = (int64_t)time(NULL) * 1000;int64_t one_week_ago = now - 7LL * 24 * 60 * 60 * 1000;
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);long now = System.currentTimeMillis();long oneWeekAgo = now - 7L * 24 * 60 * 60 * 1000;
Statement stmt = new Statement();stmt.setNamespace("test");stmt.setFilter(Filter.range("created", oneWeekAgo, now));
RecordSet recordSet = client.query(null, stmt);import time
from aerospike import predicates as p
now = int(time.time() * 1000)one_week_ago = now - 7 * 24 * 60 * 60 * 1000
query = client.query("test")query.where(p.between("created", one_week_ago, now))
results = query.results()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
DataSet donor = DataSet.of("test", "donor");
RecordStream stream = session.query(donor) .where("$.address.zip == 94023 and $.age >= 18") .execute();from aerospike_sdk import DataSet
donor = DataSet.of("test", "donor")
stream = session.query(donor).where("$.address.zip == 94023 and $.age >= 18").execute()use aerospike::expressions::{ge, int_bin, int_val};use aerospike::operations::cdt_context::ctx_map_key;use aerospike::query::Filter;use aerospike::{Bins, PartitionFilter, QueryPolicy, Statement};
let mut stmt = Statement::new("test", "donor", Bins::All);stmt.add_filter(Filter::equal("address", 94023).context(vec![ctx_map_key("zip".into())]));
let mut qp = QueryPolicy::default();qp.base_policy.filter_expression = Some(ge(int_bin("age".into()), int_val(18)));
let rs = client.query(&qp, PartitionFilter::all(), stmt).await?;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);// Requires: import as "github.com/aerospike/aerospike-client-go/v8"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)const exp = Aerospike.exp;
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);bool query_cb(const as_val* val, void* udata) { if (!val) return false; printf("%s\n", as_record_get_str(as_record_fromval(val), "name")); return true;}
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);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);from aerospike import predicates as pfrom aerospike_helpers import cdt_ctxfrom aerospike_helpers import expressions as exp
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)You now have two potential secondary indexes to use. If the query carries AEL, the query optimizer chooses between them by entries_per_bval. Otherwise the application chooses.
The following manual comparison applies when the application chooses the index itself: on Database versions earlier than 8.2.0, or for a query the optimizer does not select for, such as a background query, an aggregation, or any query that carries no AEL. Using entries_per_bval in the Avg Per Bin Val column of asadm’s info sindex or the 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 SI.
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 |integer|Read-Write|1.000 |0.000 |0.000 |shmem|16.000 MB|-- |--dnr-sum-idx|test |donor|1.0.0.127:3000|null |integer|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|integer|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=0Query 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
DataSet donor = DataSet.of("test", "donor");
RecordStream stream = session.query(donor) .where("$.campaign1:INT + $.campaign2:INT + $.campaign3:INT >= 200" + " and $.campaign1:INT + $.campaign2:INT + $.campaign3:INT <= 2000" + " and $.age >= 18") .withHint(hint -> hint.forIndex("dnr-sum-idx")) .execute();from aerospike_sdk import DataSet, QueryHint
donor = DataSet.of("test", "donor")
stream = ( session.query(donor) .where( "$.campaign1:INT + $.campaign2:INT + $.campaign3:INT >= 200" " and $.campaign1:INT + $.campaign2:INT + $.campaign3:INT <= 2000" " and $.age >= 18" ) .with_hint(QueryHint(index_name="dnr-sum-idx")) .execute())use aerospike::expressions::{ge, int_bin, int_val};use aerospike::query::Filter;use aerospike::{Bins, PartitionFilter, QueryPolicy, Statement};
let mut stmt = Statement::new("test", "donor", Bins::All);stmt.add_filter(Filter::range_by_index("dnr-sum-idx", 200, 2000));
let mut qp = QueryPolicy::default();qp.base_policy.filter_expression = Some(ge(int_bin("age".into()), int_val(18)));
let rs = client.query(&qp, PartitionFilter::all(), stmt).await?;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);// Requires: import as "github.com/aerospike/aerospike-client-go/v8"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)const exp = Aerospike.exp;
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);bool query_cb(const as_val* val, void* udata) { if (!val) return false; printf("%s\n", as_record_get_str(as_record_fromval(val), "name")); return true;}
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);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);from aerospike import predicates as pfrom aerospike_helpers import expressions as exp
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)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
DataSet donor = DataSet.of("test", "donor");
RecordStream stream = session.query(donor) .where("$.campaign1:INT + $.campaign2:INT + $.campaign3:INT >= 200" + " and $.campaign1:INT + $.campaign2:INT + $.campaign3:INT <= 2000" + " and $.age >= 18") .execute();from aerospike_sdk import DataSet
donor = DataSet.of("test", "donor")
stream = ( session.query(donor) .where( "$.campaign1:INT + $.campaign2:INT + $.campaign3:INT >= 200" " and $.campaign1:INT + $.campaign2:INT + $.campaign3:INT <= 2000" " and $.age >= 18" ) .execute())use aerospike::expressions::{ge, int_bin, int_val, num_add};use aerospike::query::Filter;use aerospike::{Bins, PartitionFilter, QueryPolicy, Statement};
let sum_exp = num_add(vec![ int_bin("campaign1".into()), int_bin("campaign2".into()), int_bin("campaign3".into()),]);
let mut stmt = Statement::new("test", "donor", Bins::All);stmt.add_filter(Filter::range("", 200, 2000).expression(sum_exp));
let mut qp = QueryPolicy::default();qp.base_policy.filter_expression = Some(ge(int_bin("age".into()), int_val(18)));
let rs = client.query(&qp, PartitionFilter::all(), stmt).await?;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);// Requires: import as "github.com/aerospike/aerospike-client-go/v8"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)const exp = Aerospike.exp;
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);bool query_cb(const as_val* val, void* udata) { if (!val) return false; printf("%s\n", as_record_get_str(as_record_fromval(val), "name")); return true;}
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);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);from aerospike import predicates as pfrom aerospike_helpers import expressions as exp
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)You can get the expression used for an SI through asadm’s show sindex command, and get either the human-readable expression or its base64-encoded format using the sindex-list info command. For an index created from AEL (Database 8.2.0 or later), show sindex in Tools 13.1.0 (asadm 5.1.0) or later shows the AEL source itself in the AEL column, and the Expression column shows --.
Hint the query optimizer to use a specific index
Some secondary index queries can be answered by more than one secondary index. For example, a
query that filters on both age and country, where each bin has its own index,
could be answered by either one, and the optimizer picks one automatically based on its own selectivity
estimate. A QueryHint with index_name tells the optimizer which index to prefer instead.
bin_name is not a second hint shape: it bypasses index selection entirely and runs the query
against the primary index, and exists only to migrate code that relied on the older client-side
behavior. The two are mutually exclusive. A hint can also override the query’s expected duration
(query_duration). See Query optimizer.
This is the Developer SDK equivalent of naming an index explicitly with the legacy client’s Filter.rangeByIndex(...) / where_with_index_name(...) (see Query an expression index by index name), for use with where() predicates instead of a hand-built Statement/Filter.
import com.aerospike.client.sdk.DataSet;import com.aerospike.client.sdk.Record;import com.aerospike.client.sdk.RecordStream;
DataSet customers = DataSet.of("test", "customers");
RecordStream stream = session.query(customers) .where("$.age > 30 and $.country == 'USA'") .withHint(hint -> hint.forIndex("age_idx")) .execute();stream.forEach(result -> { Record rec = result.recordOrThrow(); System.out.printf("age=%d country=%s%n", rec.getInt("age"), rec.getString("country"));});stream.close();from aerospike_sdk import DataSet, QueryHint
customers = DataSet.of("test", "customers")
stream = ( session.query(customers) .where("$.age > 30 and $.country == 'USA'") .with_hint(QueryHint(index_name="age_idx")) .execute())for row in stream: rec = row.record_or_raise() print(f"age={rec.bins['age']} country={rec.bins['country']}")stream.close()📖 API reference:
QueryBuilder.with_hint()|QueryHint
Background queries
Background queries modify records in place on the server. The example below uses the dnr-age-idx SI 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, because 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
from aerospike_sdk import DataSet, Filter
donor = DataSet.of("test", "donor")
task = ( session.background_task() .update(donor) .index_filters(Filter.range("age", 30, 65)) .where("not($.update_pass.exists()) or $.update_pass < 5") .bin("campaign1").add(50) .bin("update_pass").set_to(5) .records_per_second(1000) .execute())task.wait_till_complete_blocking()use aerospike::expressions::{bin_exists, int_bin, int_val, lt, not, or};use aerospike::operations;use aerospike::query::Filter;use aerospike::{as_bin, Bins, Statement, Task, WritePolicy};
let mut stmt = Statement::new("test", "donor", Bins::All);stmt.add_filter(Filter::range("age", 30, 65));
let mut wp = WritePolicy::default();wp.records_per_second = 1000;wp.base_policy.filter_expression = Some(or(vec![ not(bin_exists("update_pass".into())), lt(int_bin("update_pass".into()), int_val(5)),]));
let task = client.query_operate(&wp, stmt, &[ operations::add(&as_bin!("campaign1", 50)), operations::put(&as_bin!("update_pass", 5)),]).await?;task.wait_till_complete(None).await?;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();// Requires: import as "github.com/aerospike/aerospike-client-go/v8"stmt := as.NewStatement("test", "donor")stmt.SetFilter(as.NewRangeFilter("age", 30, 65))
qp := as.NewQueryPolicy()qp.RecordsPerSecond = 1000qp.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()const exp = Aerospike.exp;
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();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);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();from aerospike import predicates as pfrom aerospike_helpers.operations import operationsfrom 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)Querying a masked bin
Data masking does not prevent you querying a set that contains masked bins. It prevents one thing: using a secondary index that is built on a masked bin. This applies to every client and every query type, including background and aggregation queries, since Database 8.1.1.
| The query | Role without read-masked |
|---|---|
| Uses no secondary index | Runs. Masked bins come back masked, as they do for a record read |
| Filters with an expression that reads a masked bin | Runs. The expression sees the masked value, not the original |
| Uses a secondary index built on a masked bin | Fails with AS_SEC_ERR_ROLE_VIOLATION (81) |
| Uses a secondary index built on a bin that is not masked | Runs, even if the records contain masked bins |
The same applies to an expression index whose definition references a masked bin.
A secondary index stores the original values, not the masked ones. Letting a query range over an index on a masked bin would let a caller recover a masked value by narrowing the predicate until rows appear, which is what masking exists to prevent. A filter expression cannot be used that way, because it only ever sees masked data.
Grant read-masked on the bin, or query through an index on a bin that is not masked.
Related information
- Query optimizer
- Secondary indexes (Developer SDK)
- Projection for read-only query results (bin projection and, on Database 8.1.2+, operation projection).
- Secondary index capacity planning 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.
- Language-specific examples