Как ускорить Prisma до производительности ванильного SQL

Как приблизиться к производительности «чистого SQL», сохранив удобство разработки и типизацию Prisma.

Обложка: Как ускорить Prisma до производительности ванильного SQL


Движок Prisma добавляет накладные издержки примерно в 2–7 раз даже в версии 7. В этом руководстве показано, как приблизиться к производительности «чистого SQL», сохранив удобство разработки и типизацию Prisma.

Проблема: почему Prisma медленнее «чистого SQL»

Prisma — удобная ORM, но каждый запрос проходит через несколько уровней обработки:

			Ваш код
    ↓
Prisma Client (TypeScript)
    ↓
Query Engine (бинарник на Rust)
    ↓
Генерация SQL
    ↓
Валидация запроса
    ↓
Сериализация результата
    ↓
Преобразование типов
    ↓
База данных

		

Каждый уровень добавляет задержку. Улучшения в Prisma v7 не устраняют фундаментальные ограничения этой архитектуры.

Практический эффект:

			// Prisma v7: ~0.34 ms накладных издержек
const users = await prisma.user.findMany({
  where: { status: 'ACTIVE' }
})

// Чистый SQL: ~0.17 ms накладных издержек
const users = await sql`
  SELECT * FROM users WHERE status = 'ACTIVE'
`

		

Для высоконагруженных API, которые обрабатывают тысячи запросов в секунду, разница в 0.17 ms на запрос быстро превращается в заметную прибавку к общей задержке.

Решение: выполнять SQL напрямую

Единственный способ добиться производительности «чистого SQL» — обойти Query Engine Prisma, но при этом сохранить привычный API Prisma клиента.

Сравнение архитектур

Обычная Prisma:

			prisma.user.findMany()
  → Query Engine (Rust)
  → Генерация SQL
  → База данных
  → Обработка результата
  → Возврат

		

Оптимизированный подход:

			prisma.user.findMany()
  → Генерация SQL (TypeScript)
  → postgres.js / better-sqlite3
  → База данных
  → Возврат (в том же формате)

		

Если генерировать SQL прямо из JSON-запроса и выполнять его через проверенные драйверы, накладные расходы Query Engine исчезают. Процесс подготовки становится обычным превращением JSON в строку.

Реализация

1) Установка зависимостей

PostgreSQL:

			npm install prisma-sql postgres

		

SQLite:

			npm install prisma-sql better-sqlite3

		

2) Базовая настройка (runtime-режим)

PostgreSQL:

			import { PrismaClient, Prisma } from '@prisma/client'
import { speedExtension, convertDMMFToModels } from 'prisma-sql'
import postgres from 'postgres'

// Конвертируем схему Prisma во внутренний формат (один раз)
const models = convertDMMFToModels(Prisma.dmmf.datamodel)

// Создаём postgres.js клиент
const sql = postgres(process.env.DATABASE_URL)

// Расширяем Prisma клиент
const prisma = new PrismaClient().$extends(
  speedExtension({
    postgres: sql,
    models
  })
)

// Используем как обычно — чтение станет быстрее в 2–7 раз
const users = await prisma.user.findMany({
  where: { status: 'ACTIVE' },
  include: { posts: true }
})

		

SQLite:

			import { PrismaClient, Prisma } from '@prisma/client'
import { speedExtension, convertDMMFToModels } from 'prisma-sql'
import Database from 'better-sqlite3'

const models = convertDMMFToModels(Prisma.dmmf.datamodel)
const db = new Database('./data.db')

const prisma = new PrismaClient().$extends(
  speedExtension({
    sqlite: db,
    models
  })
)

const users = await prisma.user.findMany({
  where: { status: 'ACTIVE' }
})

		

Готово. Код запросов менять не нужно. Все операции чтения теперь выполняются быстрее.

3) Продвинутый режим: generator (предварительная генерация SQL)

Для максимальной производительности можно «запечь» самые частые запросы на этапе сборки.

Добавьте в schema.prisma:

			generator sql {
  provider = "prisma-sql-generator"
}

