设计与多媒体

Prisma

为 Node 与 TypeScript 项目设计 Prisma schema、编写类型安全查询,并解决迁移、连接池与 N+1 关联加载问题。

它能做什么

覆盖 schema.prisma 建模(关系、索引、枚举、JSON、Decimal)、迁移流程(db push / migrate dev / migrate deploy 的选用、baseline、drift、失败迁移),以及 Prisma Client 查询(过滤、嵌套写入、upsert、$queryRaw 与 TypedSQL)。按错误码(P2002、P2025、P2024、P2034、P3009 等)和症状(查询慢、连接池耗尽、事务超时、死锁)定位运行时问题。涵盖 Lambda、Vercel 等 Serverless 部署、PgBouncer 配合、$extends 客户端扩展(软删除、审计、租户隔离)、种子数据与测试隔离。明确不覆盖数据库服务器调优和手写 SQL 优化,也不面向其他 ORM。

什么时候用它

  • 设计或审查 schema.prisma 中的模型、关系、索引和枚举
  • 处理迁移 drift、失败迁移,或在 push / migrate dev / migrate deploy 之间选择
  • 排查 P2002、P2024、P2034、P3009,或 Lambda、Vercel 上连接池耗尽
  • 修复 N+1 关联加载、慢查询,或事务超时与死锁

技能文档

User preferences and memory live in ~/Clawic/data/prisma/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/prisma/ or ~/clawic/prisma/), move it to ~/Clawic/data/prisma/.

When To Use

  • Writing or reviewing schema.prisma: models, relations, referential actions, indexes, enums, JSON, money and time columns
  • Getting a schema change into a database safely: db push vs migrate dev vs migrate deploy, renames, backfills, baselining, drift, failed migrations
  • Writing Prisma Client queries: filters, pagination, nested writes, upserts, aggregation, and the raw escape hatch
  • Diagnosing runtime failures by error code (P1xxx connectivity, P2xxx query, P3xxx migrate) or by symptom (slow, too many queries, pool exhausted, transaction timeout)
  • Deploying Prisma: generate in CI and Docker, engine targets, serverless and edge, poolers, test databases
  • Extending the client: soft delete, audit logs, tenant scoping, computed fields — and porting existing $use middleware
  • Not for tuning the database server itself (EXPLAIN plans, vacuum, index internals) or hand-written SQL optimization — see Related Skills

Quick Reference

SituationPlay
Schema edited, database unchangedDev: npx prisma migrate dev; prod/CI: migrate deploy; throwaway prototype only: db push (→ Core Rules 1)
"Unknown argument" or a model missing on the clientClient is stale generated code — npx prisma generate (→ Core Rules 2)
Renaming a field or table with rows in itPrisma diffs by name: rename = DROP + ADD. @map to keep the column, or hand-edit the SQL (→ Core Rules 3)
Existing database, no prisma/migrations folderdb pull to introspect, then baseline the first migration as applied (→ migrations.md)
"Drift detected" or migrate dev wants to reset prod-like dataSomeone changed the DB out of band; diff before you accept anything (→ migrations.md)
P3009 — failed migration blocks every deployFix the SQL by hand, then migrate resolve --applied or --rolled-back (→ errors.md)
P2002 unique constraint failedGenuine duplicate or an upsert race — catch the code, retry once (→ errors.md)
P2025 record not found on update/deleteRow gone, or an extra where filter did not match — this is the optimistic-locking signal (→ transactions.md)
P2024 timed out fetching a new connectionPool exhausted: long transactions, too many client instances, or a limit below concurrency (→ connections.md)
"Too many connections" on Lambda, Vercel or Next.js devOne client per process behind a globalThis singleton, plus an external pooler (→ connections.md)
Query log shows hundreds of queries per requestRelation loads in a loop, not include — measure before rewriting (→ performance.md)
Relation is undefined at runtimePrisma never loads relations implicitly; include or select it (→ queries.md)
List endpoint gets slower as the table growstake missing, offset pagination, or count() scanning (→ performance.md)
Transaction times out at 5s or deadlocks under loadShrink the body, raise timeout deliberately, retry P2034 (→ transactions.md)
Soft delete, audit trail, tenant scoping, computed fieldsClient extensions ($extends), not middleware (→ extensions.md)
include result does not narrow in TypeScriptGetPayload / validator instead of hand-written interfaces (→ typescript.md)
Do not know how to serialize a BigInt, or Decimal arrives as an objectPrisma returns BigInt and Decimal, not numbers (→ typescript.md)
Query the schema cannot express (CTE, window, DISTINCT ON, upsert-heavy batch)$queryRaw tagged template, or TypedSQL for typed results (→ raw-sql.md)
Deploy fails with "Query engine could not be located"prisma generate missing from the build, or wrong binaryTargets for the image (→ deployment.md)
Tests interfere with each other or need a real databasePer-worker database or schema, rollback-per-test, deterministic seed (→ testing.md)
Works on PostgreSQL, breaks on MySQL, SQLite, or MongoDBProvider capability gap, not a Prisma bug (→ providers.md)
Anything elseTurn on query logging and read the SQL Prisma actually sent, then run that SQL by hand: the answer is almost always in the gap between what you expressed and what was emitted (→ performance.md)

