Install

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]

$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']}"
end
ts
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"])
end
ts
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: 3

Notes ​

  • 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 (AggregateAs gives typed rows). Ruby and TypeScript return plain hashes and objects.
  • Every SDK supports every step of this example. ndb-rails also has group(:status).count style calls.

Run it ​

sh
cd examples
make run-reports LANG=go   # go, ruby, ts or curl
make run-reports           # every language

Source: examples/reports. The programs read NDB_URL and NDB_TOKEN, and they can run again and again.