Projection
For the complete documentation index see: llms.txt
All documentation pages available in markdown.
Projection is choosing which data is returned from each matching record, analogous to choosing columns in a relational database using SELECT.
Aerospike distinguishes two types of projection:
- Bin projection — Return values from one or more named bins (a vertical subset of the record’s bin data).
- Operation projection — Return the results of read-only bin operations, including simple bin reads, path expressions, and other read operations described under operation expressions. This supports nested paths, computed values, and lightweight server-side shaping without an extra round trip.
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.
Uniform API across commands
Single-record operate and batched operate have always supported both bin projection and operation projection in one command: you specify an ordered list of read operations, which can be plain bin reads or richer read operations.
Prior to Aerospike Database 8.1.2, foreground queries supported only a list of bin names (bin projection). Starting with 8.1.2, queries also support operation projection, so the query result shape matches what you can request from operate on a single key or in a batch.
Specifying a list of bin names for projection remains supported. Performance of that form is equivalent to using corresponding bin read operations in the projection list.
Benefits
- Unified API: Queries behave like batched operations for projections, with a predictable interface across single-key, batch, and query commands.
- Path expressions in query projection: Use path expressions and other expression APIs in the projection, not just in the filter selection.
- Server-side computation: Offload lightweight transformations, arithmetic, and aggregations to the server; only requested bins or results are sent back.
- Lower latency: Avoid the two-phase query-then-batch pattern; one query returns the projected data.
Supported projection operations
Query projection accepts read-only operations:
- Bin read operations:
Operation.get(binName)or equivalent in your client. Equivalent to specifying bin names. - CDT read operations: Map and List read operations (e.g.,
MapOperation.getByKey,ListOperation.getByIndex). - Read expressions: Operation expressions that compute a value and return it in a computed bin via
ExpOperation.read().
Example: Replacing the two-phase pattern
Before (two-phase): Query with no bins to get keys, then operate batched commands to get projected data.
DataSet reports = DataSet.of("test", "reports");
// Phase 1: Query returns only keys (no bin data)List<Key> keysList = new ArrayList<>();try (RecordStream rs = session.query(reports).withNoBins().execute()) { rs.forEach(result -> keysList.add(result.getKey()));}
// Phase 2: Batch operate to get city and state from report maptry (RecordStream rs = session.query(keysList) .bin("report").onMapKeyList(List.of("city", "state")).getValues() .execute()) { rs.forEach(result -> System.out.println(result.recordOrThrow().getList("report")));}from aerospike_sdk import DataSet
reports = DataSet.of("test", "reports")
# Phase 1: Query returns only keys (no bin data)keys_list = [row.key for row in session.query(reports).with_no_bins().execute()]
# Phase 2: Batch operate to get city and state from report mapstream = ( session.query(keys_list) .bin("report").on_map_key_list(["city", "state"]).get_values() .execute())for row in stream: print(row.record_or_raise().bins["report"])stream.close()use aerospike::operations::maps;use aerospike::{BatchOperation, BatchPolicy, BatchReadPolicy, Bins, MapReturnType, PartitionFilter, QueryPolicy, Statement, Value};use futures::StreamExt;
// Phase 1: Query returns only keys (no bin data)let stmt = Statement::new("test", "reports", Bins::None);let rs = client.query(&QueryPolicy::default(), PartitionFilter::all(), stmt).await?;let mut stream = rs.into_stream();let mut keys = Vec::new();while let Some(result) = stream.next().await { keys.push(result?.key.unwrap());}
// Phase 2: Batch operate to get city and state from report maplet brp = BatchReadPolicy::default();let mut batch: Vec<BatchOperation> = keys.into_iter().map(|key| { BatchOperation::read_ops(&brp, key, vec![ maps::get_by_key_list("report", vec![Value::from("city"), Value::from("state")], MapReturnType::Value), ])}).collect();client.batch(&BatchPolicy::default(), &mut batch).await?;for op in &batch { if let Some(rec) = op.record() { println!("{}", rec.bins["report"]); }}Statement stmt = new();stmt.SetNamespace("test");stmt.SetSetName("reports");
// Phase 1: Query returns only keys (no bin data)QueryPolicy queryPolicy = new() { includeBinData = false };RecordSet recordSet = client.Query(queryPolicy, stmt);List<Key> keysList = new();while (recordSet.Next()){ keysList.Add(recordSet.Key);}recordSet.Close();
// Phase 2: Batch operate to get city and state from report mapBatchResults batchResult = client.Operate(null, null, keysList.ToArray(), MapOperation.GetByKeyList("report", new List<Value> { Value.Get("city"), Value.Get("state") }, MapReturnType.VALUE));foreach (BatchRecord br in batchResult.records){ Console.WriteLine(string.Join(", ", br.record.GetList("report").Cast<object>()));}// Requires: import as "github.com/aerospike/aerospike-client-go/v8"// Phase 1: Query returns only keys (no bin data)stmt := as.NewStatement("test", "reports")qp := as.NewQueryPolicy()qp.IncludeBinData = false
rs, err := client.Query(qp, stmt)if err != nil { log.Fatal(err)}
var batchRecords []as.BatchRecordIfcfor res := range rs.Results() { if res.Err != nil { log.Fatal(res.Err) } batchRecords = append(batchRecords, as.NewBatchReadOps(nil, res.Record.Key, as.MapGetByKeyListOp("report", []any{"city", "state"}, as.MapReturnType.VALUE)))}
// Phase 2: Batch operate to get city and state from report maperr = client.BatchOperate(nil, batchRecords)if err != nil { log.Fatal(err)}for _, br := range batchRecords { fmt.Println(br.BatchRec().Record.Bins["report"])}const Aerospike = await import("aerospike");const maps = Aerospike.maps;
// Phase 1: Query returns only keys (no bin data)const query = client.query("test", "reports");query.nobins = true;const records = await query.results();
// Phase 2: Batch operate to get city and state from report mapconst batchRecords = records.map((rec) => ({ type: Aerospike.batchType.BATCH_READ, key: rec.key, ops: [maps.getByKeyList("report", ["city", "state"], maps.returnType.VALUE)],}));const batchResults = await client.batchRead(batchRecords);for (const br of batchResults) { console.log(br.record.bins.report);}typedef struct { pthread_mutex_t lock; as_vector keys;} collected_keys;
// The client calls a query callback from several threads, so it takes a lock.bool collect_keys_cb(const as_val* val, void* udata) { if (!val) return false; as_record* rec = as_record_fromval(val); collected_keys* ck = (collected_keys*)udata; pthread_mutex_lock(&ck->lock); as_key_init_digest((as_key*)as_vector_reserve(&ck->keys), "test", "reports", rec->key.digest.value); pthread_mutex_unlock(&ck->lock); return true;}
bool batch_cb(const as_batch_result* results, uint32_t n, void* udata) { for (uint32_t i = 0; i < n; i++) { char* report = as_val_tostring(as_record_get(&results[i].record, "report")); printf("%s\n", report); free(report); } return true;}
// Phase 1: Query returns only keys (no bin data)collected_keys ck;pthread_mutex_init(&ck.lock, NULL);as_vector_init(&ck.keys, sizeof(as_key), 16);
as_query q;as_query_init(&q, "test", "reports");q.no_bins = true;aerospike_query_foreach(&as, &err, NULL, &q, collect_keys_cb, &ck);as_query_destroy(&q);
// Phase 2: Batch operate to get city and state from report mapas_batch batch;as_batch_inita(&batch, ck.keys.size);for (uint32_t i = 0; i < ck.keys.size; i++) { as_key* key = (as_key*)as_vector_get(&ck.keys, i); as_key_init_digest(as_batch_keyat(&batch, i), "test", "reports", key->digest.value);}
as_arraylist key_list;as_arraylist_init(&key_list, 2, 0);as_arraylist_append_str(&key_list, "city");as_arraylist_append_str(&key_list, "state");
as_operations ops;as_operations_inita(&ops, 1);as_operations_map_get_by_key_list( &ops, "report", NULL, (as_list*)&key_list, AS_MAP_RETURN_VALUE);
aerospike_batch_operate(&as, &err, NULL, NULL, &batch, &ops, batch_cb, NULL);as_operations_destroy(&ops);as_batch_destroy(&batch);as_vector_destroy(&ck.keys);Statement stmt = new Statement();stmt.setNamespace("test");stmt.setSetName("reports");
// Phase 1: Query returns only keys (no bin data)QueryPolicy queryPolicy = new QueryPolicy();queryPolicy.includeBinData = false;RecordSet recordSet = client.query(queryPolicy, stmt);List<Key> keysList = new ArrayList<>();while (recordSet.next()) { keysList.add(recordSet.getKey());}recordSet.close();
// Phase 2: Batch operate to get city and state from report mapKey[] keys = keysList.toArray(new Key[0]);BatchResults batchResult = client.operate(null, null, keys, MapOperation.getByKeyList("report", Arrays.asList(Value.get("city"), Value.get("state")), MapReturnType.VALUE));for (BatchRecord br : batchResult.records) { System.out.println(br.record.getList("report"));}from aerospike_helpers.operations import map_operations
# Phase 1: Query returns only keys (no bin data)query = client.query("test", "reports")keys_list = []for key, _, _ in query.results(options={"nobins": True}): keys_list.append(key)
# Phase 2: Batch operate to get city and state from report mapops = [ map_operations.map_get_by_key_list( "report", ["city", "state"], aerospike.MAP_RETURN_VALUE )]batch_results = client.batch_operate(keys_list, ops)for br in batch_results.batch_records: print(br.record[2]["report"])After (single query with projection ops): One query returns the projected data.
DataSet reports = DataSet.of("test", "reports");
try (RecordStream rs = session.query(reports) .bin("report").onMapKeyList(List.of("city", "state")).getValues() .execute()) { rs.forEach(result -> System.out.println(result.recordOrThrow().getList("report")));}from aerospike_sdk import DataSet
reports = DataSet.of("test", "reports")
stream = ( session.query(reports) .bin("report").on_map_key_list(["city", "state"]).get_values() .execute())for row in stream: print(row.record_or_raise().bins["report"])stream.close()use aerospike::operations::maps;use aerospike::{Bins, MapReturnType, PartitionFilter, QueryPolicy, Statement, Value};use futures::StreamExt;
let mut stmt = Statement::new("test", "reports", Bins::All);stmt.set_operations(vec![ maps::get_by_key_list("report", vec![Value::from("city"), Value::from("state")], MapReturnType::Value),]);
let rs = client.query(&QueryPolicy::default(), PartitionFilter::all(), stmt).await?;let mut stream = rs.into_stream();while let Some(result) = stream.next().await { println!("{}", result?.bins["report"]);}Statement stmt = new();stmt.SetNamespace("test");stmt.SetSetName("reports");stmt.Operations = new Operation[] { MapOperation.GetByKeyList("report", new List<Value> { Value.Get("city"), Value.Get("state") }, MapReturnType.VALUE)};RecordSet recordSet = client.Query(null, stmt);while (recordSet.Next()){ Record record = recordSet.Record; Console.WriteLine(string.Join(", ", record.GetList("report").Cast<object>()));}recordSet.Close();// Requires: import as "github.com/aerospike/aerospike-client-go/v8"stmt := as.NewStatement("test", "reports")stmt.Operations = []*as.Operation{ as.MapGetByKeyListOp("report", []any{"city", "state"}, as.MapReturnType.VALUE),}
rs, err := client.Query(nil, stmt)if err != nil { log.Fatal(err)}for res := range rs.Results() { if res.Err != nil { log.Fatal(res.Err) } fmt.Println(res.Record.Bins)}const Aerospike = await import("aerospike");const maps = Aerospike.maps;
const query = client.query("test", "reports");query.ops = [ maps.getByKeyList("report", ["city", "state"], maps.returnType.VALUE),];
const records = await query.results();for (const rec of records) { console.log(rec.bins);}bool query_cb(const as_val* val, void* udata) { if (!val) return false; char* report = as_val_tostring(as_record_get(as_record_fromval(val), "report")); printf("%s\n", report); free(report); return true;}
as_arraylist key_list;as_arraylist_init(&key_list, 2, 0);as_arraylist_append_str(&key_list, "city");as_arraylist_append_str(&key_list, "state");
as_operations ops;as_operations_inita(&ops, 1);as_operations_map_get_by_key_list( &ops, "report", NULL, (as_list*)&key_list, AS_MAP_RETURN_VALUE);
as_query q;as_query_init(&q, "test", "reports");q.ops = &ops;
aerospike_query_foreach(&as, &err, NULL, &q, query_cb, NULL);as_operations_destroy(&ops);as_query_destroy(&q);Statement stmt = new Statement();stmt.setNamespace("test");stmt.setSetName("reports");stmt.setOperations(new Operation[] { MapOperation.getByKeyList("report", Arrays.asList(Value.get("city"), Value.get("state")), MapReturnType.VALUE)});RecordSet recordSet = client.query(null, stmt);while (recordSet.next()) { Record record = recordSet.getRecord(); System.out.println(record.getList("report"));}recordSet.close();from aerospike_helpers.operations import map_operations
query = client.query("test", "reports")query.add_ops([ map_operations.map_get_by_key_list( "report", ["city", "state"], aerospike.MAP_RETURN_VALUE )])
for key, _, bins in query.results(): print(bins)Example: Read expression in projection
Use a read expression to compute a value and return it in a computed bin:
DataSet reports = DataSet.of("test", "reports");
RecordStream stream = session.query(reports) .bin("daysToPost").selectFrom("$.posted:INT - $.occurred:INT") .execute();stream.forEach(result -> { Record rec = result.recordOrThrow(); System.out.println(rec.getLong("daysToPost"));});stream.close();from aerospike_sdk import DataSet
reports = DataSet.of("test", "reports")
stream = ( session.query(reports) .bin("daysToPost").select_from("$.posted:INT - $.occurred:INT") .execute())for row in stream: rec = row.record_or_raise() print(rec.bins["daysToPost"])stream.close()use aerospike::expressions::{int_bin, num_sub};use aerospike::operations::exp::{read_exp, ExpReadFlags};use aerospike::{Bins, PartitionFilter, QueryPolicy, Statement};use futures::StreamExt;
let days_to_post = num_sub(vec![int_bin("posted".into()), int_bin("occurred".into())]);
let mut stmt = Statement::new("test", "reports", Bins::All);stmt.set_operations(vec![read_exp("daysToPost", days_to_post, ExpReadFlags::Default)]);
let rs = client.query(&QueryPolicy::default(), PartitionFilter::all(), stmt).await?;let mut stream = rs.into_stream();while let Some(result) = stream.next().await { println!("{}", result?.bins["daysToPost"]);}Statement stmt = new();stmt.SetNamespace("test");stmt.SetSetName("reports");
Expression readExp = Exp.Build( Exp.Sub(Exp.IntBin("posted"), Exp.IntBin("occurred")));
stmt.Operations = new Operation[] { ExpOperation.Read("daysToPost", readExp, ExpReadFlags.DEFAULT)};
RecordSet recordSet = client.Query(null, stmt);while (recordSet.Next()){ Console.WriteLine(recordSet.Record.GetLong("daysToPost"));}recordSet.Close();// Requires: import as "github.com/aerospike/aerospike-client-go/v8"readExp := as.ExpNumSub(as.ExpIntBin("posted"), as.ExpIntBin("occurred"))
stmt := as.NewStatement("test", "reports")stmt.Operations = []*as.Operation{ as.ExpReadOp("daysToPost", readExp, as.ExpReadFlagDefault),}
rs, err := client.Query(nil, stmt)if err != nil { log.Fatal(err)}for res := range rs.Results() { if res.Err != nil { log.Fatal(res.Err) } fmt.Println(res.Record.Bins["daysToPost"])}const Aerospike = await import("aerospike");const exp = Aerospike.exp;
const readExp = exp.sub(exp.binInt("posted"), exp.binInt("occurred"));
const query = client.query("test", "reports");query.ops = [ exp.operations.read("daysToPost", readExp, exp.expReadFlags.DEFAULT),];
const records = await query.results();for (const rec of records) { console.log(rec.bins.daysToPost);}bool query_cb(const as_val* val, void* udata) { if (!val) return false; as_record* rec = as_record_fromval(val); printf("%ld\n", (long)as_record_get_int64(rec, "daysToPost", 0)); return true;}
as_exp_build(read_exp, as_exp_sub(as_exp_bin_int("posted"), as_exp_bin_int("occurred")));
as_operations ops;as_operations_inita(&ops, 1);as_operations_exp_read(&ops, "daysToPost", read_exp, AS_EXP_READ_DEFAULT);
as_query q;as_query_init(&q, "test", "reports");q.ops = &ops;
aerospike_query_foreach(&as, &err, NULL, &q, query_cb, NULL);as_exp_destroy(read_exp);as_operations_destroy(&ops);as_query_destroy(&q);Statement stmt = new Statement();stmt.setNamespace("test");stmt.setSetName("reports");
Expression readExp = Exp.build( Exp.sub(Exp.intBin("posted"), Exp.intBin("occurred")));
stmt.setOperations(new Operation[] { ExpOperation.read("daysToPost", readExp, ExpReadFlags.DEFAULT)});
RecordSet recordSet = client.query(null, stmt);while (recordSet.next()) { System.out.println(recordSet.getRecord().getLong("daysToPost"));}recordSet.close();from aerospike_helpers.expressions.arithmetic import Subfrom aerospike_helpers.expressions import base as expfrom aerospike_helpers.operations import expression_operations as expr_ops
read_exp = Sub(exp.IntBin("posted"), exp.IntBin("occurred")).compile()
query = client.query("test", "reports")query.add_ops([ expr_ops.expression_read("daysToPost", read_exp)])
for key, _, bins in query.results(): print(bins["daysToPost"])