25 нояб. 2024 г.·7 мин чтения

Prisma vs raw SQL, когда продуктовые правила становятся неудобными

Prisma vs raw SQL — это не проверка на верность идее. Узнайте, где ORM экономит время, где помогают собственные SQL-запросы и как выбирать под каждую задачу.

Prisma vs raw SQL, когда продуктовые правила становятся неудобными

Почему этот выбор быстро становится запутанным

В начале большинству продуктовых команд не приходится делать жесткий выбор для базы данных. Им нужны простые чтения, простые записи и способ выпускать экраны, не тратя половину недели на код запросов. ORM вроде Prisma хорошо подходит для этого этапа, потому что превращает обычную работу в обычный код приложения.

Трудности начинаются позже, и начинают они тихо. Команда добавляет одно небольшое правило в список клиентов. Потом еще одно — в биллинг. Потом еще одно исключение — для поддержки. Ни одно изменение само по себе не кажется страшным, но логика запроса растет экран за экраном, пока правила работы с данными уже не кажутся простыми.

Обычная страница может быстро стать неудобной. Например, продукт должен показывать активных клиентов, скрывать отмененные подписки, если поддержка не открыла обращение, сортировать по последнему оплаченному счету и добавлять предупреждение, если за месяц аккаунт превысил лимит использования. Это все еще одна страница. Никто не просил сложный отчет, но запрос уже смешивает фильтры, соединения, сортировку и особые случаи.

Отчеты обычно делают проблему очевидной. Они берут данные из нескольких таблиц, считают итоги, группируют результаты, применяют фильтры по датам и обрабатывают исключения, которые важны для бизнеса. Один стейкхолдер хочет исключить заказы с возвратом. Другой хочет учитывать их в отдельной колонке. Третий хочет оба варианта — в зависимости от типа аккаунта. Теперь запрос несет в себе продуктовые правила, а не просто доступ к данным.

Вот почему разговор о Prisma vs raw SQL быстро становится запутанным. Обычно первый вопрос — не «что быстрее?». Чаще всего первая боль — это читаемость. Код Prisma может выглядеть аккуратно для обычной CRUD-работы, а потом превратиться в глубокий объект с вложенными условиями, который трудно быстро прочитать. Raw SQL сначала кажется жестче, но для некоторых запросов он яснее показывает логику.

Конечно, скорость тоже важна. Но многие команды обращаются к raw SQL, потому что хотят контроля и запроса, который можно понять с первого взгляда. Когда продуктовые правила становятся неудобными, ясность тоже становится частью производительности. Запрос, который один инженер понимает за две минуты, часто лучше, чем хитрая абстракция, которую никто не хочет трогать.

Где Prisma помогает в повседневной работе

Для ежедневных продуктовых задач Prisma обычно экономит время на скучных вещах. Когда команда добавляет поле вроде trialEndsAt или billingStatus, изменение схемы находится рядом с кодом приложения, а не где-то в отдельной куче SQL-файлов и вручную написанных моделей. Так небольшие изменения продукта проще отслеживать в одном месте.

Самая заметная польза появляется в обычных сценариях создания и обновления. Форма, которая создает клиента, сохраняет несколько связанных записей и возвращает результат, с Prisma часто требует намного меньше кода, чем при ручных запросах. Вы тратите меньше времени на сопоставление столбцов, меньше времени на проверку null и меньше времени на исправление мелких ошибок в названиях.

Типы помогают сильнее, чем многие ожидают. Если кто-то меняет companyId на workspaceId, приложение часто ломается на этапе компиляции, а не уже в продакшене. Это тихое преимущество, но оно важно. Команды раньше замечают путаницу в полях, особенно когда несколько разработчиков каждую неделю работают с одними и теми же таблицами.

Проверять обычные запросы тоже проще. Короткий вызов Prisma показывает большинству разработчиков, что делает код, без необходимости разбирать SQL построчно. В pull request это очень помогает. Люди могут сосредоточиться на самом бизнес-правиле, а не проверять, не спрятан ли баг в join, alias или выбранном столбце.

Этот выигрыш особенно заметен, когда продукт еще быстро меняется. Стартап может трижды поменять онбординг за месяц, добавить проверку подписки, а потом разделить одну роль пользователя на две. Prisma хорошо справляется с такой текучкой, потому что модель данных, сгенерированные типы и код запросов двигаются вместе.

