
В версии 1.58.0 библиотеки prisma-sql появился метод $batch, который позволяет выполнять несколько Prisma-запросов за один раунд-трип к базе данных.
Проблема
Типичный код для загрузки данных дашборда выглядит так:
const totalUsers = await prisma.user.count() const activeUsers = await prisma.user.count({ where: { status: 'ACTIVE' } }) const recentProjects = await prisma.project.findMany({ take: 5 }) const taskStats = await prisma.task.aggregate({ _count: true })
4 отдельных обращения к БД = 4 раунд-трипа.
Даже если каждый запрос выполняется за 1мс, накладные расходы на сетевые вызовы могут добавить 3-5мс. При 10+ запросах это становится заметно.
Метод $batch объединяет запросы в один SQL с использованием CTE (Common Table Expressions) или UNION (в зависимости от методов внутри батча):
const dashboard = await prisma.$batch(batch => ({ totalUsers: batch.user.count(), activeUsers: batch.user.count({ where: { status: 'ACTIVE' } }), recentProjects: batch.project.findMany({ take: 5 }), taskStats: batch.task.aggregate({ _count: true }) })) // Результат идентичен отдельным вызовам функций console.log(dashboard.totalUsers) // number console.log(dashboard.recentProjects) // Project[] console.log(dashboard.taskStats) // { _count: number }
1 обращение к БД вместо 4.
Как это работает
Библиотека генерирует единый SQL с использованием CTE:
WITH batch_0 AS (SELECT count(*)::int AS "_count._all" FROM "users"), batch_1 AS (SELECT count(*)::int AS "_count._all" FROM "users" WHERE status = $1), batch_2 AS (SELECT * FROM "projects" ORDER BY created_at DESC LIMIT 5), batch_3 AS (SELECT count(*)::int FROM "tasks") SELECT (SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_0 t) AS k0, (SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_1 t) AS k1, (SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_2 t) AS k2, (SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_3 t) AS k3
Каждый подзапрос выполняется параллельно внутри БД, результаты собираются в один JSON-ответ.
Специальная оптимизация для count
Когда батчите несколько count() по одной таблице, запросы автоматически оптимизируются с использованием FILTER:
const counts = await prisma.$batch(batch => ({ total: batch.user.count(), active: batch.user.count({ where: { status: 'ACTIVE' } }), pending: batch.user.count({ where: { status: 'PENDING' } }), inactive: batch.user.count({ where: { status: 'INACTIVE' } }) }))
Генерируется оптимальный SQL:
SELECT count(*) AS total, count(*) FILTER (WHERE status = $1) AS active, count(*) FILTER (WHERE status = $2) AS pending, count(*) FILTER (WHERE status = $3) AS inactive FROM users
Один проход по таблице вместо четырех.
Цифры производительности
Тесты на MacBook Pro M1, PostgreSQL 15:
Простые запросы (4 запроса):
Последовательно: 1.43мс (0.36мс на запрос)
Batch: 0.67мс (0.17мс на запрос)
Ускорение: 2.12x
Сложный дашборд (8 запросов с relations):
Последовательно: 9.90мс
Batch: 6.07мс
Ускорение: 1.63x
Стресс-тест (45 запросов):
Построение SQL: 1.48мс (0.03мс на запрос)
Выполнение: 3.13мс
Парсинг: 0.09мс
Итого: 4.71мс на 45 запросов
Установка и использование
npm install prisma-sql postgres
В schema.prisma:
generator client { provider = "prisma-client" } generator sql { provider = "prisma-sql-generator" }
Генерация:
npx prisma generate
Подключение:
import { PrismaClient } from '@prisma/client' import { speedExtension, type SpeedClient } from './generated/sql' import postgres from 'postgres' const sql = postgres(process.env.DATABASE_URL) const basePrisma = new PrismaClient() export const prisma = basePrisma.$extends( speedExtension({ postgres: sql }) ) as SpeedClient<typeof basePrisma> // Готово! Теперь можно использовать $batch const results = await prisma.$batch(batch => ({ users: batch.user.findMany(), posts: batch.post.count() }))
Поддерживаемые методы
✅
findMany- выборка записей✅
findFirst- первая подходящая запись✅
findUnique- по уникальному полю✅
count- подсчет (с оптимизацией)✅
aggregate- агрегации✅
groupBy- группировка
Важно: не используйте await внутри batch
// ❌ Неправильно - выбросит ошибку await prisma.$batch(async batch => ({ users: await batch.user.findMany(), // Не await! posts: await batch.post.findMany() })) // ✅ Правильно - возвращаем запросы без await await prisma.$batch(batch => ({ users: batch.user.findMany(), posts: batch.post.findMany() }))
Типизация
TypeScript полностью понимает типы результатов:
const results = await prisma.$batch(batch => ({ users: batch.user.findMany({ select: { id: true, email: true } }), count: batch.post.count() })) results.users[0].email // string results.users[0].name // ❌ ошибка - поля нет в select results.count // number
Сценарии использования
Дашборды - загрузка всех данных одним запросом
Аналитика - множественные агрегации
Сравнения - данные за разные периоды
и тд.
Пример: реальный дашборд
const dashboard = await prisma.$batch(batch => ({ // Статистика организаций totalOrgs: batch.organization.count(), activeOrgs: batch.organization.count({ where: { status: 'ACTIVE' } }), // Статистика пользователей 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' } }), // Пользователи по ролям adminCount: batch.user.count({ where: { role: 'ADMIN' } }), userCount: batch.user.count({ where: { role: 'USER' } }) })) console.log(` Организации: ${dashboard.activeOrgs}/${dashboard.totalOrgs} Пользователи: ${dashboard.activeUsers}/${dashboard.totalUsers} Админы: ${dashboard.adminCount} Средние часы задачи: ${dashboard.taskStats._avg.estimatedHours} `)
Ограничения
Только PostgreSQL (поддержка SQLite в планах)
Запросы выполняются параллельно, не в транзакции
Каждый запрос независим (нельзя использовать результат одного в другом)
Для транзакционных гарантий используйте
$transaction
Отладка
Включите режим отладки для просмотра генерируемого SQL:
const prisma = basePrisma.$extends( speedExtension({ postgres: sql, debug: true // Выводит SQL в консоль }) ) as SpeedClient<typeof basePrisma>
Мониторинг производительности
Отслеживайте производительность через callback:
const prisma = basePrisma.$extends( speedExtension({ postgres: sql, onQuery: (info) => { console.log(`${info.model}.${info.method}: ${info.duration}мс`) if (info.duration > 100) { logger.warn('Медленный запрос', { model: info.model, method: info.method, sql: info.sql }) } } }) ) as SpeedClient<typeof basePrisma>
Заключение
Метод $batch позволяет:
Сократить количество раунд-трипов к БД
Ускорить загрузку дашбордов
Сохранить привычный Prisma API
Автоматически оптимизировать count-запросы
Библиотека prisma-sql также ускоряет обычные запросы в 2-7 раз благодаря выполнению через postgres.js вместо query engine Prisma.
Ссылки:
Теги: #postgresql #prisma #nodejs #typescript #optimization #database
