
Путь разработчика от «Prisma — это магия» через «почему это так медленно?» к решению, которое сохраняет и DX, и производительность.
Проблема, которую я не увидел заранее
Несколько месяцев назад наш API работал отлично. Мы выбрали Prisma для работы с базой данных — и это действительно ощущалось как магия. Типобезопасные запросы? Есть. Автогенерация типов? Есть. Миграции, которые реально работают? Тоже есть.
А потом начал расти трафик.
Эндпоинт /dashboard стал уходить в таймаут. Аналитические запросы начали «ползти». Время ответа выросло до нескольких секунд.
В чём была проблема? Каждая операция чтения из базы занимала в 2–3 раза больше времени, чем должна была.
Дилемма разработчика
Вот о чём редко говорят, когда обсуждают Prisma: это архитектурный компромисс, а не волшебная таблетка.
Когда ты пишешь:
const users = await prisma.user.findMany({
where: { status: 'ACTIVE' },
include: { posts: true }
})
Внутри происходит что-то такое:
TypeScript-запрос валидируется относительно схемы
Конвертируется во внутренний формат Prisma
Query Engine (TypeScript в v7, раньше Rust) обрабатывает его
Генерируется и выполняется SQL
Результаты возвращаются обратно через engine
Финально сериализуются в типизированные JavaScript-объекты
Это много слоёв.
Даже с TypeScript движком в v7 каждый шаг добавляет оверхэд. Движок обеспечивает безопасность и корректность, но каждая валидация и трансформация стоит времени. На простых запросах это незаметно. Но когда ты запускаешь сложную аналитику по 100k строкам или обслуживаешь 1000 запросов в минуту — эти миллисекунды превращаются в реальные проблемы.
Варианты, которые я рассмотрел (и отклонил)
Когда я поднял эту тему в команде, предложения были предсказуемыми.
Вариант 1: «Используй сырой SQL для медленных запросов»
Да, но тогда я теряю типобезопасность. Нужно поддерживать две разные системы запросов. Новым людям в команде придётся разбираться в обеих.
Вариант 2: «Переходи на Drizzle / Kysely / другую ORM»
Отличные инструменты, но это месяцы миграции. Переписывание сотен запросов, обновление типов, переобучение команды и риск привнести новые баги. На это просто не было ресурсов.
Вариант 3: «Агрессивное кеширование»
Мы попробовали. Redis помог, но инвалидация кеша стала отдельной головной болью. К тому же это не решает фундаментальную проблему — при cache miss запросы всё равно остаются медленными.
Землетрясение и идея
28 марта 2025 года мощное землетрясение магнитудой 7.7 произошло в Мьянме возле Мандалая. Погибли более 3000 человек, были серьёзные разрушения. Толчки дошли более чем на 1000 километров до Бангкока — мягкие почвы усилили сейсмические волны. Здания раскачивались, 33-этажная стройка обрушилась, погибли 95 человек, в городе ввели режим ЧС.
Я схватил ноутбук и уехал из города в Bang Kachao — зелёную зону через реку, где можно было спокойно думать без постоянных уведомлений о повторных толчках.
Проблема производительности Prisma не давала мне покоя неделями. Сидя под деревом с плохим интернетом, я начал формулировать идею: а что если генерировать SQL на этапе сборки?
Концепция была простой: во время prisma generate анализировать схему и создавать оптимизированные SQL-запросы для частых операций. Сохранять их в generated-папку. А в runtime просто выполнять готовый SQL напрямую.
Я начал писать код. Без встреч, без отвлечений — только я и проблема.
Чего я тогда не осознавал — насколько это окажется сложно. Построить корректный SQL-компилятор для ORM с 44.8k звёздами на GitHub, которой доверяют production-базы, оказалось гораздо тяжелее, чем я ожидал. Потребовался почти год разработки, огромное количество edge-case’ов и комплексное тестирование, прежде чем я был достаточно уверен, чтобы выпустить решение.
Первая (провальная) попытка
Изначально я попытался перехватывать debug-запросы Prisma.
Prisma умеет логировать генерируемый SQL:
const prisma = new PrismaClient({
log: [{ emit: 'event', level: 'query' }]
})
prisma.$on('query', (e) => {
console.log('Query: ' + e.query)
console.log('Params: ' + e.params)
})
Я подумал: «Если я могу получить эти SQL-строки, то смогу выполнять их напрямую через postgres.js и полностью обойти engine».
Это не сработало.
Возникли фундаментальные проблемы с параметрами и жёсткой привязкой к прегенерированным строкам. Стало понятно — это тупиковый путь.
Пришлось вернуться к проектированию.
Прорыв
Через несколько месяцев у меня появился рабочий прототип, который генерировал SQL на этапе сборки. Но когда я сделал бенчмарки, произошло неожиданное: даже runtime-генерация SQL (без прегенерации) оказалась заметно быстрее, чем прохождение через Prisma engine.
Прегенерация была полезной оптимизацией, но реальное узкое место оказалось не в генерации SQL — а во всех слоях engine.
Генерируя SQL напрямую из Prisma query format и выполняя его через postgres.js, я обходил весь overhead.
Ключевой инсайт: Prisma публикует полные метаданные схемы через DMMF (Data Model Meta Format). Используя эту информацию, я смог конвертировать Prisma-запросы напрямую в SQL, полностью минуя engine.
Построение решения
Следующие месяцы ушли на доводку:
Query builder, который конвертирует Prisma query format напрямую в оптимизированный SQL
Прямое выполнение через postgres.js (позже — better-sqlite3)
Prisma extension для бесшовного перехвата read-операций
Поддержка двух режимов: runtime-генерация SQL и build-time прегенерация
Цель была прагматичной: оставить Prisma там, где она сильна (миграции, схема, типы, записи), но выполнять read-запросы напрямую через нативный драйвер базы.
Я написал много тестов. 137 E2E-кейсов, покрывающих все паттерны запросов, которые смог придумать. Всё валидировалось против Prisma v6, v7 и Drizzle, чтобы гарантировать корректность.
Интеграция в одну строку
Вот что изменилось в коде.
Было:
const prisma = new PrismaClient()
Стало:
import { speedExtension, convertDMMFToModels } from 'prisma-sql'
import postgres from 'postgres'
import { Prisma } from '@prisma/client'
const sql = postgres(process.env.DATABASE_URL)
const models = convertDMMFToModels(Prisma.dmmf.datamodel)
const prisma = new PrismaClient().$extends(
speedExtension({ postgres: sql, models })
)
Вот и всё. Семь строк настройки. Без переписывания запросов. Без миграций.
Результаты
Результаты оказались впечатляющими.
Сложные агрегации:
Было: ~800ms
Стало: ~150ms
Ускорение: 5–7x
Вложенные отношения:
Было: ~230ms
Стало: ~90ms
Ускорение: 2.5x
Простые выборки:
Было: ~45ms
Стало: ~18ms
Ускорение: 2.5x
Count для пагинации:
Было: ~150ms
Стало: ~22ms
Ускорение: 6–7x
P95 response time упал с >1 секунды до <400ms. Это сразу стало видно в мониторинге.
Для PostgreSQL это типичные цифры. SQLite показал ещё более сильный эффект — в среднем 5.5x, а некоторые операции доходили до 7x.
cmyser
Факт этой оптимизации говорит о качестве продукта "Prisma"
После такого не хочется её использовать
thousandsofraccoons Автор
Не существует ни одной ORM, которая не добавляет слоев логики поверх движка. Однако, только Prisma позволяет такие оптимизации, так как строит DMMF-карту базы и имеет достаточно экспрессивные JSON-параметры в аргументах чтобы сделать вывод какой именно SQL пользователь ждет
cmyser
У постгри же и так хороший планировщик
Мне в этом плане очень нравится генератор sqlc. Мы пишем чистый sql, а генерируется шаблонный код запросов к базе, Мега удобно
Всегда актуальный код согласованный с миграциями выходит
thousandsofraccoons Автор
Это хорошо, а в serverless go умеет?)
cmyser
Да, умеет