/// @optimize {
///   "method": "findMany",
///   "query": {
///     "where": { "status": "ACTIVE" },
///     "orderBy": { "createdAt": "desc" },
///     "take": "$take",
///     "skip": "$skip"
///   }
/// }
/// @optimize {
///   "method": "count",
///   "query": { "where": { "status": "ACTIVE" } }
/// }
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  status    String
  createdAt DateTime @default(now())
  posts     Post[]
}

		

Сгенерируйте код:

			npx prisma generate

		

Подключите сгенерированное расширение:

			import { PrismaClient } from '@prisma/client'
import { createExtension } from './generated/sql'
import postgres from 'postgres'

const sql = postgres(process.env.DATABASE_URL)
const prisma = new PrismaClient().$extends(
  createExtension({ postgres: sql })
)

// ⚡ использует заранее созданный SQL (~0.03 ms накладных издержек)
const activeUsers = await prisma.user.findMany({
  where: { status: 'ACTIVE' },
  orderBy: { createdAt: 'desc' },
  take: 10,
  skip: 0
})

// 🔧 RUNTIME: генерирует SQL «на лету» (~0.2 ms накладных издержек)
const searchUsers = await prisma.user.findMany({
  where: { email: { contains: '@example.com' } }
})

		

Сравнение накладных издержек:

  • Prisma v7: ~0.34 ms
  • Runtime-режим: ~0.20 ms (примерно в 1.7 раза быстрее)
  • Generator-режим: ~0.03 ms (примерно в 11 раз быстрее)

Результаты бенчмарков

Тесты на 137 E2E-запросах, сравнивающих идентичные операции.

PostgreSQL

В среднем: в 2.10 раза быстрее, чем Prisma v7

SQLite

В среднем: в 5.48 раза быстрее, чем Prisma v7

Конфигурация для production

1) Режим отладки

Можно видеть сгенерированный SQL для каждого запроса:

			const prisma = new PrismaClient().$extends(
  speedExtension({
    postgres: sql,
    models,
    debug: true // Логирует SQL для отладки
  })
)

// Пример вывода:
// [postgres] User.findMany
// SQL: SELECT * FROM users WHERE status = $1
// Params: ['ACTIVE']

		

2) Мониторинг производительности

Можно отслеживать время выполнения запросов в production:

			const slowQueries: Array<{ query: string; duration: number }> = []

const prisma = new PrismaClient().$extends(
  speedExtension({
    postgres: sql,
    models,
    onQuery: (info) => {
      // Логируем медленные запросы
      if (info.duration > 100) {
        slowQueries.push({
          query: `${info.model}.${info.method}`,
          duration: info.duration
        })

        // Критически медленный запрос
        if (info.duration > 500) {
          logger.error('Критически медленный запрос', {
            model: info.model,
            method: info.method,
            sql: info.sql,
            duration: info.duration
          })
        }
      }

      // Метрики
      metrics.histogram('db.query.duration', info.duration, {
        model: info.model,
        method: info.method,
        prebaked: info.prebaked
      })
    }
  })
)

		

3) Выборочная оптимизация

Можно ускорять только «горячие» модели:

			const prisma = new PrismaClient().$extends(
  speedExtension({
    postgres: sql,
    models,
    allowedModels: ['User', 'Post', 'Order'] // Только горячие пути
  })
)

// Быстро (оптимизировано)
await prisma.user.findMany()
await prisma.post.findMany()

// Обычная Prisma (откат к стандартному поведению)
await prisma.auditLog.findMany()

		

4) Настройка пула соединений

Пример конфигурации postgres.js для production:

			const sql = postgres(process.env.DATABASE_URL, {
  max: 20,              // Размер пула соединений
  idle_timeout: 20,     // Закрывать idle-соединения через 20 секунд
  connect_timeout: 10,  // Быстро падать при проблемах с подключением
  prepare: true,        // Использовать prepared statements

  transform: {
    undefined: null     // Преобразовывать undefined в NULL
  },

  ssl: process.env.NODE_ENV === 'production'
    ? { rejectUnauthorized: false }
    : undefined
})

		

Поддерживаемые возможности

✅ Что ускоряется (выполняется через «чистый SQL»)

