Reports (aggregate)
Group, sum and average with a pipeline. The same JSON runs from every SDK and from curl.
When to use
You need "orders per hour" or "revenue per status" straight from the data, without an export.
Collection
Declared in examples/ndb.yml:
yaml
orders:
id: order_id
time: created_at
indexes: [status]Paid orders per hour
$match keeps paid orders. $group uses $dateTrunc to make one group per hour, with $count, $sum and $avg. $sort after the group puts the best hour first.
go
perHour, err := orders.Aggregate(ctx, ndb.Pipeline{
{"$match": map[string]any{"status": "paid"}},
{"$group": map[string]any{
"_id": map[string]any{"$dateTrunc": map[string]any{"date": "$created_at", "unit": "hour"}},
"orders": map[string]any{"$count": map[string]any{}},
"revenue": map[string]any{"$sum": "$amount"},
"avg_ticket": map[string]any{"$avg": "$amount"},
}},
ndb.SortStage(ndb.Desc("revenue")),
})
if err != nil {
return err
}
fmt.Println("paid orders per hour, best revenue first:")
for _, row := range perHour.Rows {
fmt.Printf(" %s orders=%v revenue=%v avg=%v\n", row["_id"], row["orders"], row["revenue"], row["avg_ticket"])
}rb
per_hour = ndb.aggregate("orders", pipeline: [
{ "$match" => { "status" => "paid" } },
{ "$group" => {
"_id" => { "$dateTrunc" => { "date" => "$created_at", "unit" => "hour" } },
"orders" => { "$count" => {} },
"revenue" => { "$sum" => "$amount" },
"avg_ticket" => { "$avg" => "$amount" }
} },
{ "$sort" => { "revenue" => -1 } }
])
puts "paid orders per hour, best revenue first:"
per_hour.rows.each do |row|
puts " #{row['_id']} orders=#{row['orders']} revenue=#{row['revenue']} avg=#{row['avg_ticket']}"
endts
const perHour = await orders.aggregate([
{ $match: { status: 'paid' } },
{
$group: {
_id: { $dateTrunc: { date: '$created_at', unit: 'hour' } },
orders: { $count: {} },
revenue: { $sum: '$amount' },
avg_ticket: { $avg: '$amount' },
},
},
{ $sort: { revenue: -1 } },
])
console.log('paid orders per hour, best revenue first:')
for (const row of perHour.rows) {
console.log(` ${row._id} orders=${row.orders} revenue=${row.revenue} avg=${row.avg_ticket}`)
}sh
echo "paid orders per hour, best revenue first:"
ndb POST /v1/c/orders/aggregate '{"pipeline":[
{"$match":{"status":"paid"}},
{"$group":{
"_id":{"$dateTrunc":{"date":"$created_at","unit":"hour"}},
"orders":{"$count":{}},
"revenue":{"$sum":"$amount"},
"avg_ticket":{"$avg":"$amount"}
}},
{"$sort":{"revenue":-1}}
]}' | jq -r '" \(._id) orders=\(.orders) revenue=\(.revenue) avg=\(.avg_ticket)"'Group by a field
A group key can be a plain field path.
go
byStatus, err := orders.Aggregate(ctx, ndb.Pipeline{
{"$group": map[string]any{
"_id": "$status",
"orders": map[string]any{"$count": map[string]any{}},
"revenue": map[string]any{"$sum": "$amount"},
}},
ndb.SortStage(ndb.Asc("_id")),
})
if err != nil {
return err
}
fmt.Println("by status:")
for _, row := range byStatus.Rows {
fmt.Printf(" %-8s orders=%v revenue=%v\n", row["_id"], row["orders"], row["revenue"])
}rb
by_status = ndb.aggregate("orders", pipeline: [
{ "$group" => {
"_id" => "$status",
"orders" => { "$count" => {} },
"revenue" => { "$sum" => "$amount" }
} },
{ "$sort" => { "_id" => 1 } }
])
puts "by status:"
by_status.rows.each do |row|
puts format(" %-8s orders=%s revenue=%s", row["_id"], row["orders"], row["revenue"])
endts
const byStatus = await orders.aggregate([
{ $group: { _id: '$status', orders: { $count: {} }, revenue: { $sum: '$amount' } } },
{ $sort: { _id: 1 } },
])
console.log('by status:')
for (const row of byStatus.rows) {
console.log(` ${String(row._id).padEnd(8)} orders=${row.orders} revenue=${row.revenue}`)
}sh
echo "by status:"
ndb POST /v1/c/orders/aggregate '{"pipeline":[
{"$group":{"_id":"$status","orders":{"$count":{}},"revenue":{"$sum":"$amount"}}},
{"$sort":{"_id":1}}
]}' | jq -r '" \(._id + " " | .[0:8]) orders=\(.orders) revenue=\(.revenue)"'Count with a filter
count: true returns the number of matches in the X-Ndb-Total header, whatever the page size.
go
big, err := orders.Where(ndb.Eq("status", "paid"), ndb.Gte("amount", 100)).Count(ctx)
if err != nil {
return err
}
fmt.Printf("paid orders of 100 or more: %d\n", big)rb
big = ndb.find("orders", where: { status: "paid", amount: { "$gte" => 100 } }, count: true, limit: 1).total
puts "paid orders of 100 or more: #{big}"ts
const big = await orders.where({ status: 'paid', amount: { $gte: 100 } }).count()
console.log(`paid orders of 100 or more: ${big}`)sh
big=$(ndb_header X-Ndb-Total POST /v1/c/orders/find '{"where":{"status":"paid","amount":{"$gte":100}},"count":true,"limit":1}')
echo "paid orders of 100 or more: $big"Output
The four programs print the same text.
text
orders: 12
paid orders per hour, best revenue first:
2026-11-01T10:00:00Z orders=3 revenue=360 avg=120
2026-11-01T11:00:00Z orders=3 revenue=210 avg=70
2026-11-01T09:00:00Z orders=2 revenue=120 avg=60
by status:
paid orders=8 revenue=690
pending orders=2 revenue=80
refunded orders=2 revenue=120
paid orders of 100 or more: 3Notes
- The first run inserts 12 fixed orders. A later run finds them and does not insert again.
- A pipeline has exactly one
$group. See Aggregate. - Go returns rows as maps (
AggregateAsgives typed rows). Ruby and TypeScript return plain hashes and objects. - Every SDK supports every step of this example.
ndb-railsalso hasgroup(:status).countstyle calls.
Run it
sh
cd examples
make run-reports LANG=go # go, ruby, ts or curl
make run-reports # every languageSource: examples/reports. The programs read NDB_URL and NDB_TOKEN, and they can run again and again.