Depth on demand, by phase:

  • Modelschema.md relations, referential actions, keys, indexes, types, multi-tenancy · providers.md what PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and PlanetScale each refuse to do
  • Changemigrations.md push vs migrate, renames, baselining, drift, zero-downtime sequences · testing.md test databases, isolation, seeding, mocking · deployment.md generate in CI and Docker, engines, serverless, edge, monorepos
  • Queryqueries.md filters, nested writes, upsert, pagination, aggregation · performance.md relation loading, indexes, counts, logging, payload size · transactions.md batch vs interactive, isolation, retries, optimistic locking · raw-sql.md $queryRaw, TypedSQL, safe interpolation
  • Extend and typeextensions.md $extends for soft delete, audit, RLS, computed fields · typescript.md generated types, payload types, JSON, Decimal, BigInt
  • Operateconnections.md pool sizing, PgBouncer, serverless, directUrl · errors.md every P-code to cause and fix

Core Rules

  1. One migration command per environment, and never the other one. db push for a throwaway prototype (no history, silently drops columns to converge); migrate dev in development only (generates SQL, needs a shadow database, may reset); migrate deploy in CI and production (applies existing files, never generates, never resets). Check: grep the deploy pipeline — a migrate dev or a db push there is an incident waiting for its trigger.
  2. The client is generated code, not a schema reader. Every schema edit needs npx prisma generate, and every install path needs it too (postinstall script plus an explicit step in the build). The signature failure: types compile locally, production throws "Unknown argument" or a model that exists in the schema is missing on the client, because a cached node_modules shipped a client generated from an older schema.
  3. Rename with @map, never by renaming the field. Prisma diffs by name, so renaming fullName to name emits DROP COLUMN + ADD COLUMN: every row loses that value. Keep the column and rename only in Prisma (name String @map("fullName")), or hand-edit the generated migration to ALTER TABLE ... RENAME COLUMN before it is applied. Same rule for models (@@map).
  4. Index the foreign key yourself on PostgreSQL. Prisma migrate creates indexes for @id and @unique only. MySQL auto-indexes FK columns; PostgreSQL does not — so @@index([authorId]) is your job, and without it a where: { authorId } filter or a cascading parent delete scans the child table. Under relationMode = "prisma" (PlanetScale and friends) there are no FK constraints at all and the index is mandatory on every relation scalar.
  5. include costs one query per relation; loops cost one per row. Queries = 1 + one per distinct relation at each nesting level: findMany with three includes is 4 round trips whether it returns 10 rows or 10,000. The N+1 you actually have comes from a loop or a GraphQL resolver — with one exception: findUnique/findUniqueOrThrow calls on the same model in the same event-loop tick are batched into a single WHERE id IN (...). Nothing else batches.
  6. undefined means "ignore this filter", null means "match NULL". deleteMany({ where: { tenantId: undefined } }) is a full-table delete, and findFirst({ where: { email: undefined } }) returns a stranger's row. Rule: never let a possibly-undefined variable reach a where. Validate first, or make the skip explicit with Prisma.skip under the strictUndefinedChecks preview (prisma >=5.20), which turns implicit undefined into an error.
  7. An interactive transaction holds a pooled connection for its entire body. Defaults: timeout 5000 ms, maxWait 2000 ms. No HTTP calls, no queues, no user input inside it. Concurrency ceiling is the pool: with connection_limit=5, the sixth concurrent interactive transaction waits and then fails P2028 after maxWait (2 s by default) — the app looks "deadlocked" while the database is idle. Ordinary queries queueing for the same pool fail P2024 instead, at pool_timeout (10 s).
  8. Size the pool against the database, not against hope. Prisma's default connection_limit is num_physical_cpus * 2 + 1 per client instance. Budget: connection_limit ≤ (max_connections − 3 reserved − other consumers) / expected instances. PostgreSQL ships max_connections = 100 with 3 reserved, so on a 4-core runtime (9 connections each) eleven instances ask for 99 against the 97 available, and the eleventh gets P1001. Serverless: 1-2 plus an external pooler (connections.md).
  9. Retry only the codes that are retryable. P2034 (write conflict / deadlock) and P2024 (pool timeout) deserve a retry of the whole transaction with jitter, capped at 3 attempts; P2002 deserves exactly one retry when it came from an upsert race, and zero when the duplicate is real. Never retry P2003, P2025 or any P1012 — nothing about a second attempt changes them.

