prisma-sql 1.76.1 β 1.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generator.cjs +2423 -2279
- package/dist/generator.cjs.map +1 -1
- package/dist/generator.js +2423 -2279
- package/dist/generator.js.map +1 -1
- package/dist/index.cjs +3511 -3210
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +22 -6
- package/dist/index.d.ts +22 -6
- package/dist/index.js +3511 -3210
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +589 -899
package/readme.md
CHANGED
|
@@ -2,97 +2,95 @@
|
|
|
2
2
|
|
|
3
3
|
<img width="250" height="170" alt="image" src="https://github.com/user-attachments/assets/3f9233f2-5d5c-41e3-b1cd-ced7ce0b54c2" />
|
|
4
4
|
|
|
5
|
-
Prerender Prisma SQL and
|
|
5
|
+
Prerender Prisma queries to SQL and execute them directly via `postgres.js` or `better-sqlite3`.
|
|
6
6
|
|
|
7
|
-
**Same API. Same types.
|
|
7
|
+
**Same Prisma API. Same Prisma types. Lower read overhead.**
|
|
8
8
|
|
|
9
|
-
```
|
|
9
|
+
```ts
|
|
10
10
|
import { PrismaClient } from '@prisma/client'
|
|
11
11
|
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
12
12
|
import postgres from 'postgres'
|
|
13
13
|
|
|
14
|
-
const sql = postgres(process.env.DATABASE_URL)
|
|
14
|
+
const sql = postgres(process.env.DATABASE_URL!)
|
|
15
15
|
const basePrisma = new PrismaClient()
|
|
16
16
|
|
|
17
17
|
export const prisma = basePrisma.$extends(
|
|
18
18
|
speedExtension({ postgres: sql }),
|
|
19
19
|
) as SpeedClient<typeof basePrisma>
|
|
20
20
|
|
|
21
|
-
// Regular queries - 2-7x faster
|
|
22
21
|
const users = await prisma.user.findMany({
|
|
23
22
|
where: { status: 'ACTIVE' },
|
|
24
23
|
include: { posts: true },
|
|
25
24
|
})
|
|
26
25
|
|
|
27
|
-
// Batch queries - combine multiple queries into one database call
|
|
28
26
|
const dashboard = await prisma.$batch((batch) => ({
|
|
29
27
|
activeUsers: batch.user.count({ where: { status: 'ACTIVE' } }),
|
|
30
|
-
recentPosts: batch.post.findMany({
|
|
31
|
-
|
|
28
|
+
recentPosts: batch.post.findMany({
|
|
29
|
+
take: 10,
|
|
30
|
+
orderBy: { createdAt: 'desc' },
|
|
31
|
+
}),
|
|
32
|
+
taskStats: batch.task.aggregate({
|
|
33
|
+
_count: true,
|
|
34
|
+
_avg: { estimatedHours: true },
|
|
35
|
+
}),
|
|
32
36
|
}))
|
|
33
37
|
```
|
|
34
38
|
|
|
39
|
+
## What it does
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
`prisma-sql` accelerates Prisma **read** queries by skipping Prisma's read execution path and running generated SQL directly through a database-native client.
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
It keeps the Prisma client for:
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
- schema and migrations
|
|
46
|
+
- generated types
|
|
47
|
+
- writes
|
|
48
|
+
- fallback for unsupported cases
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
const users = await prisma.user.findMany({ where: { status: 'ACTIVE' } })
|
|
44
|
-
const posts = await prisma.post.count()
|
|
45
|
-
const stats = await prisma.task.aggregate({ _count: true })
|
|
46
|
-
// β 3 separate database round trips = slower
|
|
47
|
-
```
|
|
50
|
+
It accelerates:
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
- `findMany`
|
|
53
|
+
- `findFirst`
|
|
54
|
+
- `findUnique`
|
|
55
|
+
- `count`
|
|
56
|
+
- `aggregate`
|
|
57
|
+
- `groupBy`
|
|
58
|
+
- PostgreSQL `$batch`
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
const results = await prisma.$batch((batch) => ({
|
|
53
|
-
users: batch.user.findMany({ where: { status: 'ACTIVE' } }),
|
|
54
|
-
posts: batch.post.count(),
|
|
55
|
-
stats: batch.task.aggregate({ _count: true }),
|
|
56
|
-
}))
|
|
57
|
-
// β
1 database round trip = 2-3x faster
|
|
58
|
-
```
|
|
60
|
+
## Why use it
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
Prisma's DX is excellent, but read queries still pay runtime overhead for query-engine planning, validation, transformation, and result mapping.
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
`prisma-sql` moves that work out of the hot path:
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
- builds SQL from Prisma-style query args
|
|
67
|
+
- can prebake hot queries at generate time
|
|
68
|
+
- executes via `postgres.js` or `better-sqlite3`
|
|
69
|
+
- maps results back to Prisma-like shapes
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
The goal is simple:
|
|
67
72
|
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
- Result serialization and mapping
|
|
72
|
-
|
|
73
|
-
This extension bypasses the engine for read queries and executes raw SQL directly via postgres.js or better-sqlite3.
|
|
74
|
-
|
|
75
|
-
**Result:** Same API, same types, 2-7x faster reads.
|
|
73
|
+
- keep Prisma's developer experience
|
|
74
|
+
- cut read-path overhead
|
|
75
|
+
- stay compatible with existing Prisma code
|
|
76
76
|
|
|
77
77
|
## Installation
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
### PostgreSQL
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
82
|
npm install prisma-sql postgres
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
### SQLite
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
88
|
npm install prisma-sql better-sqlite3
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
## Quick
|
|
91
|
+
## Quick start
|
|
92
92
|
|
|
93
|
-
###
|
|
94
|
-
|
|
95
|
-
Add the SQL generator to your `schema.prisma`:
|
|
93
|
+
### 1) Add the generator
|
|
96
94
|
|
|
97
95
|
```prisma
|
|
98
96
|
generator client {
|
|
@@ -111,62 +109,41 @@ model User {
|
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
model Post {
|
|
114
|
-
id
|
|
115
|
-
title
|
|
116
|
-
authorId
|
|
117
|
-
author
|
|
112
|
+
id Int @id @default(autoincrement())
|
|
113
|
+
title String
|
|
114
|
+
authorId Int
|
|
115
|
+
author User @relation(fields: [authorId], references: [id])
|
|
118
116
|
}
|
|
119
117
|
```
|
|
120
118
|
|
|
121
|
-
###
|
|
119
|
+
### 2) Generate
|
|
122
120
|
|
|
123
121
|
```bash
|
|
124
122
|
npx prisma generate
|
|
125
123
|
```
|
|
126
124
|
|
|
127
|
-
This
|
|
125
|
+
This generates `./generated/sql/index.ts`.
|
|
128
126
|
|
|
129
|
-
###
|
|
127
|
+
### 3) Extend Prisma
|
|
130
128
|
|
|
131
|
-
|
|
129
|
+
### PostgreSQL
|
|
132
130
|
|
|
133
|
-
```
|
|
131
|
+
```ts
|
|
134
132
|
import { PrismaClient } from '@prisma/client'
|
|
135
133
|
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
136
134
|
import postgres from 'postgres'
|
|
137
135
|
|
|
138
|
-
const sql = postgres(process.env.DATABASE_URL)
|
|
136
|
+
const sql = postgres(process.env.DATABASE_URL!)
|
|
139
137
|
const basePrisma = new PrismaClient()
|
|
140
138
|
|
|
141
|
-
// Type the extended client properly
|
|
142
139
|
export const prisma = basePrisma.$extends(
|
|
143
140
|
speedExtension({ postgres: sql }),
|
|
144
141
|
) as SpeedClient<typeof basePrisma>
|
|
145
|
-
|
|
146
|
-
// Regular queries - 2-7x faster
|
|
147
|
-
const users = await prisma.user.findMany({
|
|
148
|
-
where: { status: 'ACTIVE' },
|
|
149
|
-
include: { posts: true },
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
// Batch queries - combine multiple queries into one database call
|
|
153
|
-
const dashboard = await prisma.$batch((batch) => ({
|
|
154
|
-
activeUsers: batch.user.count({ where: { status: 'ACTIVE' } }),
|
|
155
|
-
recentPosts: batch.post.findMany({
|
|
156
|
-
take: 10,
|
|
157
|
-
orderBy: { createdAt: 'desc' },
|
|
158
|
-
}),
|
|
159
|
-
stats: batch.task.aggregate({
|
|
160
|
-
_count: true,
|
|
161
|
-
_avg: { estimatedHours: true },
|
|
162
|
-
}),
|
|
163
|
-
}))
|
|
164
|
-
// dashboard.activeUsers, dashboard.recentPosts, dashboard.stats
|
|
165
142
|
```
|
|
166
143
|
|
|
167
|
-
|
|
144
|
+
### SQLite
|
|
168
145
|
|
|
169
|
-
```
|
|
146
|
+
```ts
|
|
170
147
|
import { PrismaClient } from '@prisma/client'
|
|
171
148
|
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
172
149
|
import Database from 'better-sqlite3'
|
|
@@ -177,22 +154,20 @@ const basePrisma = new PrismaClient()
|
|
|
177
154
|
export const prisma = basePrisma.$extends(
|
|
178
155
|
speedExtension({ sqlite: db }),
|
|
179
156
|
) as SpeedClient<typeof basePrisma>
|
|
180
|
-
|
|
181
|
-
const users = await prisma.user.findMany({ where: { status: 'ACTIVE' } })
|
|
182
157
|
```
|
|
183
158
|
|
|
184
|
-
|
|
159
|
+
### With existing Prisma extensions
|
|
185
160
|
|
|
186
|
-
|
|
161
|
+
Apply `speedExtension` last so it sees the final query surface.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
187
164
|
import { PrismaClient } from '@prisma/client'
|
|
188
165
|
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
189
|
-
import { myCustomExtension } from './my-extension'
|
|
190
166
|
import postgres from 'postgres'
|
|
191
167
|
|
|
192
|
-
const sql = postgres(process.env.DATABASE_URL)
|
|
168
|
+
const sql = postgres(process.env.DATABASE_URL!)
|
|
193
169
|
const basePrisma = new PrismaClient()
|
|
194
170
|
|
|
195
|
-
// Chain extensions - speedExtension should be last
|
|
196
171
|
const extendedPrisma = basePrisma
|
|
197
172
|
.$extends(myCustomExtension)
|
|
198
173
|
.$extends(anotherExtension)
|
|
@@ -202,346 +177,235 @@ export const prisma = extendedPrisma.$extends(
|
|
|
202
177
|
) as SpeedClient<typeof extendedPrisma>
|
|
203
178
|
```
|
|
204
179
|
|
|
205
|
-
|
|
180
|
+
## Supported queries
|
|
206
181
|
|
|
207
|
-
|
|
182
|
+
### Accelerated
|
|
208
183
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
| Complex conditions | 13.10ms | 6.90ms | 2.37ms | **2.9x** β‘ |
|
|
217
|
-
| With relations | 0.83ms | 0.72ms | 0.41ms | **1.8x** β‘ |
|
|
218
|
-
| Nested relations | 28.35ms | 14.34ms | 4.81ms | **3.0x** β‘ |
|
|
219
|
-
| Aggregations | 0.42ms | 0.44ms | 0.24ms | **1.8x** β‘ |
|
|
220
|
-
| Multi-field orderBy | 3.97ms | 2.38ms | 1.09ms | **2.2x** β‘ |
|
|
221
|
-
| **Batch queries (4 queries)** | **1.43ms** | **-** | **0.67ms** | **2.12x** β‘ |
|
|
222
|
-
|
|
223
|
-
**Overall:** 2.10x faster than Prisma v7, 2.39x faster than v6
|
|
224
|
-
|
|
225
|
-
### SQLite Results (Highlights)
|
|
226
|
-
|
|
227
|
-
| Query Type | Prisma v6 | Prisma v7 | This Extension | Speedup vs v7 |
|
|
228
|
-
| ------------------ | --------- | --------- | -------------- | ------------- |
|
|
229
|
-
| Simple where | 0.45ms | 0.23ms | 0.03ms | **7.7x** β‘ |
|
|
230
|
-
| Complex conditions | 10.32ms | 3.87ms | 0.93ms | **4.2x** β‘ |
|
|
231
|
-
| Relation filters | 166.62ms | 128.44ms | 2.40ms | **53.5x** β‘ |
|
|
232
|
-
| Count queries | 0.17ms | 0.07ms | 0.01ms | **7.0x** β‘ |
|
|
233
|
-
|
|
234
|
-
**Overall:** 5.48x faster than Prisma v7, 7.51x faster than v6
|
|
235
|
-
|
|
236
|
-
<details>
|
|
237
|
-
<summary><b>View Full Benchmark Results (137 queries)</b></summary>
|
|
238
|
-
|
|
239
|
-
### POSTGRES - Complete Results
|
|
240
|
-
|
|
241
|
-
| Test | Prisma v6 | Prisma v7 | Generated | Drizzle | v6 Speedup | v7 Speedup |
|
|
242
|
-
| ------------------------ | --------- | --------- | --------- | ------- | ---------- | ---------- |
|
|
243
|
-
| findMany basic | 0.45ms | 0.35ms | 0.18ms | 0.29ms | 2.53x | 1.74x |
|
|
244
|
-
| findMany where = | 0.40ms | 0.34ms | 0.17ms | 0.24ms | 2.39x | 1.55x |
|
|
245
|
-
| findMany where >= | 15.88ms | 8.03ms | 2.86ms | 6.18ms | 5.55x | 2.92x |
|
|
246
|
-
| findMany where IN | 0.52ms | 0.52ms | 0.29ms | 0.38ms | 1.79x | 1.38x |
|
|
247
|
-
| findMany where null | 0.23ms | 0.29ms | 0.10ms | 0.16ms | 2.20x | 2.56x |
|
|
248
|
-
| findMany ILIKE | 0.24ms | 0.22ms | 0.18ms | 0.17ms | 1.29x | 0.99x |
|
|
249
|
-
| findMany AND | 2.36ms | 1.20ms | 0.44ms | 1.42ms | 5.41x | 2.73x |
|
|
250
|
-
| findMany OR | 13.10ms | 6.90ms | 2.37ms | 5.58ms | 5.53x | 2.93x |
|
|
251
|
-
| findMany NOT | 0.51ms | 1.14ms | 0.30ms | 0.33ms | 1.73x | 3.74x |
|
|
252
|
-
| findMany orderBy | 2.19ms | 2.31ms | 0.85ms | 0.72ms | 2.58x | 2.05x |
|
|
253
|
-
| findMany pagination | 0.23ms | 0.28ms | 0.20ms | 0.19ms | 1.15x | 1.39x |
|
|
254
|
-
| findMany select | 0.23ms | 0.22ms | 0.09ms | 0.13ms | 2.57x | 2.21x |
|
|
255
|
-
| findMany relation some | 0.83ms | 0.72ms | 0.41ms | N/A | 2.04x | 1.72x |
|
|
256
|
-
| findMany relation every | 0.70ms | 0.79ms | 0.47ms | N/A | 1.50x | 1.70x |
|
|
257
|
-
| findMany relation none | 28.35ms | 14.34ms | 4.81ms | N/A | 5.90x | 2.75x |
|
|
258
|
-
| findMany nested relation | 0.70ms | 0.72ms | 0.71ms | N/A | 0.98x | 1.36x |
|
|
259
|
-
| findMany complex | 1.18ms | 1.19ms | 0.48ms | 0.64ms | 2.45x | 2.81x |
|
|
260
|
-
| findFirst | 0.22ms | 0.25ms | 0.15ms | 0.20ms | 1.45x | 3.05x |
|
|
261
|
-
| findFirst skip | 0.26ms | 0.32ms | 0.15ms | 0.23ms | 1.75x | 3.09x |
|
|
262
|
-
| findUnique id | 0.20ms | 0.21ms | 0.13ms | 0.13ms | 1.52x | 2.53x |
|
|
263
|
-
| findUnique email | 0.18ms | 0.19ms | 0.09ms | 0.12ms | 2.03x | 2.17x |
|
|
264
|
-
| count | 0.11ms | 0.12ms | 0.04ms | 0.07ms | 2.95x | 2.50x |
|
|
265
|
-
| count where | 0.43ms | 0.47ms | 0.26ms | 0.27ms | 1.62x | 1.90x |
|
|
266
|
-
| aggregate count | 0.22ms | 0.24ms | 0.13ms | N/A | 1.63x | 1.51x |
|
|
267
|
-
| aggregate sum/avg | 0.30ms | 0.32ms | 0.23ms | N/A | 1.32x | 1.35x |
|
|
268
|
-
| aggregate where | 0.42ms | 0.44ms | 0.24ms | N/A | 1.75x | 1.84x |
|
|
269
|
-
| aggregate min/max | 0.30ms | 0.32ms | 0.23ms | N/A | 1.29x | 1.32x |
|
|
270
|
-
| aggregate complete | 0.36ms | 0.41ms | 0.27ms | N/A | 1.36x | 1.39x |
|
|
271
|
-
| groupBy | 0.38ms | 0.41ms | 0.29ms | N/A | 1.33x | 1.36x |
|
|
272
|
-
| groupBy count | 0.44ms | 0.42ms | 0.32ms | N/A | 1.36x | 1.37x |
|
|
273
|
-
| groupBy multi | 0.53ms | 0.51ms | 0.37ms | N/A | 1.43x | 1.43x |
|
|
274
|
-
| groupBy having | 0.52ms | 0.50ms | 0.40ms | N/A | 1.29x | 1.40x |
|
|
275
|
-
| groupBy + where | 0.52ms | 0.49ms | 0.31ms | N/A | 1.65x | 1.88x |
|
|
276
|
-
| groupBy aggregates | 0.50ms | 0.48ms | 0.38ms | N/A | 1.31x | 1.32x |
|
|
277
|
-
| groupBy min/max | 0.49ms | 0.50ms | 0.38ms | N/A | 1.29x | 1.35x |
|
|
278
|
-
| include posts | 2.53ms | 1.59ms | 1.85ms | N/A | 1.37x | 0.81x |
|
|
279
|
-
| include profile | 0.47ms | 0.60ms | 0.21ms | N/A | 2.26x | 2.89x |
|
|
280
|
-
| include 3 levels | 1.56ms | 1.64ms | 1.15ms | N/A | 1.36x | 1.33x |
|
|
281
|
-
| include 4 levels | 1.80ms | 1.76ms | 0.99ms | N/A | 1.81x | 1.74x |
|
|
282
|
-
| include + where | 1.21ms | 1.06ms | 1.47ms | N/A | 0.82x | 0.69x |
|
|
283
|
-
| include + select nested | 1.23ms | 0.84ms | 1.43ms | N/A | 0.86x | 0.54x |
|
|
284
|
-
| findMany startsWith | 0.21ms | 0.24ms | 0.14ms | 0.17ms | 1.46x | 1.73x |
|
|
285
|
-
| findMany endsWith | 0.48ms | 0.38ms | 0.19ms | 0.28ms | 2.51x | 1.87x |
|
|
286
|
-
| findMany NOT contains | 0.47ms | 0.40ms | 0.18ms | 0.25ms | 2.62x | 2.49x |
|
|
287
|
-
| findMany LIKE | 0.19ms | 0.22ms | 0.09ms | 0.14ms | 2.19x | 2.61x |
|
|
288
|
-
| findMany < | 25.94ms | 14.16ms | 4.16ms | 9.59ms | 6.23x | 3.12x |
|
|
289
|
-
| findMany <= | 26.79ms | 13.87ms | 4.90ms | 9.73ms | 5.46x | 3.07x |
|
|
290
|
-
| findMany > | 15.22ms | 7.55ms | 2.69ms | 5.74ms | 5.66x | 2.71x |
|
|
291
|
-
| findMany NOT IN | 0.54ms | 0.42ms | 0.26ms | 0.36ms | 2.07x | 1.42x |
|
|
292
|
-
| findMany isNot null | 0.52ms | 0.39ms | 0.19ms | 0.24ms | 2.75x | 2.21x |
|
|
293
|
-
| orderBy multi-field | 3.97ms | 2.38ms | 1.09ms | 1.54ms | 3.65x | 3.71x |
|
|
294
|
-
| distinct status | 8.72ms | 7.84ms | 2.15ms | N/A | 4.06x | 4.68x |
|
|
295
|
-
| distinct multi | 11.71ms | 11.09ms | 2.09ms | N/A | 5.61x | 5.30x |
|
|
296
|
-
| cursor pagination | 0.29ms | 0.34ms | 0.23ms | N/A | 1.25x | 1.62x |
|
|
297
|
-
| select + include | 0.89ms | 0.70ms | 0.17ms | N/A | 5.19x | 3.46x |
|
|
298
|
-
| \_count relation | 0.71ms | 0.66ms | 0.55ms | N/A | 1.29x | 1.20x |
|
|
299
|
-
| \_count multi-relation | 0.24ms | 0.29ms | 0.16ms | N/A | 1.52x | 1.90x |
|
|
300
|
-
| ILIKE special chars | 0.22ms | 0.26ms | 0.14ms | N/A | 1.54x | 1.76x |
|
|
301
|
-
| LIKE case sensitive | 0.19ms | 0.22ms | 0.12ms | N/A | 1.62x | 1.85x |
|
|
302
|
-
|
|
303
|
-
### SQLITE - Complete Results
|
|
304
|
-
|
|
305
|
-
| Test | Prisma v6 | Prisma v7 | Generated | Drizzle | v6 Speedup | v7 Speedup |
|
|
306
|
-
| ------------------------ | --------- | --------- | --------- | ------- | ---------- | ---------- |
|
|
307
|
-
| findMany basic | 0.44ms | 0.27ms | 0.04ms | 0.17ms | 9.59x | 5.47x |
|
|
308
|
-
| findMany where = | 0.45ms | 0.23ms | 0.03ms | 0.10ms | 14.14x | 6.25x |
|
|
309
|
-
| findMany where >= | 12.72ms | 4.70ms | 1.02ms | 2.09ms | 12.51x | 4.16x |
|
|
310
|
-
| findMany where IN | 0.40ms | 0.28ms | 0.04ms | 0.10ms | 10.35x | 6.55x |
|
|
311
|
-
| findMany where null | 0.15ms | 0.19ms | 0.01ms | 0.06ms | 10.97x | 12.56x |
|
|
312
|
-
| findMany LIKE | 0.15ms | 0.17ms | 0.02ms | 0.06ms | 8.64x | 9.41x |
|
|
313
|
-
| findMany AND | 1.49ms | 0.95ms | 0.26ms | 0.43ms | 5.75x | 3.45x |
|
|
314
|
-
| findMany OR | 10.32ms | 3.87ms | 0.93ms | 1.85ms | 11.09x | 3.64x |
|
|
315
|
-
| findMany NOT | 0.42ms | 0.28ms | 0.03ms | 0.09ms | 12.59x | 7.05x |
|
|
316
|
-
| findMany orderBy | 2.24ms | 1.92ms | 1.76ms | 1.81ms | 1.27x | 1.11x |
|
|
317
|
-
| findMany pagination | 0.13ms | 0.15ms | 0.02ms | 0.06ms | 5.69x | 6.24x |
|
|
318
|
-
| findMany select | 0.15ms | 0.11ms | 0.02ms | 0.04ms | 9.50x | 6.22x |
|
|
319
|
-
| findMany relation some | 4.50ms | 0.56ms | 0.40ms | N/A | 11.15x | 1.32x |
|
|
320
|
-
| findMany relation every | 9.53ms | 9.54ms | 6.38ms | N/A | 1.49x | 1.45x |
|
|
321
|
-
| findMany relation none | 166.62ms | 128.44ms | 2.40ms | N/A | 69.43x | 49.51x |
|
|
322
|
-
| findMany nested relation | 1.00ms | 0.51ms | 0.31ms | N/A | 3.28x | 1.70x |
|
|
323
|
-
| findMany complex | 0.79ms | 0.83ms | 0.43ms | 0.48ms | 1.84x | 1.74x |
|
|
324
|
-
| findFirst | 0.16ms | 0.17ms | 0.01ms | 0.06ms | 11.57x | 12.00x |
|
|
325
|
-
| findFirst skip | 0.25ms | 0.23ms | 0.03ms | 0.08ms | 8.62x | 13.31x |
|
|
326
|
-
| findUnique id | 0.12ms | 0.15ms | 0.01ms | 0.05ms | 9.92x | 11.62x |
|
|
327
|
-
| findUnique email | 0.12ms | 0.15ms | 0.01ms | 0.05ms | 8.73x | 11.43x |
|
|
328
|
-
| count | 0.17ms | 0.07ms | 0.01ms | 0.02ms | 13.33x | 10.73x |
|
|
329
|
-
| count where | 0.28ms | 0.28ms | 0.16ms | 0.17ms | 1.77x | 1.85x |
|
|
330
|
-
| aggregate count | 0.15ms | 0.11ms | 0.01ms | N/A | 14.80x | 9.69x |
|
|
331
|
-
| aggregate sum/avg | 0.27ms | 0.25ms | 0.15ms | N/A | 1.82x | 1.62x |
|
|
332
|
-
| aggregate where | 0.25ms | 0.26ms | 0.15ms | N/A | 1.66x | 1.73x |
|
|
333
|
-
| aggregate min/max | 0.28ms | 0.25ms | 0.16ms | N/A | 1.80x | 1.52x |
|
|
334
|
-
| aggregate complete | 0.39ms | 0.34ms | 0.21ms | N/A | 1.81x | 1.61x |
|
|
335
|
-
| groupBy | 0.56ms | 0.53ms | 0.44ms | N/A | 1.28x | 1.22x |
|
|
336
|
-
| groupBy count | 0.57ms | 0.57ms | 0.45ms | N/A | 1.28x | 1.27x |
|
|
337
|
-
| groupBy multi | 1.14ms | 1.08ms | 0.95ms | N/A | 1.20x | 1.17x |
|
|
338
|
-
| groupBy having | 0.64ms | 0.64ms | 0.47ms | N/A | 1.37x | 1.32x |
|
|
339
|
-
| groupBy + where | 0.31ms | 0.33ms | 0.18ms | N/A | 1.70x | 1.84x |
|
|
340
|
-
| groupBy aggregates | 0.71ms | 0.66ms | 0.54ms | N/A | 1.32x | 1.23x |
|
|
341
|
-
| groupBy min/max | 0.72ms | 0.70ms | 0.56ms | N/A | 1.29x | 1.25x |
|
|
342
|
-
| include posts | 1.88ms | 1.13ms | 0.90ms | N/A | 2.10x | 1.12x |
|
|
343
|
-
| include profile | 0.32ms | 0.41ms | 0.05ms | N/A | 6.17x | 6.48x |
|
|
344
|
-
| include 3 levels | 1.11ms | 1.08ms | 0.63ms | N/A | 1.77x | 1.86x |
|
|
345
|
-
| include 4 levels | 1.15ms | 1.10ms | 0.42ms | N/A | 2.72x | 2.70x |
|
|
346
|
-
| include + where | 0.77ms | 0.72ms | 0.11ms | N/A | 7.13x | 6.99x |
|
|
347
|
-
| include + select nested | 0.73ms | 0.53ms | 0.83ms | N/A | 0.88x | 0.64x |
|
|
348
|
-
| findMany startsWith | 0.15ms | 0.16ms | 0.02ms | 0.06ms | 6.73x | 6.98x |
|
|
349
|
-
| findMany endsWith | 0.43ms | 0.26ms | 0.04ms | 0.15ms | 9.74x | 5.22x |
|
|
350
|
-
| findMany NOT contains | 0.45ms | 0.28ms | 0.04ms | 0.11ms | 11.65x | 6.57x |
|
|
351
|
-
| findMany < | 21.60ms | 8.27ms | 1.88ms | 4.07ms | 11.49x | 4.24x |
|
|
352
|
-
| findMany <= | 22.34ms | 8.50ms | 1.97ms | 4.40ms | 11.36x | 4.25x |
|
|
353
|
-
| findMany > | 11.54ms | 4.33ms | 0.94ms | 2.13ms | 12.22x | 4.17x |
|
|
354
|
-
| findMany NOT IN | 0.42ms | 0.28ms | 0.04ms | 0.12ms | 10.40x | 6.23x |
|
|
355
|
-
| findMany isNot null | 0.45ms | 0.27ms | 0.03ms | 0.11ms | 13.03x | 6.91x |
|
|
356
|
-
| orderBy multi-field | 0.66ms | 0.59ms | 0.37ms | 0.43ms | 1.78x | 1.67x |
|
|
357
|
-
| distinct status | 10.61ms | 6.79ms | 4.09ms | N/A | 2.59x | 1.53x |
|
|
358
|
-
| distinct multi | 11.66ms | 7.12ms | 5.09ms | N/A | 2.29x | 1.34x |
|
|
359
|
-
| cursor pagination | 0.21ms | 0.26ms | 0.04ms | N/A | 4.60x | 5.52x |
|
|
360
|
-
| select + include | 0.51ms | 0.43ms | 0.04ms | N/A | 13.19x | 11.31x |
|
|
361
|
-
| \_count relation | 0.62ms | 0.46ms | 0.32ms | N/A | 1.93x | 1.44x |
|
|
362
|
-
| \_count multi-relation | 0.14ms | 0.17ms | 0.04ms | N/A | 3.22x | 4.09x |
|
|
363
|
-
|
|
364
|
-
</details>
|
|
365
|
-
|
|
366
|
-
> **Note:** Benchmarks run on MacBook Pro M1 with PostgreSQL 15 and SQLite 3.43. Results vary based on database config, indexes, query complexity, and hardware. Run your own benchmarks for accurate measurements.
|
|
367
|
-
|
|
368
|
-
## What Gets Faster
|
|
369
|
-
|
|
370
|
-
**Accelerated (via raw SQL):**
|
|
371
|
-
|
|
372
|
-
- β
`findMany`, `findFirst`, `findUnique`
|
|
373
|
-
- β
`count`
|
|
374
|
-
- β
`aggregate` (\_count, \_sum, \_avg, \_min, \_max)
|
|
375
|
-
- β
`groupBy` with having clauses
|
|
376
|
-
- β
`$batch` - multiple queries in one database round trip
|
|
377
|
-
|
|
378
|
-
**Unchanged (still uses Prisma):**
|
|
379
|
-
|
|
380
|
-
- `create`, `update`, `delete`, `upsert`
|
|
381
|
-
- `createMany`, `updateMany`, `deleteMany`
|
|
382
|
-
- Transactions (`$transaction`)
|
|
383
|
-
- Middleware
|
|
184
|
+
- `findMany`
|
|
185
|
+
- `findFirst`
|
|
186
|
+
- `findUnique`
|
|
187
|
+
- `count`
|
|
188
|
+
- `aggregate`
|
|
189
|
+
- `groupBy`
|
|
190
|
+
- `$batch` for PostgreSQL
|
|
384
191
|
|
|
385
|
-
|
|
192
|
+
### Not accelerated
|
|
386
193
|
|
|
387
|
-
|
|
194
|
+
These continue to run through Prisma:
|
|
388
195
|
|
|
389
|
-
|
|
196
|
+
- `create`
|
|
197
|
+
- `update`
|
|
198
|
+
- `delete`
|
|
199
|
+
- `upsert`
|
|
200
|
+
- `createMany`
|
|
201
|
+
- `updateMany`
|
|
202
|
+
- `deleteMany`
|
|
390
203
|
|
|
391
|
-
|
|
392
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
204
|
+
### Fallback behavior
|
|
393
205
|
|
|
394
|
-
|
|
395
|
-
speedExtension({
|
|
396
|
-
postgres: sql,
|
|
397
|
-
debug: true, // Logs SQL for every query
|
|
398
|
-
}),
|
|
399
|
-
) as SpeedClient<typeof PrismaClient>
|
|
400
|
-
```
|
|
206
|
+
If a query shape is unsupported or cannot be accelerated safely, the extension falls back to Prisma instead of returning incorrect results.
|
|
401
207
|
|
|
402
|
-
|
|
208
|
+
Enable `debug: true` to see generated SQL and fallback behavior.
|
|
403
209
|
|
|
404
|
-
|
|
210
|
+
## Features
|
|
405
211
|
|
|
406
|
-
|
|
407
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
212
|
+
### 1) Runtime SQL generation
|
|
408
213
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
sql: info.sql,
|
|
421
|
-
})
|
|
422
|
-
}
|
|
423
|
-
},
|
|
424
|
-
}),
|
|
425
|
-
) as SpeedClient<typeof PrismaClient>
|
|
214
|
+
Any supported read query can be converted from Prisma args into SQL at runtime.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const users = await prisma.user.findMany({
|
|
218
|
+
where: {
|
|
219
|
+
status: 'ACTIVE',
|
|
220
|
+
email: { contains: '@example.com' },
|
|
221
|
+
},
|
|
222
|
+
orderBy: { createdAt: 'desc' },
|
|
223
|
+
take: 20,
|
|
224
|
+
})
|
|
426
225
|
```
|
|
427
226
|
|
|
428
|
-
|
|
227
|
+
### 2) Prebaked hot queries with `@optimize`
|
|
429
228
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
229
|
+
For the hottest query shapes, you can prebake SQL at generate time.
|
|
230
|
+
|
|
231
|
+
```prisma
|
|
232
|
+
/// @optimize {
|
|
233
|
+
/// "method": "findMany",
|
|
234
|
+
/// "query": {
|
|
235
|
+
/// "where": { "status": "ACTIVE" },
|
|
236
|
+
/// "orderBy": { "createdAt": "desc" },
|
|
237
|
+
/// "skip": "$skip",
|
|
238
|
+
/// "take": "$take"
|
|
239
|
+
/// }
|
|
240
|
+
/// }
|
|
241
|
+
model User {
|
|
242
|
+
id Int @id @default(autoincrement())
|
|
243
|
+
email String @unique
|
|
244
|
+
status String
|
|
245
|
+
createdAt DateTime @default(now())
|
|
438
246
|
}
|
|
439
247
|
```
|
|
440
248
|
|
|
441
|
-
|
|
249
|
+
At runtime:
|
|
250
|
+
|
|
251
|
+
- matching query shape β prebaked SQL
|
|
252
|
+
- non-matching query shape β runtime SQL generation
|
|
253
|
+
|
|
254
|
+
### 3) PostgreSQL batch queries
|
|
255
|
+
|
|
256
|
+
`$batch` combines multiple independent read queries into one round trip.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
const results = await prisma.$batch((batch) => ({
|
|
260
|
+
users: batch.user.findMany({ where: { status: 'ACTIVE' } }),
|
|
261
|
+
posts: batch.post.count(),
|
|
262
|
+
stats: batch.task.aggregate({ _count: true }),
|
|
263
|
+
}))
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 4) Include and relation reduction
|
|
267
|
+
|
|
268
|
+
For supported include trees, `prisma-sql` can execute flat SQL and reduce rows back into Prisma-like nested results.
|
|
269
|
+
|
|
270
|
+
### 5) Aggregate result type handling
|
|
271
|
+
|
|
272
|
+
Aggregates are mapped back to Prisma-style value types instead of flattening everything into strings or plain numbers.
|
|
273
|
+
|
|
274
|
+
That includes preserving types like:
|
|
275
|
+
|
|
276
|
+
- `Decimal`
|
|
277
|
+
- `BigInt`
|
|
278
|
+
- `DateTime`
|
|
279
|
+
- `_count`
|
|
280
|
+
|
|
281
|
+
## Query examples
|
|
442
282
|
|
|
443
283
|
### Filters
|
|
444
284
|
|
|
445
|
-
```
|
|
446
|
-
// Comparisons
|
|
285
|
+
```ts
|
|
447
286
|
{ age: { gt: 18, lte: 65 } }
|
|
448
287
|
{ status: { in: ['ACTIVE', 'PENDING'] } }
|
|
449
288
|
{ status: { notIn: ['DELETED'] } }
|
|
450
289
|
|
|
451
|
-
// String operations
|
|
452
290
|
{ email: { contains: '@example.com' } }
|
|
453
291
|
{ email: { startsWith: 'user' } }
|
|
454
292
|
{ email: { endsWith: '.com' } }
|
|
455
293
|
{ email: { contains: 'EXAMPLE', mode: 'insensitive' } }
|
|
456
294
|
|
|
457
|
-
// Boolean logic
|
|
458
295
|
{ AND: [{ status: 'ACTIVE' }, { verified: true }] }
|
|
459
296
|
{ OR: [{ role: 'ADMIN' }, { role: 'MODERATOR' }] }
|
|
460
297
|
{ NOT: { status: 'DELETED' } }
|
|
461
298
|
|
|
462
|
-
// Null checks
|
|
463
299
|
{ deletedAt: null }
|
|
464
300
|
{ deletedAt: { not: null } }
|
|
465
301
|
```
|
|
466
302
|
|
|
467
303
|
### Relations
|
|
468
304
|
|
|
469
|
-
```
|
|
470
|
-
// Include relations
|
|
305
|
+
```ts
|
|
471
306
|
{
|
|
472
307
|
include: {
|
|
473
308
|
posts: true,
|
|
474
|
-
profile: true
|
|
309
|
+
profile: true,
|
|
475
310
|
}
|
|
476
311
|
}
|
|
312
|
+
```
|
|
477
313
|
|
|
478
|
-
|
|
314
|
+
```ts
|
|
479
315
|
{
|
|
480
316
|
include: {
|
|
481
317
|
posts: {
|
|
482
|
-
include: { comments: true },
|
|
483
318
|
where: { published: true },
|
|
484
319
|
orderBy: { createdAt: 'desc' },
|
|
485
|
-
take: 5
|
|
486
|
-
|
|
487
|
-
|
|
320
|
+
take: 5,
|
|
321
|
+
include: {
|
|
322
|
+
comments: true,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
488
326
|
}
|
|
327
|
+
```
|
|
489
328
|
|
|
490
|
-
|
|
329
|
+
```ts
|
|
491
330
|
{
|
|
492
331
|
where: {
|
|
493
|
-
posts: { some: { published: true } }
|
|
494
|
-
}
|
|
332
|
+
posts: { some: { published: true } },
|
|
333
|
+
},
|
|
495
334
|
}
|
|
335
|
+
```
|
|
496
336
|
|
|
337
|
+
```ts
|
|
497
338
|
{
|
|
498
339
|
where: {
|
|
499
|
-
posts: { every: { published: true } }
|
|
500
|
-
}
|
|
340
|
+
posts: { every: { published: true } },
|
|
341
|
+
},
|
|
501
342
|
}
|
|
343
|
+
```
|
|
502
344
|
|
|
345
|
+
```ts
|
|
503
346
|
{
|
|
504
347
|
where: {
|
|
505
|
-
posts: { none: { published: false } }
|
|
506
|
-
}
|
|
348
|
+
posts: { none: { published: false } },
|
|
349
|
+
},
|
|
507
350
|
}
|
|
508
351
|
```
|
|
509
352
|
|
|
510
|
-
### Pagination
|
|
353
|
+
### Pagination and ordering
|
|
511
354
|
|
|
512
|
-
```
|
|
513
|
-
// Basic pagination
|
|
355
|
+
```ts
|
|
514
356
|
{
|
|
515
357
|
take: 10,
|
|
516
358
|
skip: 20,
|
|
517
|
-
orderBy: { createdAt: 'desc' }
|
|
359
|
+
orderBy: { createdAt: 'desc' },
|
|
518
360
|
}
|
|
361
|
+
```
|
|
519
362
|
|
|
520
|
-
|
|
363
|
+
```ts
|
|
521
364
|
{
|
|
522
365
|
cursor: { id: 100 },
|
|
523
|
-
take: 10,
|
|
524
366
|
skip: 1,
|
|
525
|
-
|
|
367
|
+
take: 10,
|
|
368
|
+
orderBy: { id: 'asc' },
|
|
526
369
|
}
|
|
370
|
+
```
|
|
527
371
|
|
|
528
|
-
|
|
372
|
+
```ts
|
|
529
373
|
{
|
|
530
374
|
orderBy: [
|
|
531
375
|
{ status: 'asc' },
|
|
532
376
|
{ priority: 'desc' },
|
|
533
|
-
{ createdAt: 'desc' }
|
|
534
|
-
]
|
|
377
|
+
{ createdAt: 'desc' },
|
|
378
|
+
],
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Composite cursor pagination
|
|
383
|
+
|
|
384
|
+
For composite cursors, use an `orderBy` that starts with the cursor fields in the same order.
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
{
|
|
388
|
+
cursor: { tenantId: 10, id: 500 },
|
|
389
|
+
skip: 1,
|
|
390
|
+
take: 20,
|
|
391
|
+
orderBy: [
|
|
392
|
+
{ tenantId: 'asc' },
|
|
393
|
+
{ id: 'asc' },
|
|
394
|
+
],
|
|
535
395
|
}
|
|
536
396
|
```
|
|
537
397
|
|
|
538
|
-
|
|
398
|
+
This matches keyset pagination expectations and avoids unstable page boundaries.
|
|
539
399
|
|
|
540
|
-
|
|
541
|
-
// Count
|
|
542
|
-
await prisma.user.count({ where: { status: 'ACTIVE' } })
|
|
400
|
+
### Aggregates
|
|
543
401
|
|
|
544
|
-
|
|
402
|
+
```ts
|
|
403
|
+
await prisma.user.count({
|
|
404
|
+
where: { status: 'ACTIVE' },
|
|
405
|
+
})
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
```ts
|
|
545
409
|
await prisma.task.aggregate({
|
|
546
410
|
where: { status: 'DONE' },
|
|
547
411
|
_count: { _all: true },
|
|
@@ -550,8 +414,9 @@ await prisma.task.aggregate({
|
|
|
550
414
|
_min: { startedAt: true },
|
|
551
415
|
_max: { completedAt: true },
|
|
552
416
|
})
|
|
417
|
+
```
|
|
553
418
|
|
|
554
|
-
|
|
419
|
+
```ts
|
|
555
420
|
await prisma.task.groupBy({
|
|
556
421
|
by: ['status', 'priority'],
|
|
557
422
|
_count: { _all: true },
|
|
@@ -564,393 +429,347 @@ await prisma.task.groupBy({
|
|
|
564
429
|
})
|
|
565
430
|
```
|
|
566
431
|
|
|
567
|
-
##
|
|
432
|
+
## Cardinality planner
|
|
568
433
|
|
|
569
|
-
|
|
434
|
+
The cardinality planner is the piece that decides how relation-heavy reads should be executed for best performance.
|
|
570
435
|
|
|
571
|
-
|
|
436
|
+
In practice, it helps choose between strategies such as:
|
|
572
437
|
|
|
573
|
-
|
|
438
|
+
- direct joins
|
|
439
|
+
- lateral/subquery-style fetches
|
|
440
|
+
- flat row expansion + reducer
|
|
441
|
+
- segmented follow-up loading for high fan-out relations
|
|
574
442
|
|
|
575
|
-
|
|
443
|
+
This matters because the fastest strategy depends on **cardinality**, not just query shape.
|
|
576
444
|
|
|
577
|
-
|
|
578
|
-
βββββββββββ Query 1 ββββββββββββ
|
|
579
|
-
β App β βββββββββββββββββββΆβ Database β
|
|
580
|
-
β β ββββββββββββββββββββ β
|
|
581
|
-
β β Query 2 β β
|
|
582
|
-
β β βββββββββββββββββββΆβ β
|
|
583
|
-
β β ββββββββββββββββββββ β
|
|
584
|
-
β β Query 3 β β
|
|
585
|
-
β β βββββββββββββββββββΆβ β
|
|
586
|
-
β β ββββββββββββββββββββ β
|
|
587
|
-
βββββββββββ ββββββββββββ
|
|
588
|
-
Total: ~3ms (3 round trips Γ ~1ms each)
|
|
589
|
-
```
|
|
445
|
+
A `profile` include behaves very differently from a `posts.comments.likes` include.
|
|
590
446
|
|
|
591
|
-
|
|
447
|
+
### Why it matters
|
|
592
448
|
|
|
593
|
-
|
|
594
|
-
βββββββββββ Combined Query ββββββββββββ
|
|
595
|
-
β App β βββββββββββββββββββΆβ Database β
|
|
596
|
-
β β ββββββββββββββββββββ β
|
|
597
|
-
βββββββββββ ββββββββββββ
|
|
598
|
-
Total: ~1ms (1 round trip with all queries)
|
|
599
|
-
```
|
|
449
|
+
A naive join strategy can explode row counts:
|
|
600
450
|
|
|
601
|
-
|
|
451
|
+
- `User -> Profile` is usually low fan-out
|
|
452
|
+
- `User -> Posts -> Comments` can multiply rows aggressively
|
|
453
|
+
- `Organization -> Users -> Sessions -> Events` can become huge very quickly
|
|
602
454
|
|
|
603
|
-
|
|
604
|
-
const dashboard = await prisma.$batch((batch) => ({
|
|
605
|
-
// Organization stats
|
|
606
|
-
totalOrgs: batch.organization.count(),
|
|
607
|
-
activeOrgs: batch.organization.count({
|
|
608
|
-
where: { status: 'ACTIVE' },
|
|
609
|
-
}),
|
|
455
|
+
The planner tries to keep read amplification under control.
|
|
610
456
|
|
|
611
|
-
|
|
612
|
-
totalUsers: batch.user.count(),
|
|
613
|
-
activeUsers: batch.user.count({
|
|
614
|
-
where: { status: 'ACTIVE' },
|
|
615
|
-
}),
|
|
457
|
+
### Best setup for the planner
|
|
616
458
|
|
|
617
|
-
|
|
618
|
-
recentProjects: batch.project.findMany({
|
|
619
|
-
take: 5,
|
|
620
|
-
orderBy: { createdAt: 'desc' },
|
|
621
|
-
include: { organization: true },
|
|
622
|
-
}),
|
|
459
|
+
To get the best results, prepare your schema and indexes so the planner can make good choices.
|
|
623
460
|
|
|
624
|
-
|
|
625
|
-
taskStats: batch.task.aggregate({
|
|
626
|
-
_count: true,
|
|
627
|
-
_avg: { estimatedHours: true },
|
|
628
|
-
where: { status: 'IN_PROGRESS' },
|
|
629
|
-
}),
|
|
630
|
-
}))
|
|
461
|
+
#### 1) Model real cardinality accurately
|
|
631
462
|
|
|
632
|
-
|
|
633
|
-
console.log(`Active users: ${dashboard.activeUsers}`)
|
|
634
|
-
console.log(`Recent projects:`, dashboard.recentProjects)
|
|
635
|
-
console.log(`Avg task hours:`, dashboard.taskStats._avg.estimatedHours)
|
|
636
|
-
```
|
|
463
|
+
Use correct relation fields and uniqueness constraints.
|
|
637
464
|
|
|
638
|
-
|
|
465
|
+
Good examples:
|
|
639
466
|
|
|
640
|
-
|
|
467
|
+
```prisma
|
|
468
|
+
model User {
|
|
469
|
+
id Int @id @default(autoincrement())
|
|
470
|
+
profile Profile?
|
|
471
|
+
posts Post[]
|
|
472
|
+
}
|
|
641
473
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
474
|
+
model Profile {
|
|
475
|
+
id Int @id @default(autoincrement())
|
|
476
|
+
userId Int @unique
|
|
477
|
+
user User @relation(fields: [userId], references: [id])
|
|
478
|
+
}
|
|
645
479
|
|
|
646
|
-
|
|
480
|
+
model Post {
|
|
481
|
+
id Int @id @default(autoincrement())
|
|
482
|
+
authorId Int
|
|
483
|
+
author User @relation(fields: [authorId], references: [id])
|
|
647
484
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
485
|
+
@@index([authorId])
|
|
486
|
+
}
|
|
487
|
+
```
|
|
651
488
|
|
|
652
|
-
|
|
489
|
+
Why this helps:
|
|
653
490
|
|
|
654
|
-
-
|
|
655
|
-
-
|
|
656
|
-
- Parse: 0.09ms
|
|
657
|
-
- **Total: 4.71ms for 45 queries** β‘
|
|
491
|
+
- `@unique` on one-to-one foreign keys tells the planner the relation is bounded
|
|
492
|
+
- indexes on one-to-many foreign keys make follow-up or segmented loading cheap
|
|
658
493
|
|
|
659
|
-
|
|
494
|
+
#### 2) Index every foreign key used in includes and relation filters
|
|
660
495
|
|
|
661
|
-
|
|
496
|
+
At minimum, index:
|
|
662
497
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
batch_1 AS (SELECT count(*)::int AS "_count._all" FROM "organizations" WHERE status = $1),
|
|
668
|
-
batch_2 AS (SELECT count(*)::int AS "_count._all" FROM "users"),
|
|
669
|
-
batch_3 AS (SELECT count(*)::int AS "_count._all" FROM "users" WHERE status = $2),
|
|
670
|
-
batch_4 AS (SELECT * FROM "projects" ORDER BY created_at DESC LIMIT 5),
|
|
671
|
-
batch_5 AS (SELECT count(*)::int, avg(estimated_hours) FROM "tasks" WHERE status = $3)
|
|
672
|
-
SELECT
|
|
673
|
-
(SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_0 t) AS k0,
|
|
674
|
-
(SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_1 t) AS k1,
|
|
675
|
-
(SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_2 t) AS k2,
|
|
676
|
-
(SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_3 t) AS k3,
|
|
677
|
-
(SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_4 t) AS k4,
|
|
678
|
-
(SELECT COALESCE(json_agg(row_to_json(t)), '[]'::json) FROM batch_5 t) AS k5
|
|
679
|
-
```
|
|
498
|
+
- all `@relation(fields: [...])` foreign keys on the child side
|
|
499
|
+
- fields used in nested `where`
|
|
500
|
+
- fields used in nested `orderBy`
|
|
501
|
+
- fields used in cursor pagination
|
|
680
502
|
|
|
681
|
-
|
|
503
|
+
Example:
|
|
682
504
|
|
|
683
|
-
|
|
505
|
+
```prisma
|
|
506
|
+
model Comment {
|
|
507
|
+
id Int @id @default(autoincrement())
|
|
508
|
+
postId Int
|
|
509
|
+
createdAt DateTime @default(now())
|
|
510
|
+
published Boolean @default(false)
|
|
684
511
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}))
|
|
512
|
+
post Post @relation(fields: [postId], references: [id])
|
|
513
|
+
|
|
514
|
+
@@index([postId])
|
|
515
|
+
@@index([postId, createdAt])
|
|
516
|
+
@@index([postId, published])
|
|
517
|
+
}
|
|
692
518
|
```
|
|
693
519
|
|
|
694
|
-
|
|
520
|
+
#### 3) Prefer deterministic nested ordering
|
|
521
|
+
|
|
522
|
+
When including collections, always provide a stable order when practical.
|
|
695
523
|
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
524
|
+
```ts
|
|
525
|
+
const users = await prisma.user.findMany({
|
|
526
|
+
include: {
|
|
527
|
+
posts: {
|
|
528
|
+
orderBy: { createdAt: 'desc' },
|
|
529
|
+
take: 5,
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
})
|
|
703
533
|
```
|
|
704
534
|
|
|
705
|
-
|
|
535
|
+
That helps both the planner and the reducer keep result shapes predictable.
|
|
706
536
|
|
|
707
|
-
|
|
708
|
-
- β
`findFirst` - fetch first matching record
|
|
709
|
-
- β
`findUnique` - fetch by unique field
|
|
710
|
-
- β
`count` - count records (with special optimization)
|
|
711
|
-
- β
`aggregate` - compute aggregations
|
|
712
|
-
- β
`groupBy` - group and aggregate
|
|
537
|
+
### What to configure
|
|
713
538
|
|
|
714
|
-
|
|
539
|
+
Use the cardinality planner wherever your generator/runtime exposes it.
|
|
715
540
|
|
|
716
|
-
|
|
717
|
-
// β Wrong - will throw error
|
|
718
|
-
await prisma.$batch(async (batch) => ({
|
|
719
|
-
users: await batch.user.findMany(), // Don't await!
|
|
720
|
-
posts: await batch.post.findMany(), // Don't await!
|
|
721
|
-
}))
|
|
541
|
+
Because config names can differ between versions, the safe rule is:
|
|
722
542
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
posts: batch.post.findMany(), // Return the query
|
|
727
|
-
}))
|
|
728
|
-
```
|
|
543
|
+
- enable the planner in generator/runtime config if your build exposes that switch
|
|
544
|
+
- keep it on for relation-heavy workloads
|
|
545
|
+
- tune any thresholds only after measuring with real production-shaped queries
|
|
729
546
|
|
|
730
|
-
|
|
547
|
+
If your project has planner thresholds, start conservatively:
|
|
731
548
|
|
|
732
|
-
|
|
733
|
-
|
|
549
|
+
- prefer bounded strategies for one-to-one and unique includes
|
|
550
|
+
- prefer segmented or reduced strategies for one-to-many and many-to-many
|
|
551
|
+
- lower thresholds for deep includes with large child tables
|
|
552
|
+
- raise thresholds only after verifying lower fan-out in production data
|
|
734
553
|
|
|
735
|
-
|
|
736
|
-
const prisma = basePrisma.$extends(
|
|
737
|
-
speedExtension({ postgres: sql }),
|
|
738
|
-
) as SpeedClient<typeof basePrisma>
|
|
554
|
+
### How to verify the planner is helping
|
|
739
555
|
|
|
740
|
-
|
|
741
|
-
const results = await prisma.$batch((batch) => ({
|
|
742
|
-
users: batch.user.findMany({ select: { id: true, email: true } }),
|
|
743
|
-
count: batch.post.count(),
|
|
744
|
-
}))
|
|
556
|
+
Use `debug` and `onQuery`.
|
|
745
557
|
|
|
746
|
-
|
|
747
|
-
results.users[0].email // string
|
|
748
|
-
results.count // number
|
|
749
|
-
```
|
|
558
|
+
Look for:
|
|
750
559
|
|
|
751
|
-
|
|
560
|
+
- large latency spikes on include-heavy queries
|
|
561
|
+
- unusually large result sets for a small parent page
|
|
562
|
+
- repeated slow nested includes on high-fanout relations
|
|
752
563
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
564
|
+
```ts
|
|
565
|
+
const prisma = basePrisma.$extends(
|
|
566
|
+
speedExtension({
|
|
567
|
+
postgres: sql,
|
|
568
|
+
debug: true,
|
|
569
|
+
onQuery: (info) => {
|
|
570
|
+
console.log(`${info.model}.${info.method} ${info.duration}ms`)
|
|
571
|
+
console.log(info.sql)
|
|
572
|
+
},
|
|
573
|
+
}),
|
|
574
|
+
) as SpeedClient<typeof basePrisma>
|
|
575
|
+
```
|
|
758
576
|
|
|
759
|
-
|
|
577
|
+
What good results look like:
|
|
760
578
|
|
|
761
|
-
-
|
|
762
|
-
-
|
|
763
|
-
-
|
|
764
|
-
-
|
|
579
|
+
- small parent page stays small in latency
|
|
580
|
+
- bounded child includes remain predictable
|
|
581
|
+
- high-fanout includes stop exploding row counts
|
|
582
|
+
- moving a heavy include into `$batch` or splitting it improves latency materially
|
|
765
583
|
|
|
766
|
-
|
|
584
|
+
## Deployment without database access at build time
|
|
767
585
|
|
|
768
|
-
|
|
586
|
+
The cardinality planner collects relation statistics and roundtrip cost measurements directly from the database during `prisma generate`. In CI/CD pipelines or containerized builds, the database is often unreachable.
|
|
769
587
|
|
|
770
|
-
|
|
588
|
+
### Skip planner during generation
|
|
771
589
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
/// "skip": "$skip",
|
|
777
|
-
/// "take": "$take",
|
|
778
|
-
/// "orderBy": { "createdAt": "desc" },
|
|
779
|
-
/// "where": { "status": "ACTIVE" }
|
|
780
|
-
/// }
|
|
781
|
-
/// }
|
|
782
|
-
/// @optimize { "method": "count", "query": {} }
|
|
783
|
-
model User {
|
|
784
|
-
id Int @id @default(autoincrement())
|
|
785
|
-
email String @unique
|
|
786
|
-
status String
|
|
787
|
-
createdAt DateTime @default(now())
|
|
788
|
-
posts Post[]
|
|
789
|
-
}
|
|
590
|
+
Set `PRISMA_SQL_SKIP_PLANNER=true` to skip stats collection at generate time. The generator will emit default planner values instead.
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
PRISMA_SQL_SKIP_PLANNER=true npx prisma generate
|
|
790
594
|
```
|
|
791
595
|
|
|
792
|
-
|
|
596
|
+
### Collect stats before server start
|
|
597
|
+
|
|
598
|
+
Run `prisma-sql-collect-stats` as a pre-start step, after deployment, when the database is reachable.
|
|
793
599
|
|
|
794
600
|
```bash
|
|
795
|
-
|
|
601
|
+
prisma-sql-collect-stats \
|
|
602
|
+
--output dist/prisma/generated/sql/planner.generated.js \
|
|
603
|
+
--prisma-client dist/prisma/generated/client/index.js
|
|
796
604
|
```
|
|
797
605
|
|
|
798
|
-
|
|
606
|
+
| Flag | Default | Description |
|
|
607
|
+
| ----------------- | -------------------------------------------------- | -------------------------------------------------------------- |
|
|
608
|
+
| `--output` | `./dist/prisma/generated/sql/planner.generated.js` | Path to the generated planner module |
|
|
609
|
+
| `--prisma-client` | `@prisma/client` | Path to the compiled Prisma client (must expose `Prisma.dmmf`) |
|
|
799
610
|
|
|
800
|
-
|
|
801
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
802
|
-
|
|
803
|
-
const prisma = new PrismaClient().$extends(
|
|
804
|
-
speedExtension({ postgres: sql }),
|
|
805
|
-
) as SpeedClient<typeof PrismaClient>
|
|
611
|
+
The script reads `DATABASE_URL` from the environment (supports `.env` via `dotenv`). If the connection fails or times out, it exits silently without blocking startup.
|
|
806
612
|
|
|
807
|
-
|
|
808
|
-
const activeUsers = await prisma.user.findMany({
|
|
809
|
-
where: { status: 'ACTIVE' },
|
|
810
|
-
skip: 0,
|
|
811
|
-
take: 10,
|
|
812
|
-
orderBy: { createdAt: 'desc' },
|
|
813
|
-
})
|
|
613
|
+
### Example scripts
|
|
814
614
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
615
|
+
```json
|
|
616
|
+
{
|
|
617
|
+
"prisma:generate": "PRISMA_SQL_SKIP_PLANNER=true prisma generate",
|
|
618
|
+
"collect-planner-stats": "prisma-sql-collect-stats --output dist/prisma/generated/sql/planner.generated.js --prisma-client dist/prisma/generated/client/index.js",
|
|
619
|
+
"start:production": "yarn collect-planner-stats; node dist/src/index.js"
|
|
620
|
+
}
|
|
819
621
|
```
|
|
820
622
|
|
|
821
|
-
The
|
|
623
|
+
The semicolon (`;`) after `collect-planner-stats` ensures the server starts even if stats collection fails. Use `&&` instead if you want startup to abort on failure.
|
|
822
624
|
|
|
823
|
-
|
|
824
|
-
- Falls back to runtime generation for non-matching queries (still fast)
|
|
825
|
-
- Tracks which queries are prebaked via `onQuery` callback
|
|
625
|
+
### What happens with default planner values
|
|
826
626
|
|
|
827
|
-
|
|
627
|
+
When stats are not collected, the planner uses conservative defaults:
|
|
828
628
|
|
|
829
|
-
|
|
629
|
+
- `roundtripRowEquivalent`: 73
|
|
630
|
+
- `jsonRowFactor`: 1.5
|
|
631
|
+
- `relationStats`: empty (all relations treated as unknown cardinality)
|
|
830
632
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
/// }
|
|
840
|
-
model User {
|
|
841
|
-
id Int @id
|
|
842
|
-
status String
|
|
843
|
-
}
|
|
633
|
+
This means the planner cannot make informed decisions about join strategies. Queries still work correctly β the planner falls back to safe general-purpose strategies β but relation-heavy reads may not use the optimal execution plan.
|
|
634
|
+
|
|
635
|
+
### Timeout control
|
|
636
|
+
|
|
637
|
+
Stats collection has a default timeout of 15 seconds. Override with:
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
PRISMA_SQL_PLANNER_TIMEOUT_MS=5000 yarn collect-planner-stats
|
|
844
641
|
```
|
|
845
642
|
|
|
846
|
-
|
|
643
|
+
### Practical recommendations
|
|
847
644
|
|
|
848
|
-
|
|
849
|
-
generator sql {
|
|
850
|
-
provider = "prisma-sql-generator"
|
|
645
|
+
For best results with the planner:
|
|
851
646
|
|
|
852
|
-
|
|
853
|
-
|
|
647
|
+
1. index all relation keys
|
|
648
|
+
2. encode one-to-one relations with `@unique`
|
|
649
|
+
3. use stable `orderBy`
|
|
650
|
+
4. cap nested collections with `take`
|
|
651
|
+
5. page parents before including deep trees
|
|
652
|
+
6. split unrelated heavy branches into `$batch`
|
|
653
|
+
7. benchmark with real data distributions, not toy fixtures
|
|
854
654
|
|
|
855
|
-
|
|
856
|
-
# output = "./generated/sql"
|
|
655
|
+
## Batch queries
|
|
857
656
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
657
|
+
`$batch` runs multiple independent read queries in one PostgreSQL round trip.
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
const dashboard = await prisma.$batch((batch) => ({
|
|
661
|
+
totalUsers: batch.user.count(),
|
|
662
|
+
activeUsers: batch.user.count({
|
|
663
|
+
where: { status: 'ACTIVE' },
|
|
664
|
+
}),
|
|
665
|
+
recentProjects: batch.project.findMany({
|
|
666
|
+
take: 5,
|
|
667
|
+
orderBy: { createdAt: 'desc' },
|
|
668
|
+
include: { organization: true },
|
|
669
|
+
}),
|
|
670
|
+
taskStats: batch.task.aggregate({
|
|
671
|
+
_count: true,
|
|
672
|
+
_avg: { estimatedHours: true },
|
|
673
|
+
where: { status: 'IN_PROGRESS' },
|
|
674
|
+
}),
|
|
675
|
+
}))
|
|
861
676
|
```
|
|
862
677
|
|
|
863
|
-
###
|
|
678
|
+
### Rules
|
|
864
679
|
|
|
865
|
-
|
|
680
|
+
Do not `await` inside the callback.
|
|
866
681
|
|
|
867
|
-
|
|
868
|
-
import { PrismaClient } from '@prisma/client'
|
|
869
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
870
|
-
import postgres from 'postgres'
|
|
682
|
+
Incorrect:
|
|
871
683
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
)
|
|
684
|
+
```ts
|
|
685
|
+
await prisma.$batch(async (batch) => ({
|
|
686
|
+
users: await batch.user.findMany(),
|
|
687
|
+
}))
|
|
688
|
+
```
|
|
876
689
|
|
|
877
|
-
|
|
690
|
+
Correct:
|
|
878
691
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
692
|
+
```ts
|
|
693
|
+
await prisma.$batch((batch) => ({
|
|
694
|
+
users: batch.user.findMany(),
|
|
695
|
+
}))
|
|
883
696
|
```
|
|
884
697
|
|
|
885
|
-
|
|
698
|
+
### Best use cases
|
|
886
699
|
|
|
887
|
-
|
|
700
|
+
- dashboards
|
|
701
|
+
- analytics summaries
|
|
702
|
+
- counts + page data
|
|
703
|
+
- multiple independent aggregates
|
|
704
|
+
- splitting unrelated heavy reads instead of building one massive include tree
|
|
888
705
|
|
|
889
|
-
|
|
890
|
-
import { createToSQL } from 'prisma-sql'
|
|
891
|
-
import { MODELS } from './generated/sql'
|
|
706
|
+
### Limitations
|
|
892
707
|
|
|
893
|
-
|
|
708
|
+
- PostgreSQL only
|
|
709
|
+
- queries are independent
|
|
710
|
+
- not transactional
|
|
711
|
+
- use `$transaction` when you need transactional guarantees
|
|
894
712
|
|
|
895
|
-
|
|
896
|
-
async fetch(request: Request, env: Env) {
|
|
897
|
-
const { sql, params } = toSQL('User', 'findMany', {
|
|
898
|
-
where: { status: 'ACTIVE' },
|
|
899
|
-
})
|
|
713
|
+
## Configuration
|
|
900
714
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
715
|
+
### Debug logging
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
const prisma = basePrisma.$extends(
|
|
719
|
+
speedExtension({
|
|
720
|
+
postgres: sql,
|
|
721
|
+
debug: true,
|
|
722
|
+
}),
|
|
723
|
+
) as SpeedClient<typeof basePrisma>
|
|
907
724
|
```
|
|
908
725
|
|
|
909
|
-
|
|
726
|
+
### Performance hook
|
|
910
727
|
|
|
911
|
-
|
|
728
|
+
```ts
|
|
729
|
+
const prisma = basePrisma.$extends(
|
|
730
|
+
speedExtension({
|
|
731
|
+
postgres: sql,
|
|
732
|
+
onQuery: (info) => {
|
|
733
|
+
console.log(`${info.model}.${info.method}: ${info.duration}ms`)
|
|
734
|
+
console.log(`prebaked=${info.prebaked}`)
|
|
735
|
+
},
|
|
736
|
+
}),
|
|
737
|
+
) as SpeedClient<typeof basePrisma>
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
The callback receives:
|
|
912
741
|
|
|
742
|
+
```ts
|
|
743
|
+
interface QueryInfo {
|
|
744
|
+
model: string
|
|
745
|
+
method: string
|
|
746
|
+
sql: string
|
|
747
|
+
params: unknown[]
|
|
748
|
+
duration: number
|
|
749
|
+
prebaked: boolean
|
|
750
|
+
}
|
|
913
751
|
```
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
dynamicKeys: []
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
export function speedExtension() { ... }
|
|
936
|
-
|
|
937
|
-
Runtime:
|
|
938
|
-
prisma.user.findMany({ where: { status: 'ACTIVE' } })
|
|
939
|
-
β
|
|
940
|
-
Normalize query β '{"where":{"status":"ACTIVE"}}'
|
|
941
|
-
β
|
|
942
|
-
QUERIES.User.findMany[query] found?
|
|
943
|
-
β
|
|
944
|
-
YES β β‘ Use prebaked SQL (0.03ms overhead)
|
|
945
|
-
β
|
|
946
|
-
NO β π¨ Generate SQL runtime (0.2ms overhead)
|
|
947
|
-
β
|
|
948
|
-
Execute via postgres.js/better-sqlite3
|
|
752
|
+
|
|
753
|
+
## Generator configuration
|
|
754
|
+
|
|
755
|
+
```prisma
|
|
756
|
+
generator sql {
|
|
757
|
+
provider = "prisma-sql-generator"
|
|
758
|
+
|
|
759
|
+
// optional
|
|
760
|
+
// dialect = "postgres"
|
|
761
|
+
|
|
762
|
+
// optional
|
|
763
|
+
// output = "./generated/sql"
|
|
764
|
+
|
|
765
|
+
// optional
|
|
766
|
+
// skipInvalid = "true"
|
|
767
|
+
}
|
|
949
768
|
```
|
|
950
769
|
|
|
951
|
-
|
|
770
|
+
## `@optimize` examples
|
|
952
771
|
|
|
953
|
-
|
|
772
|
+
### Basic prebaked query
|
|
954
773
|
|
|
955
774
|
```prisma
|
|
956
775
|
/// @optimize {
|
|
@@ -965,20 +784,24 @@ model User {
|
|
|
965
784
|
}
|
|
966
785
|
```
|
|
967
786
|
|
|
968
|
-
|
|
787
|
+
### Dynamic parameters
|
|
969
788
|
|
|
970
789
|
```prisma
|
|
971
790
|
/// @optimize {
|
|
972
791
|
/// "method": "findMany",
|
|
973
792
|
/// "query": {
|
|
793
|
+
/// "where": { "status": "$status" },
|
|
974
794
|
/// "skip": "$skip",
|
|
975
|
-
/// "take": "$take"
|
|
976
|
-
/// "orderBy": { "createdAt": "desc" }
|
|
795
|
+
/// "take": "$take"
|
|
977
796
|
/// }
|
|
978
797
|
/// }
|
|
798
|
+
model User {
|
|
799
|
+
id Int @id
|
|
800
|
+
status String
|
|
801
|
+
}
|
|
979
802
|
```
|
|
980
803
|
|
|
981
|
-
|
|
804
|
+
### Nested include
|
|
982
805
|
|
|
983
806
|
```prisma
|
|
984
807
|
/// @optimize {
|
|
@@ -993,298 +816,216 @@ model User {
|
|
|
993
816
|
/// }
|
|
994
817
|
/// }
|
|
995
818
|
/// }
|
|
819
|
+
model User {
|
|
820
|
+
id Int @id
|
|
821
|
+
posts Post[]
|
|
822
|
+
}
|
|
996
823
|
```
|
|
997
824
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
```prisma
|
|
1001
|
-
/// @optimize {
|
|
1002
|
-
/// "method": "findMany",
|
|
1003
|
-
/// "query": {
|
|
1004
|
-
/// "skip": "$skip",
|
|
1005
|
-
/// "take": "$take",
|
|
1006
|
-
/// "orderBy": { "createdAt": "desc" },
|
|
1007
|
-
/// "include": {
|
|
1008
|
-
/// "company": {
|
|
1009
|
-
/// "where": { "deletedAt": null },
|
|
1010
|
-
/// "select": { "id": true, "name": true }
|
|
1011
|
-
/// }
|
|
1012
|
-
/// }
|
|
1013
|
-
/// }
|
|
1014
|
-
/// }
|
|
1015
|
-
```
|
|
1016
|
-
|
|
1017
|
-
## Migration Guide
|
|
1018
|
-
|
|
1019
|
-
### From Prisma Client
|
|
1020
|
-
|
|
1021
|
-
**Before:**
|
|
825
|
+
## Edge usage
|
|
1022
826
|
|
|
1023
|
-
|
|
1024
|
-
const prisma = new PrismaClient()
|
|
1025
|
-
const users = await prisma.user.findMany()
|
|
1026
|
-
```
|
|
1027
|
-
|
|
1028
|
-
**After:**
|
|
827
|
+
### Vercel Edge
|
|
1029
828
|
|
|
1030
|
-
```
|
|
829
|
+
```ts
|
|
1031
830
|
import { PrismaClient } from '@prisma/client'
|
|
1032
831
|
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
1033
832
|
import postgres from 'postgres'
|
|
1034
833
|
|
|
1035
|
-
const sql = postgres(process.env.DATABASE_URL)
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
export const prisma = basePrisma.$extends(
|
|
834
|
+
const sql = postgres(process.env.DATABASE_URL!)
|
|
835
|
+
const prisma = new PrismaClient().$extends(
|
|
1039
836
|
speedExtension({ postgres: sql }),
|
|
1040
|
-
) as SpeedClient<typeof
|
|
1041
|
-
|
|
1042
|
-
const users = await prisma.user.findMany() // Same API, just faster
|
|
1043
|
-
```
|
|
1044
|
-
|
|
1045
|
-
### From Drizzle
|
|
1046
|
-
|
|
1047
|
-
**Before:**
|
|
1048
|
-
|
|
1049
|
-
```typescript
|
|
1050
|
-
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
1051
|
-
import postgres from 'postgres'
|
|
837
|
+
) as SpeedClient<typeof PrismaClient>
|
|
1052
838
|
|
|
1053
|
-
const
|
|
1054
|
-
const db = drizzle(sql)
|
|
839
|
+
export const config = { runtime: 'edge' }
|
|
1055
840
|
|
|
1056
|
-
|
|
1057
|
-
.
|
|
1058
|
-
.
|
|
1059
|
-
|
|
841
|
+
export default async function handler() {
|
|
842
|
+
const users = await prisma.user.findMany()
|
|
843
|
+
return Response.json(users)
|
|
844
|
+
}
|
|
1060
845
|
```
|
|
1061
846
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
```typescript
|
|
1065
|
-
import { PrismaClient } from '@prisma/client'
|
|
1066
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
1067
|
-
import postgres from 'postgres'
|
|
1068
|
-
|
|
1069
|
-
const sql = postgres(DATABASE_URL)
|
|
1070
|
-
const basePrisma = new PrismaClient()
|
|
847
|
+
### Cloudflare Workers
|
|
1071
848
|
|
|
1072
|
-
|
|
1073
|
-
speedExtension({ postgres: sql }),
|
|
1074
|
-
) as SpeedClient<typeof basePrisma>
|
|
849
|
+
Use the standalone SQL generation API.
|
|
1075
850
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
```
|
|
851
|
+
```ts
|
|
852
|
+
import { createToSQL } from 'prisma-sql'
|
|
853
|
+
import { MODELS } from './generated/sql'
|
|
1080
854
|
|
|
1081
|
-
|
|
855
|
+
const toSQL = createToSQL(MODELS, 'sqlite')
|
|
1082
856
|
|
|
1083
|
-
|
|
857
|
+
export default {
|
|
858
|
+
async fetch(request: Request, env: Env) {
|
|
859
|
+
const { sql, params } = toSQL('User', 'findMany', {
|
|
860
|
+
where: { status: 'ACTIVE' },
|
|
861
|
+
})
|
|
1084
862
|
|
|
1085
|
-
|
|
863
|
+
const result = await env.DB.prepare(sql)
|
|
864
|
+
.bind(...params)
|
|
865
|
+
.all()
|
|
1086
866
|
|
|
1087
|
-
|
|
1088
|
-
|
|
867
|
+
return Response.json(result.results)
|
|
868
|
+
},
|
|
869
|
+
}
|
|
870
|
+
```
|
|
1089
871
|
|
|
1090
|
-
|
|
872
|
+
## Performance
|
|
1091
873
|
|
|
1092
|
-
|
|
874
|
+
Performance depends on:
|
|
1093
875
|
|
|
1094
|
-
-
|
|
1095
|
-
-
|
|
1096
|
-
-
|
|
1097
|
-
-
|
|
876
|
+
- database type
|
|
877
|
+
- query shape
|
|
878
|
+
- indexing
|
|
879
|
+
- relation fan-out
|
|
880
|
+
- whether the query is prebaked
|
|
881
|
+
- whether the cardinality planner can choose a bounded strategy
|
|
1098
882
|
|
|
1099
|
-
|
|
883
|
+
Typical gains are strongest when:
|
|
1100
884
|
|
|
1101
|
-
|
|
885
|
+
- Prisma overhead dominates total time
|
|
886
|
+
- includes are moderate but structured well
|
|
887
|
+
- query shapes repeat
|
|
888
|
+
- indexes exist on relation and filter columns
|
|
1102
889
|
|
|
1103
|
-
|
|
1104
|
-
- β
SQLite 3.35+
|
|
1105
|
-
- β MySQL (not yet implemented)
|
|
1106
|
-
- β MongoDB (not applicable - document database)
|
|
1107
|
-
- β SQL Server (not yet implemented)
|
|
1108
|
-
- β CockroachDB (not yet tested)
|
|
890
|
+
Run your own benchmarks on production-shaped data.
|
|
1109
891
|
|
|
1110
892
|
## Troubleshooting
|
|
1111
893
|
|
|
1112
|
-
###
|
|
894
|
+
### `speedExtension requires postgres or sqlite client`
|
|
1113
895
|
|
|
1114
|
-
|
|
896
|
+
Pass a database-native client to the generated extension.
|
|
1115
897
|
|
|
1116
|
-
```
|
|
1117
|
-
|
|
1118
|
-
import postgres from 'postgres'
|
|
1119
|
-
|
|
1120
|
-
const sql = postgres(process.env.DATABASE_URL)
|
|
1121
|
-
const prisma = new PrismaClient().$extends(
|
|
1122
|
-
speedExtension({ postgres: sql }), // β
Pass postgres client
|
|
1123
|
-
) as SpeedClient<typeof PrismaClient>
|
|
898
|
+
```ts
|
|
899
|
+
const prisma = new PrismaClient().$extends(speedExtension({ postgres: sql }))
|
|
1124
900
|
```
|
|
1125
901
|
|
|
1126
|
-
###
|
|
902
|
+
### Generated dialect mismatch
|
|
903
|
+
|
|
904
|
+
If generated code targets PostgreSQL, do not pass SQLite, and vice versa.
|
|
1127
905
|
|
|
1128
|
-
|
|
906
|
+
Override dialect in the generator if needed.
|
|
1129
907
|
|
|
1130
908
|
```prisma
|
|
1131
909
|
generator sql {
|
|
1132
910
|
provider = "prisma-sql-generator"
|
|
1133
|
-
dialect = "postgres"
|
|
911
|
+
dialect = "postgres"
|
|
1134
912
|
}
|
|
1135
913
|
```
|
|
1136
914
|
|
|
1137
|
-
###
|
|
1138
|
-
|
|
1139
|
-
Enable debug mode and compare SQL:
|
|
915
|
+
### Results differ from Prisma
|
|
1140
916
|
|
|
1141
|
-
|
|
1142
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
917
|
+
Turn on debug logging and compare generated SQL with Prisma query logs.
|
|
1143
918
|
|
|
919
|
+
```ts
|
|
1144
920
|
const prisma = new PrismaClient().$extends(
|
|
1145
921
|
speedExtension({
|
|
1146
922
|
postgres: sql,
|
|
1147
|
-
debug: true,
|
|
923
|
+
debug: true,
|
|
1148
924
|
}),
|
|
1149
|
-
)
|
|
925
|
+
)
|
|
1150
926
|
```
|
|
1151
927
|
|
|
1152
|
-
|
|
928
|
+
If behavior differs, open an issue with:
|
|
1153
929
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
930
|
+
- schema excerpt
|
|
931
|
+
- Prisma query
|
|
932
|
+
- generated SQL
|
|
933
|
+
- expected result
|
|
934
|
+
- actual result
|
|
935
|
+
|
|
936
|
+
### Performance is worse on a relation-heavy query
|
|
1157
937
|
|
|
1158
|
-
|
|
938
|
+
Check these first:
|
|
1159
939
|
|
|
1160
|
-
|
|
940
|
+
- missing foreign-key indexes
|
|
941
|
+
- deep unbounded includes
|
|
942
|
+
- no nested `take`
|
|
943
|
+
- unstable or missing `orderBy`
|
|
944
|
+
- high-fanout relation trees that should be split into `$batch`
|
|
1161
945
|
|
|
1162
|
-
|
|
946
|
+
### Connection pool exhaustion
|
|
1163
947
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
948
|
+
Increase `postgres.js` pool size if needed.
|
|
949
|
+
|
|
950
|
+
```ts
|
|
951
|
+
const sql = postgres(process.env.DATABASE_URL!, {
|
|
952
|
+
max: 50,
|
|
1167
953
|
})
|
|
1168
954
|
```
|
|
1169
955
|
|
|
1170
|
-
|
|
956
|
+
## Limitations
|
|
1171
957
|
|
|
1172
|
-
|
|
958
|
+
### Partially supported
|
|
1173
959
|
|
|
1174
|
-
-
|
|
1175
|
-
-
|
|
1176
|
-
- Aggregations on unindexed fields
|
|
960
|
+
- basic array operators
|
|
961
|
+
- basic JSON path filtering
|
|
1177
962
|
|
|
1178
|
-
|
|
963
|
+
### Not yet supported
|
|
1179
964
|
|
|
1180
|
-
|
|
1181
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
965
|
+
These should fall back to Prisma:
|
|
1182
966
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
console.log(`${info.method} took ${info.duration}ms`)
|
|
1188
|
-
},
|
|
1189
|
-
}),
|
|
1190
|
-
) as SpeedClient<typeof PrismaClient>
|
|
1191
|
-
```
|
|
967
|
+
- full-text `search`
|
|
968
|
+
- composite/document-style embedded types
|
|
969
|
+
- vendor-specific extensions not yet modeled by the SQL builder
|
|
970
|
+
- some advanced `groupBy` edge cases
|
|
1192
971
|
|
|
1193
972
|
## FAQ
|
|
1194
973
|
|
|
1195
|
-
**
|
|
1196
|
-
|
|
974
|
+
**Do I still need Prisma?**
|
|
975
|
+
Yes. Prisma remains the source of truth for schema, migrations, types, writes, and fallback behavior.
|
|
1197
976
|
|
|
1198
|
-
**
|
|
1199
|
-
|
|
977
|
+
**Does this replace Prisma Client?**
|
|
978
|
+
No. It extends Prisma Client.
|
|
1200
979
|
|
|
1201
|
-
**
|
|
1202
|
-
|
|
980
|
+
**What gets accelerated?**
|
|
981
|
+
Supported read queries only.
|
|
1203
982
|
|
|
1204
|
-
**
|
|
1205
|
-
|
|
983
|
+
**What about writes?**
|
|
984
|
+
Writes continue through Prisma.
|
|
1206
985
|
|
|
1207
|
-
**
|
|
1208
|
-
|
|
986
|
+
**Do I need `@optimize`?**
|
|
987
|
+
No. It is optional. It only reduces the overhead of repeated hot query shapes.
|
|
1209
988
|
|
|
1210
|
-
**
|
|
1211
|
-
|
|
989
|
+
**Does `$batch` work with SQLite?**
|
|
990
|
+
Not currently.
|
|
1212
991
|
|
|
1213
|
-
**
|
|
1214
|
-
|
|
992
|
+
**Is it safe to use in production?**
|
|
993
|
+
Use it the same way you would adopt any query-path optimization layer: benchmark it on real data, compare against Prisma for parity, and keep Prisma fallback enabled for unsupported cases.
|
|
1215
994
|
|
|
1216
|
-
|
|
1217
|
-
A: Runtime mode: ~0.2ms per query. Generator mode with `@optimize`: ~0.03ms for prebaked queries. Still 2-7x faster than Prisma overall.
|
|
995
|
+
## Migration
|
|
1218
996
|
|
|
1219
|
-
|
|
1220
|
-
A: No! The generator works without them. `@optimize` directives are optional for squeezing out the last bit of performance on your hottest queries.
|
|
997
|
+
### Before
|
|
1221
998
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
**Q: Does batch work with SQLite?**
|
|
1226
|
-
A: Not yet. `$batch` is currently PostgreSQL only. SQLite support coming soon.
|
|
1227
|
-
|
|
1228
|
-
## Examples
|
|
1229
|
-
|
|
1230
|
-
- [Generator Mode Example](./examples/generator-mode) - Complete working example
|
|
1231
|
-
- [PostgreSQL E2E Tests](./tests/e2e/postgres.test.ts) - Comprehensive query examples
|
|
1232
|
-
- [SQLite E2E Tests](./tests/e2e/sqlite.e2e.test.ts) - SQLite-specific queries
|
|
1233
|
-
- [Batch Query Tests](./tests/sql-injection/batch-transaction.test.ts) - Batch query examples
|
|
1234
|
-
|
|
1235
|
-
To run examples locally:
|
|
1236
|
-
|
|
1237
|
-
```bash
|
|
1238
|
-
git clone https://github.com/multipliedtwice/prisma-to-sql
|
|
1239
|
-
cd prisma-sql
|
|
1240
|
-
npm install
|
|
1241
|
-
npm test
|
|
999
|
+
```ts
|
|
1000
|
+
const prisma = new PrismaClient()
|
|
1001
|
+
const users = await prisma.user.findMany()
|
|
1242
1002
|
```
|
|
1243
1003
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
Benchmark your own queries:
|
|
1004
|
+
### After
|
|
1247
1005
|
|
|
1248
|
-
```
|
|
1006
|
+
```ts
|
|
1007
|
+
import { PrismaClient } from '@prisma/client'
|
|
1249
1008
|
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
1009
|
+
import postgres from 'postgres'
|
|
1250
1010
|
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
const prisma = new PrismaClient().$extends(
|
|
1254
|
-
speedExtension({
|
|
1255
|
-
postgres: sql,
|
|
1256
|
-
onQuery: (info) => {
|
|
1257
|
-
queries.push({
|
|
1258
|
-
name: `${info.model}.${info.method}`,
|
|
1259
|
-
duration: info.duration,
|
|
1260
|
-
prebaked: info.prebaked,
|
|
1261
|
-
})
|
|
1262
|
-
},
|
|
1263
|
-
}),
|
|
1264
|
-
) as SpeedClient<typeof PrismaClient>
|
|
1011
|
+
const sql = postgres(process.env.DATABASE_URL!)
|
|
1012
|
+
const basePrisma = new PrismaClient()
|
|
1265
1013
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
users: batch.user.count(),
|
|
1270
|
-
posts: batch.post.count(),
|
|
1271
|
-
}))
|
|
1014
|
+
export const prisma = basePrisma.$extends(
|
|
1015
|
+
speedExtension({ postgres: sql }),
|
|
1016
|
+
) as SpeedClient<typeof basePrisma>
|
|
1272
1017
|
|
|
1273
|
-
|
|
1018
|
+
const users = await prisma.user.findMany()
|
|
1274
1019
|
```
|
|
1275
1020
|
|
|
1276
|
-
##
|
|
1277
|
-
|
|
1278
|
-
PRs welcome! Priority areas:
|
|
1021
|
+
## Examples
|
|
1279
1022
|
|
|
1280
|
-
-
|
|
1281
|
-
-
|
|
1282
|
-
-
|
|
1283
|
-
-
|
|
1284
|
-
- Edge runtime compatibility
|
|
1285
|
-
- Documentation improvements
|
|
1023
|
+
- `examples/generator-mode`
|
|
1024
|
+
- `tests/e2e/postgres.test.ts`
|
|
1025
|
+
- `tests/e2e/sqlite.e2e.test.ts`
|
|
1026
|
+
- `tests/sql-injection/batch-transaction.test.ts`
|
|
1286
1027
|
|
|
1287
|
-
|
|
1028
|
+
## Development
|
|
1288
1029
|
|
|
1289
1030
|
```bash
|
|
1290
1031
|
git clone https://github.com/multipliedtwice/prisma-to-sql
|
|
@@ -1294,52 +1035,6 @@ npm run build
|
|
|
1294
1035
|
npm test
|
|
1295
1036
|
```
|
|
1296
1037
|
|
|
1297
|
-
Please ensure:
|
|
1298
|
-
|
|
1299
|
-
- All tests pass (`npm test`)
|
|
1300
|
-
- New features have tests
|
|
1301
|
-
- Types are properly exported
|
|
1302
|
-
- README is updated
|
|
1303
|
-
|
|
1304
|
-
## How It Works
|
|
1305
|
-
|
|
1306
|
-
```
|
|
1307
|
-
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1308
|
-
β prisma.user.findMany({ where: { status: 'ACTIVE' }})β
|
|
1309
|
-
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
|
|
1310
|
-
β
|
|
1311
|
-
βββββββββββββΌβββββββββββ
|
|
1312
|
-
β Generated Extension β
|
|
1313
|
-
β Uses internal MODELSβ
|
|
1314
|
-
βββββββββββββ¬βββββββββββ
|
|
1315
|
-
β
|
|
1316
|
-
βββββββββββββΌβββββββββββ
|
|
1317
|
-
β Check for prebaked β
|
|
1318
|
-
β query in QUERIES β
|
|
1319
|
-
βββββββββββββ¬βββββββββββ
|
|
1320
|
-
β
|
|
1321
|
-
βββββββββββββΌβββββββββββ
|
|
1322
|
-
β Generate SQL β
|
|
1323
|
-
β (if not prebaked) β
|
|
1324
|
-
βββββββββββββ¬βββββββββββ
|
|
1325
|
-
β
|
|
1326
|
-
βββββββββββββΌβββββββββββ
|
|
1327
|
-
β SELECT ... FROM usersβ
|
|
1328
|
-
β WHERE status = $1 β
|
|
1329
|
-
βββββββββββββ¬βββββββββββ
|
|
1330
|
-
β
|
|
1331
|
-
βββββββββββββΌβββββββββββ
|
|
1332
|
-
β Execute via β
|
|
1333
|
-
β postgres.js β β Bypasses Prisma's query engine
|
|
1334
|
-
βββββββββββββ¬βββββββββββ
|
|
1335
|
-
β
|
|
1336
|
-
βββββββββββββΌβββββββββββ
|
|
1337
|
-
β Return results β
|
|
1338
|
-
β (same format as β
|
|
1339
|
-
β Prisma) β
|
|
1340
|
-
ββββββββββββββββββββββββ
|
|
1341
|
-
```
|
|
1342
|
-
|
|
1343
1038
|
## License
|
|
1344
1039
|
|
|
1345
1040
|
MIT
|
|
@@ -1349,8 +1044,3 @@ MIT
|
|
|
1349
1044
|
- [NPM Package](https://www.npmjs.com/package/prisma-sql)
|
|
1350
1045
|
- [GitHub Repository](https://github.com/multipliedtwice/prisma-to-sql)
|
|
1351
1046
|
- [Issue Tracker](https://github.com/multipliedtwice/prisma-to-sql/issues)
|
|
1352
|
-
|
|
1353
|
-
---
|
|
1354
|
-
|
|
1355
|
-
**Made for developers who need Prisma's DX with raw SQL performance.**
|
|
1356
|
-
````
|