Это еще и снижает порог входа в команду. Сильный SQL-разработчик может писать отличные запросы, но не каждая задача должна ждать именно этого человека. Prisma позволяет большему числу людей безопасно делать CRUD-работу, сохранять понятные ревью и вносить изменения в схему с меньшим сопротивлением. Для обычного проектирования backend-запросов это часто самый быстрый путь.

Где raw SQL дает больше контроля

Raw SQL помогает, когда вопрос по данным перестает быть простым. Если в запросе пять соединений, собственная сортировка, групповые итоги и правила для крайних случаев, обычный SQL часто читается лучше, потому что каждый шаг виден прямо на странице.

Это важнее, чем многие признают. В ORM логика может расползаться по вложенным include, вспомогательным функциям и коду приложения, который меняет результат после возврата запроса. База данных все равно делает сложную работу, но понять замысел становится труднее.

Сложные соединения — хороший пример. В SQL сразу видно, какие таблицы соединяются, зачем они соединяются и где применяются фильтры. Не нужно гадать, загрузилась ли связь вторым запросом или условие сработало до соединения или после него.

Оконные функции — еще одно место, где SQL выглядит чище. Если нужны «топ-3 заказа по каждому клиенту», скользящий итог или рейтинг внутри каждого аккаунта, SQL говорит об этом напрямую. Та же задача в Prisma может потребовать нескольких вызовов или дополнительной обработки в JavaScript, а это сложнее тестировать и легче замедлить.

Raw SQL также позволяет посмотреть на точный план запроса. Можно запустить запрос, проверить, как база его читает, увидеть, использует ли он индекс, и быстро заметить лишнюю работу. Это трудно превзойти, когда страница после одного нового продуктового правила падает с 80 мс до 2 секунд.

Иногда один хорошо настроенный запрос заменяет целую цепочку ORM-вызовов. Команда может сначала получить пользователей, потом заказы, потом платежи, а затем посчитать итоги в коде. Один SQL-запрос может соединить эти таблицы, сгруппировать строки и вернуть ровно то, что нужно экрану. Обычно это уменьшает число походов к базе и расход памяти.

Отчеты и массовые обновления тоже обычно лучше подходят под такой стиль.

  • Ежемесячные сводки по выручке
  • «Пометить все просроченные пробные периоды как неактивные»
  • Таблицы лидеров и рейтинги
  • Догрузка данных после изменения схемы
  • Большие выгрузки для финансов или операционной команды

В спорах Prisma vs raw SQL практическая граница выглядит так: raw SQL часто лучше там, где сама логика базы данных и есть часть продукта, а не просто место для хранения. Если запросу нужна тонкая настройка, точные итоги или одноразовая пакетная работа, ручная запись обычно оказывается яснее.

Как выбирать по одному запросу за раз

Считайте каждый запрос отдельным решением, а не правилом для всей команды. Большинство продуктов нормально живут с Prisma для повседневных чтений и записей, а raw SQL используют там, где продуктовая логика или скорость начинают усложняться.

Сначала опишите бизнес-правило простыми словами. Например: «Покажи заказы, которые ушли с опозданием, но скрой заказы, возвращенные в течение 48 часов, если только финансы не пометили их на проверку». Если вы не можете четко сформулировать правило, запрос будет непонятным и в Prisma, и в SQL.

Потом соберите самый простой запрос, который этому правилу соответствует. Если Prisma выражает его чисто, начните с него. Это ускоряет рутинную работу, уменьшает количество шаблонного кода и делает обычные изменения менее раздражающими.

Но на коде Prisma останавливаться не стоит. Посмотрите, какой SQL он реально отправляет. Аккуратный вызов ORM все равно может превратиться в лишние соединения, неудобные фильтры или сортировку, которая нагрузит базу сильнее, чем вы ожидали.

Проверьте медленный сценарий на данных, похожих на реальные. Десять тестовых строк почти ничего не доказывают. Возьмите достаточное количество пользователей, заказов, счетов или событий, чтобы это напоминало реальное использование, и измерьте, как запрос ведет себя, когда у одного аккаунта данных намного больше, чем у остальных.