Error Codes

Codes are stable across versions; message text is not. Match on e.code after narrowing with e instanceof Prisma.PrismaClientKnownRequestError. Full catalog with causes and fixes: errors.md.

CodeMeaningFirst move
P1001Can't reach database serverHost/port/SSL or network, not Prisma — test the same URL with a plain client
P1017Server has closed the connectionIdle timeout or a pooler killing sessions mid-flight (→ connections.md)
P2002Unique constraint failedRead meta.target for the field, then decide: duplicate data or upsert race (rule 9)
P2003Foreign key constraint failedParent missing, or delete order wrong — check the referential action, not the query
P2025Record to update/delete not foundRow gone, or your extra where filter did not match (the optimistic-lock signal)
P2024Timed out fetching a new connection from the poolPool exhausted; default pool timeout is 10s (→ connections.md)
P2028Transaction API errorUsually a transaction used after commit, or maxWait exceeded
P2034Write conflict or deadlockExpected under contention: retry the whole transaction (rule 9)
P3009Failed migration found in the historyDeploys stay blocked until migrate resolve records the decision
P3005Database schema is not emptyYou need a baseline migration, not a first migration (→ migrations.md)

Relation Loading

  • Nothing is loaded implicitly. A relation you did not include or select is undefined at runtime and absent from the type — which is why the type error and the runtime bug appear together.
  • include returns the full scalar set of the relation; select inside include prunes it. On wide rows the difference is bytes over the wire per row, and it is the cheapest optimization in the list.
  • select and include are mutually exclusive at the same level. Nest them instead: include: { posts: { select: { id: true, title: true } } }.
  • Filtered relations (include: { posts: { where: { published: true }, take: 5 } }) push the filter into the relation query — do this instead of loading everything and filtering in JavaScript.
  • Relation counts belong to the same round trip: select: { _count: { select: { posts: true } } }. A posts.length after loading every post is the same answer with the whole table in memory.
  • Relation load strategy is selectable where supported (relationLoadStrategy: "join" | "query", prisma >=5.7 with the relationJoins preview on PostgreSQL and MySQL): join is one round trip with JSON aggregation, query is one query per relation with a smaller, simpler payload. Measure both on a real dataset — deep nesting favors join on a distant database, wide relations favor query. Details and current status: performance.md.

Connection Budget

Formula, applied before touching any other performance knob:

total connections = client instances × connection_limit
must satisfy: total ≤ max_connections − reserved − other consumers
  • Client instances are processes, not requests: one Node server = 1; a clustered server = 1 per worker; serverless = 1 per warm sandbox, and the count is set by your traffic, not by you.
  • connection_limit is a URL parameter: ?connection_limit=10&pool_timeout=20. Raising it does not create database capacity — it decides who queues where.
  • Serverless without a pooler is the classic outage: every cold start opens its own pool and nothing gives them back. Use PgBouncer, a provider pooler, or Prisma Accelerate, and keep connection_limit at 1-2 (→ connections.md).
  • Transaction-mode poolers require ?pgbouncer=true (Prisma stops using named prepared statements) and a separate directUrl for migrations, which need a real session.

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/prisma/config.yaml.

