Projection
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.
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.
// Phase 1: Query returns only keys (no bin data)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));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)// 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));// 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)}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);// Phase 1: Query returns only keys (no bin data)as_query q;as_query_init(&q, "test", "reports");q.no_bins = true;
// Collect keys in callback (simplified)aerospike_query_foreach(&as, &err, NULL, &q, collect_keys_cb, &keys_vec);as_query_destroy(&q);
// Phase 2: Batch operate to get city and state from report mapas_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);After (single query with projection ops): One query returns the projected data.
stmt.setOperations( 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();}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)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;}recordSet.Close();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);}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);Example: Read expression in projection
Use a read expression to compute a value and return it in a computed bin:
Expression readExp = Exp.build( Exp.sub(Exp.intBin("posted"), Exp.intBin("occurred")));
stmt.setOperations( ExpOperation.read("daysToPost", readExp, ExpReadFlags.DEFAULT));
RecordSet recordSet = client.query(null, stmt);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"])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);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);}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);