Хорошо работает простое правило:

  • Обычную работу создания, чтения, обновления и удаления оставляйте в Prisma.
  • Переходите на raw SQL, если запросу нужны сложные группировки, рейтинг или точное использование индексов.
  • Переносите запрос в SQL, если Prisma справляется, но сгенерированный SQL выглядит неуклюже.
  • Меняйте только один болезненный запрос, а не всю кодовую базу.

Последний пункт экономит командам много лишних движений. Не нужен полный переписанный проект только потому, что один отчет стал сложным.

Частый пример — дашборд по оттоку. Первая версия может проверять только недавние входы в систему и неоплаченные счета, и Prisma здесь кажется вполне подходящим. Позже продукт добавляет историю обращений в поддержку, смену тарифов, льготные периоды и ручные исключения. На этом этапе один SQL-запрос может быть и легче для чтения, и быстрее в работе, а остальная часть приложения останется на Prisma.

Такой подход «запрос за запросом» делает кодовую базу спокойнее. Он еще и упрощает поддержку в будущем, потому что у каждого исключения есть ясная причина, а не новая норма для всего подряд.

Пример растущего продукта

Выстройте более четкие границы данных
Разделите работу со схемой, правила приложения и SQL, чтобы изменения перестали прыгать по кодовой базе.

Небольшая команда делает экран поддержки для заказов. Им нужна одна таблица с заказом, клиентом, статусом платежа и историей возвратов. В первый день Prisma отлично подходит для этой задачи.

Они получают заказы, включают запись клиента, включают возвраты и добавляют несколько фильтров, например диапазон дат и статус заказа. Все в команде читают объект запроса без проблем, а изменения занимают минуты вместо часов.

Потом поддержка начинает просить больше.

Им нужно фильтровать заказы, которые были отправлены до запроса на возврат. Им нужно скрыть тестовых клиентов. Им нужно видеть заказы с неудачной попыткой оплаты, но только если позже человек вручную одобрил заказ. Через неделю они просят «клиентов с двумя или более возвратами за 90 дней» и «сортировку по последнему событию возврата, а не по дате заказа».

Prisma все еще может многое из этого сделать. В этом и есть сложность. Запрос не ломается сразу. Он просто становится все труднее доверять.

Вскоре у команды появляются вложенные блоки AND и OR, фильтры по связям внутри фильтров по связям и немного дополнительного JavaScript после возврата запроса. Одно правило живет в Prisma, другое — в коде приложения, а третье прячется во вспомогательной функции. Читать это ощущается как заполнение налоговой формы.

Вот здесь Prisma vs raw SQL перестает быть спором о стиле и становится проблемой читаемости.

Один вручную написанный запрос может сделать экран понятнее. Команда может соединить заказы, клиентов, платежи и возвраты в одном месте. Можно дать названия неудобным правилам, вычислить статус поддержки прямо в запросе и отсортировать по тому самому событию, которое важно поддержке.

Выигрыш не в том, что «SQL более продвинутый». Выигрыш в том, что бизнес-правило находится в одном видимом блоке, а не распылено по include, вложенным фильтрам и постобработке.

Для такого экрана обычно лучше всего подходит смешанный подход. Оставьте Prisma для обычных операций создания и обновления. Используйте один raw SQL-запрос для страницы поддержки, которая постоянно обрастает странными маленькими исключениями.

Такое разделение часто экономит больше времени, чем попытка протащить каждый запрос через один и тот же инструмент.

Ошибки, которые делают команды

Команды обычно попадают в неприятности, когда воспринимают Prisma vs raw SQL как вопрос идентичности команды, а не выбора для конкретного запроса. Один разработчик хочет всей безопасности и скорости Prisma. Другой — полного контроля. В итоге кодовая база качается между двумя подходами без понятной причины.

Одна частая ошибка — слишком рано переписывать слишком многое на SQL. Prisma хорошо справляется со многими повседневными задачами: базовыми чтениями, записями, связями и изменениями схемы. Если команда отказывается от этого после одного медленного запроса, часто она меняет одну проблему на три новых: дублирование кода запросов, менее читаемую бизнес-логику и больше работы на ревью.

Прямо противоположная ошибка встречается не реже. Команды продолжают заставлять Prisma обслуживать неудобные правила отчетности, исключения в ценообразовании или логику рейтинга, которая явно лучше ложится на SQL. Это видно, когда один запрос превращается в слои include, постобработки и ручной сортировки в коде приложения. В этот момент ORM уже не упрощает работу.

