$batch and @optimize

Extras on top of Prisma's API: capabilities Prisma Client does not have, with the same types and code style.

$batch

$batch runs multiple independent read queries in one PostgreSQL round trip.

const dashboard = await prisma.$batch((batch) => ({
  totalUsers: batch.user.count(),
  activeUsers: batch.user.count({
    where: { status: 'ACTIVE' },
  }),
  recentProjects: batch.project.findMany({
    take: 5,
    orderBy: { createdAt: 'desc' },
    include: { organization: true },
  }),
  taskStats: batch.task.aggregate({
    _count: true,
    _avg: { estimatedHours: true },
    where: { status: 'IN_PROGRESS' },
  }),
}))

Rules

Do not await inside the callback.

// Incorrect
await prisma.$batch(async (batch) => ({
  users: await batch.user.findMany(),
}))

// Correct
await prisma.$batch((batch) => ({
  users: batch.user.findMany(),
}))

Best use cases

  • dashboards
  • analytics summaries
  • counts + page data
  • multiple independent aggregates
  • splitting unrelated heavy reads instead of one massive include tree
Limitations: PostgreSQL only. Queries are independent and not transactional. Use $transaction when you need transactional guarantees.

@optimize

For the hottest query shapes you can prebake SQL at generate time. Matching runtime shapes execute the baked SQL directly; non-matching shapes use normal runtime generation.

Basic prebaked query

/// @optimize {
///   "method": "findMany",
///   "query": {
///     "where": { "status": "ACTIVE" }
///   }
/// }
model User {
  id     Int    @id
  status String
}

Dynamic parameters

Values prefixed with $ become bind parameters at runtime.

/// @optimize {
///   "method": "findMany",
///   "query": {
///     "where": { "status": "$status" },
///     "skip": "$skip",
///     "take": "$take"
///   }
/// }

Nested include

/// @optimize {
///   "method": "findMany",
///   "query": {
///     "include": {
///       "posts": {
///         "where": { "published": true },
///         "orderBy": { "createdAt": "desc" },
///         "take": 5
///       }
///     }
///   }
/// }

@optimize is optional — it only removes the conversion overhead of repeated hot query shapes. Seegenerator configuration for the skipInvalid option if a directive should be skipped instead of failing generation.

Do you need @optimize? No. Runtime generation already covers every supported shape; prebaking is a micro-optimization for your hottest endpoints.