VariableTypeDefaultEffect
providerpostgresql | mysql | sqlite | sqlserver | mongodb | cockroachdbpostgresqlSelects every provider-gated answer: FK indexing, mode: "insensitive", arrays, enums, JSON path syntax, skipDuplicates (→ providers.md)
prisma_majornumber (5-6)6Which version-gated features are offered (createManyAndReturn, TypedSQL, omit, Prisma.skip) when the installed version is unknown
poolernone | pgbouncer | supavisor | provider-pooler | acceleratenoneWhether URLs carry pgbouncer=true, whether directUrl is required, and the recommended connection_limit
deploy_targetnode-server | serverless | edge | dockernode-serverDrives the client-instantiation pattern, binaryTargets, $disconnect advice and generate placement (→ deployment.md)
migration_workflowmigrate | push | sql-firstmigrateWhich command sequence is emitted for a schema change, and whether hand-written SQL files are the source of truth
id_stylecuid | uuid | uuidv7 | autoincrementcuidThe @id default in every generated model and example
naming_conventioncamel-with-map | db-nativecamel-with-mapWhether generated models carry @map/@@map to snake_case database names or match the database verbatim
default_takenumber (1-1000)50The pagination cap added to any findMany emitted without one (→ performance.md)
destructive_confirmbooltruemigrate reset, db push --accept-data-loss, and deleteMany/updateMany without a where are emitted for review instead of run

Preference areas — customizable dimensions; a stated preference is recorded in config.yaml and applied from then on:

  • Tooling — package manager and runner, prisma-client-js vs the newer generator, validation library paired with Prisma (Zod, Valibot), seeding tool
  • Thresholds — default page size, transaction timeout and maxWait, retry attempts and backoff, the query duration worth flagging in a review
  • Conventions — model and field naming, singular vs plural tables, soft-delete field name, timestamp columns, enum vs lookup table, schema file layout (single file vs folder)
  • Platform — provider and version, hosting target, region distance to the database (which decides how much round-trip count matters), monorepo layout and client output path
  • Risk posture — whether migrations are applied directly or handed back as SQL for review, whether raw SQL is allowed at all, how strict the ban on deleteMany without where is
  • Output format — schema plus explanation vs schema only, whether emitted queries carry the equivalent SQL in a comment, how much of the migration plan to narrate
  • Work order — schema-first vs introspection-first, whether tests and seed data are updated in the same change as the migration
  • Integrations — pooler and database host (Neon, Supabase, PlanetScale, RDS, Turso), Accelerate or a self-managed cache, observability stack for query logs
  • Restrictions — tables Prisma must not manage (@@ignore), compliance rules that forbid raw SQL or require audit logging, columns that must never be selected by default
  • Cadence — how often to re-run introspection against production, when to prune old migrations, review cycle for unused indexes

Output Gates

Before emitting a schema, a migration, or a query:

  • Every relation scalar indexed (@@index) unless the provider already indexes it, and every relationMode = "prisma" relation indexed without exception?
  • Every rename expressed as @map/@@map, or the generated SQL hand-edited to a real RENAME?
  • The destructive step (drop column, drop table) split into a later migration, after the code that stopped using it shipped?
  • onDelete/onUpdate stated explicitly on every relation instead of inherited by default?
  • Money as Decimal @db.Decimal(12,2), timestamps as DateTime @db.Timestamptz(3) where the provider has it, never Float for money?
  • Does every findMany have a take, and every where a value that cannot be undefined?
  • Does the emitted command match the environment (migrate deploy in CI, never migrate dev or db push)?
  • Is prisma generate guaranteed to run in this deployment path?

Traps

TrapWhy it failsDo instead
db push on a database with real dataConverges by dropping whatever does not match, and leaves no history to deploy elsewheremigrate dev locally, migrate deploy everywhere else (rule 1)
Building where from a request objectAny absent key becomes undefined, which Prisma reads as "no filter"Validate into an explicit shape; Prisma.skip for deliberate skips (rule 6)
new PrismaClient() per request or per moduleEach instance opens its own pool; the database hits its limit while the app looks idleOne instance per process, globalThis singleton in dev (→ connections.md)
await forgotten on a queryPrisma queries are lazy promises: nothing runs, no error, the value is a PromiseLint with no-floating-promises — this is the one bug the type checker will not show you as a failure
$transaction wrapped around a single nested writeNested writes are already one transaction; the wrapper only adds a held connectionUse the nested write alone (→ transactions.md)
Retrying an interactive transaction from inside itselfThe transaction client is dead after the failure — P2028 on the retryRetry the whole $transaction call from outside (rule 9)
Soft delete implemented in middleware or a query extensionRelation loads inside include do not pass through it: deleted children keep appearingExplicit filters, or a database view — the honest limits are in extensions.md
createMany when you need the rows backReturns a count only, and skips nested creates entirelycreateManyAndReturn (prisma >=5.14, not on MySQL) or a transaction of creates
count() on a large table for a UI badgeIt is a full scan every render, and it is on the request pathCached count, approximate count, or _count scoped to a relation (→ performance.md)
String concatenation into $queryRawUnsafeSQL injection with the word "unsafe" already in the callTagged $queryRaw with Prisma.sql/Prisma.join (→ raw-sql.md)
Raw SQL used for writes that other code reads through PrismaRaw bypasses @updatedAt, @default, extensions and middleware; rows come back with stale metadataKeep writes in the client, or set the columns yourself in the SQL
@unique on a nullable column as a "one per user" ruleSQL treats NULLs as distinct: unlimited NULL rows pass the constraintMake it NOT NULL, or add a partial/filtered unique index in raw SQL
Enum values removed or reordered in a live schemaRows holding the removed value break reads, and some engines cannot drop a value at allAdd-only enums, or a lookup table once the set churns (→ schema.md)