Еще одна путаница начинается, когда raw-запросы лежат в случайных вспомогательных файлах. Никто не понимает, какой запрос за какой экран отвечает. Потом приходит изменение схемы, тесты молчат, и баг вылезает позже — в отчете или админке. Raw SQL сам по себе нормален, но ему нужны место, владелец и короткий комментарий о том, зачем он существует.

На изменения схемы стоит смотреть особенно внимательно, когда команда смешивает модели ORM и вручную написанный SQL. Prisma может раньше поймать часть несовпадений. Raw-запросы часто не могут этого сделать. Если столбец меняет тип или условие соединения больше не совпадает с продуктовым правилом, запрос может продолжить работать и возвращать неправильные данные.

Небольшие команды еще и тратят время на споры по вкусу. «SQL чище» и «ORM всегда безопаснее» — одинаково ленивые позиции. Лучше измеряйте запрос. Смотрите на время выполнения, количество строк и на то, легко ли будет изменить код в следующем месяце.

Простой пример продукта это хорошо показывает. Запрос для дашборда начинается как обычное чтение через Prisma. Потом ему нужны собственный рейтинг, частичные возвраты и сводка на конец месяца. Оставить обычные пользовательские и заказные потоки в Prisma — разумно. Написать один отчетный запрос на SQL — тоже разумно. Проблемы начинаются, когда команда выбирает один инструмент для всех задач.

Короткая проверка перед выбором

Наведите порядок в правилах бэкенда
Получите практические советы по архитектуре продукта, работе с данными и выпуску растущего приложения.

Хороший запрос — не тот, который сегодня выглядит умно. Хороший запрос — тот, который ваша команда сможет через месяц по-прежнему читать, тестировать и менять без страха. Это важнее, чем абстрактно выигрывать спор Prisma vs raw SQL.

Перед тем как писать что-либо, задайте себе пять простых вопросов:

  • Поймет ли этот запрос кто-то из команды в следующем месяце без долгого объяснения?
  • Нужны ли экрану сортировка, пагинация и итоги одновременно?
  • Требуется ли функция базы данных, которую Prisma не выражает красиво, например оконные функции, собственные CTE или сложные агрегаты?
  • Можно ли покрыть странные случаи небольшим набором фикстур и несколькими прямыми тестами?
  • Скорее всего, продуктовое правило снова изменится уже в следующем спринте?

Если на первый и четвертый вопросы ответ «нет», притормозите. Обычно это значит, что запрос уже слишком усложнили, независимо от выбранного инструмента. Простой запрос Prisma с одной небольшой постобработкой часто лучше, чем плотный SQL-выражение, которое способен отлаживать только его автор.

Если экрану нужны все данные сразу, raw SQL начинает выглядеть привлекательнее. Таблица, которой нужны пагинация, собственная сортировка, итоги и несколько условных счетчиков, может превратиться в несколько ORM-вызовов плюс связующий код в приложении. Это работает, но слишком сильно распыляет бизнес-правила.

Возможности базы данных тоже важны. Если нужны rank, partitions, рекурсивная логика или точный контроль над соединениями, ручной SQL часто проще доверять. Prisma при этом может оставаться рядом с этим запросом для остальной модели. Полный переписанный проект не нужен только потому, что один отчет стал неудобным.

Небольшая фикстура — очень честная проверка. Создайте пять или шесть строк с дубликатами, null, старыми записями и одним крайним случаем, который уже ломал поведение раньше. Если вы не можете объяснить результат на таком маленьком наборе, запрос еще не готов.

И еще одна проверка по ощущениям: как часто будет меняться правило? Если продукт каждую неделю меняет смысл «активного клиента», сначала оставляйте версию как можно проще. Начинайте там, где изменения дешевле. Позже, когда правило устоится и форма запроса перестанет меняться, перенесите сложную часть в SQL, если она действительно этого заслуживает.

Как смешивать оба подхода без хаоса

Сделайте отчеты понятнее
Превратите разрозненную логику биллинга или операционной работы в план запроса, который команде легко читать.

Большинству команд лучше помогает разделение, а не полный переход. Пусть Prisma берет на себя скучную повторяющуюся работу: создание, обновление, удаление, простые чтения и связи, которые легко проследить. А raw SQL оставьте для мест, где правила продукта накапливаются, и ORM начинает скрывать больше, чем помогает.