Запросы:

  • findMany, findFirst, findUnique
  • count
  • aggregate (_count, _sum, _avg, _min, _max)
  • groupBy с условиями having

Фильтры:

			// Сравнения
{ age: { gt: 18, lte: 65 } }
{ status: { in: ['ACTIVE', 'PENDING'] } }
{ status: { not: 'DELETED' } }

// Строковые операции
{ email: { contains: '@example.com' } }
{ email: { startsWith: 'user' } }
{ email: { endsWith: '.com' } }
{ name: { contains: 'John', mode: 'insensitive' } }

// Булева логика
{ AND: [{ status: 'ACTIVE' }, { verified: true }] }
{ OR: [{ role: 'ADMIN' }, { role: 'MODERATOR' }] }
{ NOT: { status: 'DELETED' } }

// Проверки на null
{ deletedAt: null }
{ deletedAt: { not: null } }

		

Связи:

			// include
{
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' },
      take: 5
    },
    profile: true
  }
}

// Вложенные include
{
  include: {
    posts: {
      include: {
        comments: {
          include: { author: true }
        }
      }
    }
  }
}

// Фильтры по связям
{
  where: {
    posts: { some: { published: true } },
    comments: { none: { flagged: true } }
  }
}

		

Пагинация и сортировка:

			// Offset-пагинация
{ take: 10, skip: 20, orderBy: { createdAt: 'desc' } }

// Курсорная пагинация
{
  cursor: { id: 100 },
  take: 10,
  skip: 1,
  orderBy: { id: 'asc' }
}

// Мультисортировка
{
  orderBy: [
    { status: 'asc' },
    { priority: 'desc' },
    { createdAt: 'desc' }
  ]
}

		

❌ Что остаётся на стандартной Prisma (без изменений)

Запись:

  • create, update, delete, upsert
  • createMany, updateMany, deleteMany

Продвинутые возможности:

  • Транзакции ($transaction)
  • Сырые запросы ($queryRaw, $executeRaw)
  • Middleware
  • Другие расширения Prisma Client

Поддержка Edge Runtime

Vercel Edge Functions

			import { PrismaClient, Prisma } from '@prisma/client'
import { speedExtension, convertDMMFToModels } from 'prisma-sql'
import postgres from 'postgres'

const models = convertDMMFToModels(Prisma.dmmf.datamodel)
const sql = postgres(process.env.DATABASE_URL)

const prisma = new PrismaClient().$extends(
  speedExtension({ postgres: sql, models })
)

export const config = { runtime: 'edge' }

export default async function handler(req: Request) {
  const users = await prisma.user.findMany({
    where: { status: 'ACTIVE' }
  })

  return Response.json(users)
}

		

Cloudflare Workers

			import { createToSQL, convertDMMFToModels } from 'prisma-sql'
import { Prisma } from '@prisma/client'

const models = convertDMMFToModels(Prisma.dmmf.datamodel)
const toSQL = createToSQL(models, 'sqlite')

export default {
  async fetch(request: Request, env: Env) {
    // Генерируем SQL
    const { sql, params } = toSQL('User', 'findMany', {
      where: { status: 'ACTIVE' }
    })

    // Выполняем через D1
    const result = await env.DB.prepare(sql)
      .bind(...params)
      .all()

    return Response.json(result.results)
  }
}

		

Руководство по миграции

Со стандартной Prisma

Было:

			import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

const users = await prisma.user.findMany({
  where: { status: 'ACTIVE' },
  include: { posts: true }
})

		

Стало:

			import { PrismaClient, Prisma } from '@prisma/client'
import { speedExtension, convertDMMFToModels } from 'prisma-sql'
import postgres from 'postgres'

const models = convertDMMFToModels(Prisma.dmmf.datamodel)
const sql = postgres(process.env.DATABASE_URL)

const prisma = new PrismaClient().$extends(
  speedExtension({ postgres: sql, models })
)

// Тот же код — просто быстрее
const users = await prisma.user.findMany({
  where: { status: 'ACTIVE' },
  include: { posts: true }
})

		

С Drizzle ORM

Было:

			import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { users } from './schema'
import { eq } from 'drizzle-orm'