Where Experts Disagree

  • cuid vs uuid vs bigint primary keys. Random v4 UUIDs fragment the B-tree and widen every secondary index; sequential integers leak volume and complicate multi-writer merges. Boundary: externally visible IDs → cuid/UUIDv7 (time-ordered, still opaque); internal high-write tables → autoincrement() bigint. Switch on measured index bloat, not on aesthetics.
  • Schema-first vs introspection-first. Prisma's default is schema-first, and it is right when the application owns the database. When the database is shared with other systems, DBAs, or hand-written SQL, db pull plus baselined migrations avoids fighting for ownership — the boundary is who is allowed to change the schema, not which is more modern.
  • How much raw SQL is acceptable. One camp keeps everything in the client for type safety and extension coverage; the other drops to $queryRaw at the first CTE or window function. Workable line: raw for read-only analytics and DDL-adjacent work, client for anything that writes rows other code reads back — because raw writes bypass @updatedAt, defaults, and every extension.
  • Prisma Migrate vs a plain SQL migration tool. Migrate is excellent at diffing and terrible at expressing operations SQL has and Prisma does not (concurrent indexes, partitioning, triggers). Teams past a certain size run Prisma for the schema and hand-written SQL for the change; that is a legitimate configuration (migration_workflow: sql-first), not a defeat.

More Clawic skills, get them at https://clawic.com/skills/prisma (install if the user confirms):

  • pg — the PostgreSQL server underneath: plans, vacuum, locks, replication
  • sql — hand-written SQL, cross-engine portability, index and query design
  • typescript — type-system depth beyond Prisma's generated types
  • nodejs — process lifecycle, memory, and shutdown around the client
  • nextjs — where the client lives in App Router, server actions, and build-time generation

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/prisma.

常见问题

它会处理数据库服务器调优或手写 SQL 优化吗?
不会。明确不覆盖数据库服务器本身的调优(EXPLAIN、vacuum、索引内部机制)以及手写 SQL 优化,也不面向其他 ORM,这些都不在范围内。
db push、migrate dev 和 migrate deploy 怎么区分使用?
按环境分配命令:db push 仅用于临时原型(无历史记录,会静默删列以收敛);migrate dev 仅用于开发(生成 SQL,需要 shadow 数据库,可能重置);migrate deploy 用于 CI 和生产(应用已有文件,不生成也不重置)。
在 Serverless 平台连接耗尽时怎么处理?
覆盖连接池配置、每个进程一个 client 的 globalThis 单例模式、PgBouncer 等外部连接池以及 directUrl,并给出针对 PostgreSQL max_connections 的预算计算方法。

相关技能

编写、审阅、调优 MySQL、SQLite、MariaDB、SQL Server 上的 SQL,并安全迁移。

133 次安装8 星标

诊断 PostgreSQL 慢查询、设计表结构与索引,并在生产库上安全执行迁移。

130 次安装4 星标

诊断 GraphQL 缺陷并加固 schema、resolver 与客户端,处理 N+1、null 传播、订阅失效与联邦拆分。

91 次安装2 星标

按症状路由到 FastAPI 子指南,排查卡顿、验证错误与异步陷阱。

120 次安装7 星标

按字节、语法、类型映射、契约、尺寸五层定位并修复 JSON 问题。

作者 Iván112 次安装3 星标

排查并修复 Svelte 与 SvelteKit 的响应式、SSR 与构建问题,并把 Svelte 4 代码迁移到 runes。

62 次安装3 星标