Экран клиента — хороший пример. Создать клиента, обновить профиль или загрузить одну запись по ID удобно в Prisma. Но если этому же экрану нужны поиск, права команды, последняя активность, счетчик неоплаченных счетов и собственная сортировка для аккаунтов «недавно рискованных», один SQL-запрос часто и читать проще, и настраивать легче.

Разделение работает только если граница остается очевидной. Если запрос помещается в Prisma за несколько понятных строк, оставляйте его там. Если вам нужны собственный рейтинг, агрегаты, сложные соединения или фильтры, от которых все прищуриваются, пишите запрос вручную.

Держите этот raw-запрос рядом с той функцией, которой он нужен. Если на странице отчета есть один необычный запрос, храните его вместе с кодом отчета, а не в огромной общей папке с разрозненным SQL. Когда кто-то позже откроет эту функцию, он должен сразу увидеть запрос, ожидаемый вход и форму результата, не блуждая по всему проекту.

Хорошо работает такой небольшой ритуал:

  • Обычный CRUD оставляйте в Prisma.
  • Для каждого raw-запроса задавайте понятные имена входных и выходных данных.
  • Добавляйте тесты туда, где соединения, фильтры и сортировка могут тихо сломаться.
  • Проверяйте медленные запросы до того, как на них начнут равняться другие функции.

Имена важнее, чем многим кажется. getCustomers почти ничего не говорит. getCustomersNeedingFollowUp(teamId, now) сразу показывает правило, область действия и замысел. То же самое делайте и с полями результата. Называйте их по экрану или задаче, для которой они нужны, а не по самому короткому алиасу, который вспомнился в момент написания.

Тесты — это страховка. Добавляйте их вокруг самых неудобных мест: фильтров доступа, правил по датам, удаления дублей, итогов и любых соединений, из-за которых строки могут случайно пропасть. В большинстве схем Prisma vs raw SQL проблема не в том, что используются оба подхода. Проблема в том, что они смешаны без ясного правила, где какой уместен.

Что делать дальше

Хватит спорить о Prisma vs raw SQL в теории. Посмотрите на запросы, которые ваша команда постоянно откладывает, обходит стороной или переписывает каждые несколько недель. Именно они показывают, где проходит реальная граница.

Хорошо работает простой шаблон. Оставляйте повседневный CRUD в Prisma, когда код легко читать, его структура совпадает с моделью данных, а продукт меняется часто. Переносите запрос в SQL, когда версия на ORM скрывает настоящую логику, создает неудобные соединения или делает производительность трудно предсказуемой.

Проведите короткий обзор и разложите запросы на две группы:

  • Оставьте в Prisma, если новый коллега может прочитать запрос за минуту и безопасно его изменить.
  • Перенесите в SQL, если бизнес-правило зависит от рейтинга, агрегации, проверок доступа, оконных функций или точного поведения соединений.
  • Оставьте в Prisma, если скорость нормальная и запрос в основном повторяет вашу схему.
  • Перенесите в SQL, если нужен четкий контроль над индексами, планом запроса или особенностями конкретной базы.

Когда переносите запрос, запишите почему. Одного короткого комментария рядом с запросом обычно достаточно. Объясните, что делает запрос, почему Prisma мешал его понимать и что должно остаться верным, если кто-то будет править его позже. Такая заметка экономит больше времени, чем ожидает большинство команд.

Это еще и делает кодовую базу честнее. Raw SQL не должен превращаться в скрытую кучу разовых исправлений. У каждого запроса должна быть причина существовать. Если через месяц никто не может объяснить эту причину, значит команда, скорее всего, слишком спешила или пропустила исправление модели данных.

Если выпуск все равно замедляется, проблема может быть шире, чем один неудачный запрос. Командам часто нужен более четкий раздел между продуктовой логикой, проектированием схемы и доступом к базе данных. Oleg Sotnikov может помочь посмотреть на эту границу в рамках сфокусированной консультации Fractional CTO, особенно для стартапов и небольших команд, которым нужен более быстрый выпуск без превращения backend в лабиринт.

Лучше работают маленькие решения, а не большие переписывания. Выберите три болезненных запроса, честно разложите их и на этой неделе наведите порядок в границе.