const sql = postgres(DATABASE_URL)
const db = drizzle(sql)

const activeUsers = await db
  .select()
  .from(users)
  .where(eq(users.status, 'ACTIVE'))

		

Стало:

			import { PrismaClient, Prisma } from '@prisma/client'
import { speedExtension, convertDMMFToModels } from 'prisma-sql'
import postgres from 'postgres'

const models = convertDMMFToModels(Prisma.dmmf.datamodel)
const sql = postgres(DATABASE_URL)

const prisma = new PrismaClient().$extends(
  speedExtension({ postgres: sql, models })
)

// Более читаемый код при сопоставимой производительности
const activeUsers = await prisma.user.findMany({
  where: { status: 'ACTIVE' }
})

		

Частые проблемы и решения

1)

			speedExtension requires models parameter
		

Проблема:

			// ❌ Неправильно
const prisma = new PrismaClient().$extends(
  speedExtension({ postgres: sql })
)

		

Решение:

			// ✅ Правильно
import { Prisma } from '@prisma/client'
import { convertDMMFToModels } from 'prisma-sql'

const models = convertDMMFToModels(Prisma.dmmf.datamodel)

const prisma = new PrismaClient().$extends(
  speedExtension({ postgres: sql, models })
)

		

2) Исчерпан пул соединений

Проблема:

			Error: Connection pool exhausted

		

Решение:

			const sql = postgres(DATABASE_URL, {
  max: 50, // Увеличьте значение (по умолчанию часто 10)
  idle_timeout: 20
})

		

3) Результаты отличаются от стандартной Prisma

Проблема:

			// Результаты Prisma и оптимизированной версии отличаются

		

Решение:

			// Включите отладку
const prisma = new PrismaClient().$extends(
  speedExtension({
    postgres: sql,
    models,
    debug: true
  })
)

// Сравните с логом Prisma
const originalPrisma = new PrismaClient({
  log: ['query']
})

		

Если результаты действительно расходятся, создайте issue: https://github.com/multipliedtwice/prisma-sql/issues

4) Производительность почти не выросла

Проблема: ожидалось ускорение в 2–7 раз, но прирост минимальный.

Возможные причины:

  1. Доминирует сеть: на удалённой БД задержка сети (10–50 ms) «затмевает» накладные издержки (0.2 ms)
  2. Очень простые запросы: findUnique по индексированному ID и так быстрый
  3. Нет индексов: запросы упираются в план выполнения БД

Что сделать:

			const prisma = new PrismaClient().$extends(
  speedExtension({
    postgres: sql,
    models,
    onQuery: (info) => {
      console.log({
        query: `${info.model}.${info.method}`,
        duration: info.duration,
        prebaked: info.prebaked
      })
    }
  })
)

		

Когда это не стоит применять

Оставьте стандартную Prisma, если:

  1. Преобладает запись: больше 50% операций — запись
  2. Простой CRUD: одна таблица, минимум связей
  3. Команда не готова: нет опыта с postgres.js / better-sqlite3
  4. Нужны неподдерживаемые функции Prisma: активное использование возможностей, которые здесь не покрыты

Этот подход подходит, когда:

  1. Преобладает чтение: более 70% операций — чтение
  2. Высокая пропускная способность: >1000 rps, где важны миллисекунды
  3. Сложные запросы: много связей, агрегаций, условий
  4. Оптимизация затрат: меньше задержка → меньше ресурсов

Заключение

Можно добиться 100% производительности «чистого SQL», не отказываясь от удобства Prisma.

Основные преимущества:

  • Ускорение чтения в 2–7 раз без переписывания запросов
  • Те же типы и тот же API Prisma Client
  • Подходит для production (по заявлению авторов) и покрыто E2E-тестами
  • Работает в существующих проектах Prisma

Быстрый старт:

  1. Установите prisma-sql и postgres/better-sqlite3
  2. Подключите расширение
  3. Задеплойте и измерьте эффект

Дальнейшая оптимизация:

  1. Подключите generator для «горячих» запросов
  2. Включите мониторинг через onQuery
  3. Настройте пул соединений

Ресурсы: