@@ -489,6 +489,74 @@ export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
489489The [ ` example/convex/photos.ts ` ] ( example/convex/photos.ts ) example uses a
490490trigger.
491491
492+ ### Optimizing Triggers with Batching
493+
494+ ** Recommended:** When using triggers, combine them with the batching API for
495+ optimal performance.
496+
497+ Without batching, each table write triggers a separate call to the aggregate
498+ component. If you insert 100 rows in a mutation, that's 100 individual calls to
499+ the aggregate component, each fetching and updating the B-tree separately.
500+
501+ With batching, all triggered aggregate operations are queued and sent as a
502+ single batch at the end of the mutation. This provides:
503+
504+ - ** Single component call** instead of N separate calls
505+ - ** Single tree fetch** instead of N fetches
506+ - ** Better write contention handling** - one atomic update instead of many
507+ - ** Significant performance improvement** - especially for bulk operations
508+
509+ Here's how to set it up:
510+
511+ ``` ts
512+ import { TableAggregate } from " @convex-dev/aggregate" ;
513+ import { Triggers } from " convex-helpers/server/triggers" ;
514+ import { customMutation } from " convex-helpers/server/customFunctions" ;
515+ import { mutation as rawMutation } from " ./_generated/server" ;
516+
517+ const aggregate = new TableAggregate <{
518+ Key: number ;
519+ DataModel: DataModel ;
520+ TableName: " leaderboard" ;
521+ }>(components .aggregate , {
522+ sortKey : (doc ) => - doc .score ,
523+ });
524+
525+ // Set up triggers
526+ const triggers = new Triggers <DataModel >();
527+ triggers .register (" leaderboard" , aggregate .trigger ());
528+
529+ // Create a custom mutation that enables buffering and flushes on success
530+ const mutation = customMutation (rawMutation , {
531+ args: {},
532+ input : async (ctx ) => {
533+ aggregate .startBuffering ();
534+ return {
535+ ctx: triggers .wrapDB (ctx ),
536+ args: {},
537+ onSuccess : async ({ ctx }) => {
538+ await aggregate .finishBuffering (ctx );
539+ },
540+ };
541+ },
542+ });
543+
544+ // Now use this mutation in your functions
545+ export const addScores = mutation ({
546+ args: { scores: v .array (v .object ({ name: v .string (), score: v .number () })) },
547+ handler : async (ctx , { scores }) => {
548+ // Each insert triggers an aggregate operation, but they're all batched!
549+ for (const { name, score } of scores ) {
550+ await ctx .db .insert (" leaderboard" , { name , score });
551+ }
552+ // The flush happens automatically in the onSuccess callback
553+ },
554+ });
555+ ```
556+
557+ See [ ` example/convex/batchedWrites.ts ` ] ( example/convex/batchedWrites.ts ) for a
558+ complete working example with performance comparisons.
559+
492560### Repair incorrect aggregates
493561
494562If some mutation or direct write in the Dashboard updated the source of truth
@@ -507,10 +575,10 @@ aggregates based on the diff of these two paginated data streams.
507575
508576## Performance Optimizations
509577
510- ### Batch Operations
578+ ### Batch Read Operations
511579
512580For improved performance when making multiple similar queries, the Aggregate
513- component provides batch versions of common operations:
581+ component provides batch versions of common read operations:
514582
515583- ` countBatch() ` - Count items for multiple bounds in a single call
516584- ` sumBatch() ` - Sum items for multiple bounds in a single call
@@ -547,6 +615,46 @@ The batch functions accept arrays of query parameters and return arrays of
547615results in the same order, making them drop-in replacements for multiple
548616individual calls while providing better performance characteristics.
549617
618+ ### Batch Write Operations
619+
620+ When making multiple write operations (inserts, deletes, or updates), you can
621+ use the batching API to queue operations and send them in a single call to the
622+ aggregate component. This is especially valuable when using triggers.
623+
624+ ** When to use batching:**
625+
626+ - Making multiple aggregate writes in a single mutation
627+ - Using triggers that automatically update aggregates on table changes
628+ - Bulk operations like importing data or backfilling
629+
630+ The batching API works by enabling buffering mode, queueing operations in
631+ memory, and flushing them all at once:
632+
633+ ``` ts
634+ // Enable buffering
635+ aggregate .startBuffering ();
636+
637+ // Queue operations (not sent yet)
638+ await aggregate .insert (ctx , { key: 1 , id: " a" });
639+ await aggregate .insert (ctx , { key: 2 , id: " b" });
640+ await aggregate .insert (ctx , { key: 3 , id: " c" });
641+
642+ // Flush all operations in a single batch and stop buffering
643+ await aggregate .finishBuffering (ctx );
644+ ```
645+
646+ ** Benefits of batching:**
647+
648+ - ** Single component call** - One mutation instead of N separate calls
649+ - ** Single tree fetch** - The B-tree is fetched once for all operations
650+ - ** Better write contention** - All operations processed atomically together
651+ - ** Reduced overhead** - Less network and serialization overhead
652+
653+ See the [ Optimizing Triggers with Batching] ( #optimizing-triggers-with-batching )
654+ section for the recommended pattern when using triggers, and see
655+ [ ` example/convex/batchedWrites.ts ` ] ( example/convex/batchedWrites.ts ) for
656+ complete examples.
657+
550658## Reactivity and Atomicity
551659
552660Like all Convex queries, aggregates are
0 commit comments