prisma-sql 1.76.0 β 1.76.2
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 +2624 -2392
- package/dist/generator.cjs.map +1 -1
- package/dist/generator.js +2624 -2392
- package/dist/generator.js.map +1 -1
- package/dist/index.cjs +3562 -3177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +23 -7
- package/dist/index.d.ts +23 -7
- package/dist/index.js +3562 -3177
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/readme.md +606 -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
|
|
160
|
+
|
|
161
|
+
Apply `speedExtension` last so it sees the final query surface.
|
|
185
162
|
|
|
186
|
-
```
|
|
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
|
+
],
|
|
535
379
|
}
|
|
536
380
|
```
|
|
537
381
|
|
|
538
|
-
###
|
|
382
|
+
### Composite cursor pagination
|
|
539
383
|
|
|
540
|
-
|
|
541
|
-
// Count
|
|
542
|
-
await prisma.user.count({ where: { status: 'ACTIVE' } })
|
|
384
|
+
For composite cursors, use an `orderBy` that starts with the cursor fields in the same order.
|
|
543
385
|
|
|
544
|
-
|
|
386
|
+
```ts
|
|
387
|
+
{
|
|
388
|
+
cursor: { tenantId: 10, id: 500 },
|
|
389
|
+
skip: 1,
|
|
390
|
+
take: 20,
|
|
391
|
+
orderBy: [
|
|
392
|
+
{ tenantId: 'asc' },
|
|
393
|
+
{ id: 'asc' },
|
|
394
|
+
],
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
This matches keyset pagination expectations and avoids unstable page boundaries.
|
|
399
|
+
|
|
400
|
+
### Aggregates
|
|
401
|
+
|
|
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,364 @@ 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
|
+
#### 4) Avoid unbounded deep fan-out in a single query
|
|
713
538
|
|
|
714
|
-
|
|
539
|
+
This is the biggest real-world improvement lever.
|
|
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
|
+
Less ideal:
|
|
722
542
|
|
|
723
|
-
|
|
724
|
-
await prisma
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
543
|
+
```ts
|
|
544
|
+
await prisma.organization.findMany({
|
|
545
|
+
include: {
|
|
546
|
+
users: {
|
|
547
|
+
include: {
|
|
548
|
+
posts: {
|
|
549
|
+
include: {
|
|
550
|
+
comments: true,
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
})
|
|
728
557
|
```
|
|
729
558
|
|
|
730
|
-
|
|
559
|
+
Usually better:
|
|
731
560
|
|
|
732
|
-
|
|
733
|
-
|
|
561
|
+
- page parents
|
|
562
|
+
- cap nested collections with `take`
|
|
563
|
+
- add nested `where`
|
|
564
|
+
- split unrelated heavy branches into `$batch`
|
|
734
565
|
|
|
735
|
-
|
|
736
|
-
const prisma = basePrisma.$extends(
|
|
737
|
-
speedExtension({ postgres: sql }),
|
|
738
|
-
) as SpeedClient<typeof basePrisma>
|
|
566
|
+
Example:
|
|
739
567
|
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
568
|
+
```ts
|
|
569
|
+
const result = await prisma.$batch((batch) => ({
|
|
570
|
+
orgs: batch.organization.findMany({
|
|
571
|
+
take: 20,
|
|
572
|
+
orderBy: { id: 'asc' },
|
|
573
|
+
}),
|
|
574
|
+
recentUsers: batch.user.findMany({
|
|
575
|
+
take: 50,
|
|
576
|
+
orderBy: { createdAt: 'desc' },
|
|
577
|
+
}),
|
|
744
578
|
}))
|
|
745
|
-
|
|
746
|
-
// β
TypeScript autocomplete works
|
|
747
|
-
results.users[0].email // string
|
|
748
|
-
results.count // number
|
|
749
579
|
```
|
|
750
580
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
1. **Dashboard queries** - Load all dashboard data in one call
|
|
754
|
-
2. **Analytics** - Multiple aggregations at once
|
|
755
|
-
3. **Comparison queries** - Compare different time periods
|
|
756
|
-
4. **Multi-tenant data** - Fetch data for multiple tenants
|
|
757
|
-
5. **Search with counts** - Get results + multiple facet counts
|
|
581
|
+
#### 5) Use one-to-one uniqueness where it is actually one-to-one
|
|
758
582
|
|
|
759
|
-
|
|
583
|
+
If the database guarantees one child row, encode that in Prisma.
|
|
760
584
|
|
|
761
|
-
|
|
762
|
-
- Queries run in parallel, not in a transaction
|
|
763
|
-
- Each query must be independent (can't reference results from other queries)
|
|
764
|
-
- For transactional guarantees, use `$transaction` instead
|
|
585
|
+
This can let the planner avoid unnecessarily defensive high-fanout strategies.
|
|
765
586
|
|
|
766
|
-
|
|
587
|
+
#### 6) Keep nested filters sargable
|
|
767
588
|
|
|
768
|
-
|
|
589
|
+
Prefer predicates that use indexed equality/range conditions.
|
|
769
590
|
|
|
770
|
-
|
|
591
|
+
Better:
|
|
771
592
|
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
model User {
|
|
784
|
-
id Int @id @default(autoincrement())
|
|
785
|
-
email String @unique
|
|
786
|
-
status String
|
|
787
|
-
createdAt DateTime @default(now())
|
|
788
|
-
posts Post[]
|
|
593
|
+
```ts
|
|
594
|
+
{
|
|
595
|
+
include: {
|
|
596
|
+
comments: {
|
|
597
|
+
where: {
|
|
598
|
+
postId: 42,
|
|
599
|
+
createdAt: { gte: someDate },
|
|
600
|
+
},
|
|
601
|
+
orderBy: { createdAt: 'desc' },
|
|
602
|
+
},
|
|
603
|
+
},
|
|
789
604
|
}
|
|
790
605
|
```
|
|
791
606
|
|
|
792
|
-
|
|
607
|
+
Less planner-friendly:
|
|
793
608
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
609
|
+
- broad `contains` / `%term%` everywhere
|
|
610
|
+
- unindexed OR-heavy nested filters
|
|
611
|
+
- deep includes without limits
|
|
797
612
|
|
|
798
|
-
|
|
613
|
+
### What to configure
|
|
799
614
|
|
|
800
|
-
|
|
801
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
615
|
+
Use the cardinality planner wherever your generator/runtime exposes it.
|
|
802
616
|
|
|
803
|
-
|
|
804
|
-
speedExtension({ postgres: sql }),
|
|
805
|
-
) as SpeedClient<typeof PrismaClient>
|
|
617
|
+
Because config names can differ between versions, the safe rule is:
|
|
806
618
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
skip: 0,
|
|
811
|
-
take: 10,
|
|
812
|
-
orderBy: { createdAt: 'desc' },
|
|
813
|
-
})
|
|
619
|
+
- enable the planner in generator/runtime config if your build exposes that switch
|
|
620
|
+
- keep it on for relation-heavy workloads
|
|
621
|
+
- tune any thresholds only after measuring with real production-shaped queries
|
|
814
622
|
|
|
815
|
-
|
|
816
|
-
const searchUsers = await prisma.user.findMany({
|
|
817
|
-
where: { email: { contains: '@example.com' } },
|
|
818
|
-
})
|
|
819
|
-
```
|
|
623
|
+
If your project has planner thresholds, start conservatively:
|
|
820
624
|
|
|
821
|
-
|
|
625
|
+
- prefer bounded strategies for one-to-one and unique includes
|
|
626
|
+
- prefer segmented or reduced strategies for one-to-many and many-to-many
|
|
627
|
+
- lower thresholds for deep includes with large child tables
|
|
628
|
+
- raise thresholds only after verifying lower fan-out in production data
|
|
822
629
|
|
|
823
|
-
|
|
824
|
-
- Falls back to runtime generation for non-matching queries (still fast)
|
|
825
|
-
- Tracks which queries are prebaked via `onQuery` callback
|
|
630
|
+
### How to verify the planner is helping
|
|
826
631
|
|
|
827
|
-
|
|
632
|
+
Use `debug` and `onQuery`.
|
|
828
633
|
|
|
829
|
-
|
|
634
|
+
Look for:
|
|
830
635
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
636
|
+
- large latency spikes on include-heavy queries
|
|
637
|
+
- unusually large result sets for a small parent page
|
|
638
|
+
- repeated slow nested includes on high-fanout relations
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
const prisma = basePrisma.$extends(
|
|
642
|
+
speedExtension({
|
|
643
|
+
postgres: sql,
|
|
644
|
+
debug: true,
|
|
645
|
+
onQuery: (info) => {
|
|
646
|
+
console.log(`${info.model}.${info.method} ${info.duration}ms`)
|
|
647
|
+
console.log(info.sql)
|
|
648
|
+
},
|
|
649
|
+
}),
|
|
650
|
+
) as SpeedClient<typeof basePrisma>
|
|
844
651
|
```
|
|
845
652
|
|
|
846
|
-
|
|
653
|
+
What good results look like:
|
|
847
654
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
655
|
+
- small parent page stays small in latency
|
|
656
|
+
- bounded child includes remain predictable
|
|
657
|
+
- high-fanout includes stop exploding row counts
|
|
658
|
+
- moving a heavy include into `$batch` or splitting it improves latency materially
|
|
851
659
|
|
|
852
|
-
|
|
853
|
-
# dialect = "postgres" # or "sqlite"
|
|
660
|
+
### Practical recommendations
|
|
854
661
|
|
|
855
|
-
|
|
856
|
-
# output = "./generated/sql"
|
|
662
|
+
For best results with the planner:
|
|
857
663
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
664
|
+
1. index all relation keys
|
|
665
|
+
2. encode one-to-one relations with `@unique`
|
|
666
|
+
3. use stable `orderBy`
|
|
667
|
+
4. cap nested collections with `take`
|
|
668
|
+
5. page parents before including deep trees
|
|
669
|
+
6. split unrelated heavy branches into `$batch`
|
|
670
|
+
7. benchmark with real data distributions, not toy fixtures
|
|
671
|
+
|
|
672
|
+
## Batch queries
|
|
673
|
+
|
|
674
|
+
`$batch` runs multiple independent read queries in one PostgreSQL round trip.
|
|
675
|
+
|
|
676
|
+
```ts
|
|
677
|
+
const dashboard = await prisma.$batch((batch) => ({
|
|
678
|
+
totalUsers: batch.user.count(),
|
|
679
|
+
activeUsers: batch.user.count({
|
|
680
|
+
where: { status: 'ACTIVE' },
|
|
681
|
+
}),
|
|
682
|
+
recentProjects: batch.project.findMany({
|
|
683
|
+
take: 5,
|
|
684
|
+
orderBy: { createdAt: 'desc' },
|
|
685
|
+
include: { organization: true },
|
|
686
|
+
}),
|
|
687
|
+
taskStats: batch.task.aggregate({
|
|
688
|
+
_count: true,
|
|
689
|
+
_avg: { estimatedHours: true },
|
|
690
|
+
where: { status: 'IN_PROGRESS' },
|
|
691
|
+
}),
|
|
692
|
+
}))
|
|
861
693
|
```
|
|
862
694
|
|
|
863
|
-
###
|
|
695
|
+
### Rules
|
|
864
696
|
|
|
865
|
-
|
|
697
|
+
Do not `await` inside the callback.
|
|
866
698
|
|
|
867
|
-
|
|
868
|
-
import { PrismaClient } from '@prisma/client'
|
|
869
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
870
|
-
import postgres from 'postgres'
|
|
699
|
+
Incorrect:
|
|
871
700
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
)
|
|
701
|
+
```ts
|
|
702
|
+
await prisma.$batch(async (batch) => ({
|
|
703
|
+
users: await batch.user.findMany(),
|
|
704
|
+
}))
|
|
705
|
+
```
|
|
876
706
|
|
|
877
|
-
|
|
707
|
+
Correct:
|
|
878
708
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
709
|
+
```ts
|
|
710
|
+
await prisma.$batch((batch) => ({
|
|
711
|
+
users: batch.user.findMany(),
|
|
712
|
+
}))
|
|
883
713
|
```
|
|
884
714
|
|
|
885
|
-
|
|
715
|
+
### Best use cases
|
|
886
716
|
|
|
887
|
-
|
|
717
|
+
- dashboards
|
|
718
|
+
- analytics summaries
|
|
719
|
+
- counts + page data
|
|
720
|
+
- multiple independent aggregates
|
|
721
|
+
- splitting unrelated heavy reads instead of building one massive include tree
|
|
888
722
|
|
|
889
|
-
|
|
890
|
-
import { createToSQL } from 'prisma-sql'
|
|
891
|
-
import { MODELS } from './generated/sql'
|
|
723
|
+
### Limitations
|
|
892
724
|
|
|
893
|
-
|
|
725
|
+
- PostgreSQL only
|
|
726
|
+
- queries are independent
|
|
727
|
+
- not transactional
|
|
728
|
+
- use `$transaction` when you need transactional guarantees
|
|
894
729
|
|
|
895
|
-
|
|
896
|
-
async fetch(request: Request, env: Env) {
|
|
897
|
-
const { sql, params } = toSQL('User', 'findMany', {
|
|
898
|
-
where: { status: 'ACTIVE' },
|
|
899
|
-
})
|
|
730
|
+
## Configuration
|
|
900
731
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
732
|
+
### Debug logging
|
|
733
|
+
|
|
734
|
+
```ts
|
|
735
|
+
const prisma = basePrisma.$extends(
|
|
736
|
+
speedExtension({
|
|
737
|
+
postgres: sql,
|
|
738
|
+
debug: true,
|
|
739
|
+
}),
|
|
740
|
+
) as SpeedClient<typeof basePrisma>
|
|
907
741
|
```
|
|
908
742
|
|
|
909
|
-
|
|
743
|
+
### Performance hook
|
|
910
744
|
|
|
911
|
-
|
|
745
|
+
```ts
|
|
746
|
+
const prisma = basePrisma.$extends(
|
|
747
|
+
speedExtension({
|
|
748
|
+
postgres: sql,
|
|
749
|
+
onQuery: (info) => {
|
|
750
|
+
console.log(`${info.model}.${info.method}: ${info.duration}ms`)
|
|
751
|
+
console.log(`prebaked=${info.prebaked}`)
|
|
752
|
+
},
|
|
753
|
+
}),
|
|
754
|
+
) as SpeedClient<typeof basePrisma>
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
The callback receives:
|
|
912
758
|
|
|
759
|
+
```ts
|
|
760
|
+
interface QueryInfo {
|
|
761
|
+
model: string
|
|
762
|
+
method: string
|
|
763
|
+
sql: string
|
|
764
|
+
params: unknown[]
|
|
765
|
+
duration: number
|
|
766
|
+
prebaked: boolean
|
|
767
|
+
}
|
|
913
768
|
```
|
|
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
|
|
769
|
+
|
|
770
|
+
## Generator configuration
|
|
771
|
+
|
|
772
|
+
```prisma
|
|
773
|
+
generator sql {
|
|
774
|
+
provider = "prisma-sql-generator"
|
|
775
|
+
|
|
776
|
+
// optional
|
|
777
|
+
// dialect = "postgres"
|
|
778
|
+
|
|
779
|
+
// optional
|
|
780
|
+
// output = "./generated/sql"
|
|
781
|
+
|
|
782
|
+
// optional
|
|
783
|
+
// skipInvalid = "true"
|
|
784
|
+
}
|
|
949
785
|
```
|
|
950
786
|
|
|
951
|
-
|
|
787
|
+
## `@optimize` examples
|
|
952
788
|
|
|
953
|
-
|
|
789
|
+
### Basic prebaked query
|
|
954
790
|
|
|
955
791
|
```prisma
|
|
956
792
|
/// @optimize {
|
|
@@ -965,20 +801,24 @@ model User {
|
|
|
965
801
|
}
|
|
966
802
|
```
|
|
967
803
|
|
|
968
|
-
|
|
804
|
+
### Dynamic parameters
|
|
969
805
|
|
|
970
806
|
```prisma
|
|
971
807
|
/// @optimize {
|
|
972
808
|
/// "method": "findMany",
|
|
973
809
|
/// "query": {
|
|
810
|
+
/// "where": { "status": "$status" },
|
|
974
811
|
/// "skip": "$skip",
|
|
975
|
-
/// "take": "$take"
|
|
976
|
-
/// "orderBy": { "createdAt": "desc" }
|
|
812
|
+
/// "take": "$take"
|
|
977
813
|
/// }
|
|
978
814
|
/// }
|
|
815
|
+
model User {
|
|
816
|
+
id Int @id
|
|
817
|
+
status String
|
|
818
|
+
}
|
|
979
819
|
```
|
|
980
820
|
|
|
981
|
-
|
|
821
|
+
### Nested include
|
|
982
822
|
|
|
983
823
|
```prisma
|
|
984
824
|
/// @optimize {
|
|
@@ -993,298 +833,216 @@ model User {
|
|
|
993
833
|
/// }
|
|
994
834
|
/// }
|
|
995
835
|
/// }
|
|
836
|
+
model User {
|
|
837
|
+
id Int @id
|
|
838
|
+
posts Post[]
|
|
839
|
+
}
|
|
996
840
|
```
|
|
997
841
|
|
|
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
|
|
842
|
+
## Edge usage
|
|
1020
843
|
|
|
1021
|
-
|
|
844
|
+
### Vercel Edge
|
|
1022
845
|
|
|
1023
|
-
```
|
|
1024
|
-
const prisma = new PrismaClient()
|
|
1025
|
-
const users = await prisma.user.findMany()
|
|
1026
|
-
```
|
|
1027
|
-
|
|
1028
|
-
**After:**
|
|
1029
|
-
|
|
1030
|
-
```typescript
|
|
846
|
+
```ts
|
|
1031
847
|
import { PrismaClient } from '@prisma/client'
|
|
1032
848
|
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
1033
849
|
import postgres from 'postgres'
|
|
1034
850
|
|
|
1035
|
-
const sql = postgres(process.env.DATABASE_URL)
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
export const prisma = basePrisma.$extends(
|
|
851
|
+
const sql = postgres(process.env.DATABASE_URL!)
|
|
852
|
+
const prisma = new PrismaClient().$extends(
|
|
1039
853
|
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'
|
|
854
|
+
) as SpeedClient<typeof PrismaClient>
|
|
1052
855
|
|
|
1053
|
-
const
|
|
1054
|
-
const db = drizzle(sql)
|
|
856
|
+
export const config = { runtime: 'edge' }
|
|
1055
857
|
|
|
1056
|
-
|
|
1057
|
-
.
|
|
1058
|
-
.
|
|
1059
|
-
|
|
858
|
+
export default async function handler() {
|
|
859
|
+
const users = await prisma.user.findMany()
|
|
860
|
+
return Response.json(users)
|
|
861
|
+
}
|
|
1060
862
|
```
|
|
1061
863
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
```typescript
|
|
1065
|
-
import { PrismaClient } from '@prisma/client'
|
|
1066
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
1067
|
-
import postgres from 'postgres'
|
|
864
|
+
### Cloudflare Workers
|
|
1068
865
|
|
|
1069
|
-
|
|
1070
|
-
const basePrisma = new PrismaClient()
|
|
1071
|
-
|
|
1072
|
-
export const prisma = basePrisma.$extends(
|
|
1073
|
-
speedExtension({ postgres: sql }),
|
|
1074
|
-
) as SpeedClient<typeof basePrisma>
|
|
866
|
+
Use the standalone SQL generation API.
|
|
1075
867
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
```
|
|
868
|
+
```ts
|
|
869
|
+
import { createToSQL } from 'prisma-sql'
|
|
870
|
+
import { MODELS } from './generated/sql'
|
|
1080
871
|
|
|
1081
|
-
|
|
872
|
+
const toSQL = createToSQL(MODELS, 'sqlite')
|
|
1082
873
|
|
|
1083
|
-
|
|
874
|
+
export default {
|
|
875
|
+
async fetch(request: Request, env: Env) {
|
|
876
|
+
const { sql, params } = toSQL('User', 'findMany', {
|
|
877
|
+
where: { status: 'ACTIVE' },
|
|
878
|
+
})
|
|
1084
879
|
|
|
1085
|
-
|
|
880
|
+
const result = await env.DB.prepare(sql)
|
|
881
|
+
.bind(...params)
|
|
882
|
+
.all()
|
|
1086
883
|
|
|
1087
|
-
|
|
1088
|
-
|
|
884
|
+
return Response.json(result.results)
|
|
885
|
+
},
|
|
886
|
+
}
|
|
887
|
+
```
|
|
1089
888
|
|
|
1090
|
-
|
|
889
|
+
## Performance
|
|
1091
890
|
|
|
1092
|
-
|
|
891
|
+
Performance depends on:
|
|
1093
892
|
|
|
1094
|
-
-
|
|
1095
|
-
-
|
|
1096
|
-
-
|
|
1097
|
-
-
|
|
893
|
+
- database type
|
|
894
|
+
- query shape
|
|
895
|
+
- indexing
|
|
896
|
+
- relation fan-out
|
|
897
|
+
- whether the query is prebaked
|
|
898
|
+
- whether the cardinality planner can choose a bounded strategy
|
|
1098
899
|
|
|
1099
|
-
|
|
900
|
+
Typical gains are strongest when:
|
|
1100
901
|
|
|
1101
|
-
|
|
902
|
+
- Prisma overhead dominates total time
|
|
903
|
+
- includes are moderate but structured well
|
|
904
|
+
- query shapes repeat
|
|
905
|
+
- indexes exist on relation and filter columns
|
|
1102
906
|
|
|
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)
|
|
907
|
+
Run your own benchmarks on production-shaped data.
|
|
1109
908
|
|
|
1110
909
|
## Troubleshooting
|
|
1111
910
|
|
|
1112
|
-
###
|
|
911
|
+
### `speedExtension requires postgres or sqlite client`
|
|
1113
912
|
|
|
1114
|
-
|
|
913
|
+
Pass a database-native client to the generated extension.
|
|
1115
914
|
|
|
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>
|
|
915
|
+
```ts
|
|
916
|
+
const prisma = new PrismaClient().$extends(speedExtension({ postgres: sql }))
|
|
1124
917
|
```
|
|
1125
918
|
|
|
1126
|
-
###
|
|
919
|
+
### Generated dialect mismatch
|
|
920
|
+
|
|
921
|
+
If generated code targets PostgreSQL, do not pass SQLite, and vice versa.
|
|
1127
922
|
|
|
1128
|
-
|
|
923
|
+
Override dialect in the generator if needed.
|
|
1129
924
|
|
|
1130
925
|
```prisma
|
|
1131
926
|
generator sql {
|
|
1132
927
|
provider = "prisma-sql-generator"
|
|
1133
|
-
dialect = "postgres"
|
|
928
|
+
dialect = "postgres"
|
|
1134
929
|
}
|
|
1135
930
|
```
|
|
1136
931
|
|
|
1137
|
-
###
|
|
932
|
+
### Results differ from Prisma
|
|
1138
933
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
```typescript
|
|
1142
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
934
|
+
Turn on debug logging and compare generated SQL with Prisma query logs.
|
|
1143
935
|
|
|
936
|
+
```ts
|
|
1144
937
|
const prisma = new PrismaClient().$extends(
|
|
1145
938
|
speedExtension({
|
|
1146
939
|
postgres: sql,
|
|
1147
|
-
debug: true,
|
|
940
|
+
debug: true,
|
|
1148
941
|
}),
|
|
1149
|
-
)
|
|
942
|
+
)
|
|
1150
943
|
```
|
|
1151
944
|
|
|
1152
|
-
|
|
945
|
+
If behavior differs, open an issue with:
|
|
1153
946
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
947
|
+
- schema excerpt
|
|
948
|
+
- Prisma query
|
|
949
|
+
- generated SQL
|
|
950
|
+
- expected result
|
|
951
|
+
- actual result
|
|
952
|
+
|
|
953
|
+
### Performance is worse on a relation-heavy query
|
|
1157
954
|
|
|
1158
|
-
|
|
955
|
+
Check these first:
|
|
1159
956
|
|
|
1160
|
-
|
|
957
|
+
- missing foreign-key indexes
|
|
958
|
+
- deep unbounded includes
|
|
959
|
+
- no nested `take`
|
|
960
|
+
- unstable or missing `orderBy`
|
|
961
|
+
- high-fanout relation trees that should be split into `$batch`
|
|
1161
962
|
|
|
1162
|
-
|
|
963
|
+
### Connection pool exhaustion
|
|
1163
964
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
965
|
+
Increase `postgres.js` pool size if needed.
|
|
966
|
+
|
|
967
|
+
```ts
|
|
968
|
+
const sql = postgres(process.env.DATABASE_URL!, {
|
|
969
|
+
max: 50,
|
|
1167
970
|
})
|
|
1168
971
|
```
|
|
1169
972
|
|
|
1170
|
-
|
|
973
|
+
## Limitations
|
|
1171
974
|
|
|
1172
|
-
|
|
975
|
+
### Partially supported
|
|
1173
976
|
|
|
1174
|
-
-
|
|
1175
|
-
-
|
|
1176
|
-
- Aggregations on unindexed fields
|
|
977
|
+
- basic array operators
|
|
978
|
+
- basic JSON path filtering
|
|
1177
979
|
|
|
1178
|
-
|
|
980
|
+
### Not yet supported
|
|
1179
981
|
|
|
1180
|
-
|
|
1181
|
-
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
982
|
+
These should fall back to Prisma:
|
|
1182
983
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
console.log(`${info.method} took ${info.duration}ms`)
|
|
1188
|
-
},
|
|
1189
|
-
}),
|
|
1190
|
-
) as SpeedClient<typeof PrismaClient>
|
|
1191
|
-
```
|
|
984
|
+
- full-text `search`
|
|
985
|
+
- composite/document-style embedded types
|
|
986
|
+
- vendor-specific extensions not yet modeled by the SQL builder
|
|
987
|
+
- some advanced `groupBy` edge cases
|
|
1192
988
|
|
|
1193
989
|
## FAQ
|
|
1194
990
|
|
|
1195
|
-
**
|
|
1196
|
-
|
|
991
|
+
**Do I still need Prisma?**
|
|
992
|
+
Yes. Prisma remains the source of truth for schema, migrations, types, writes, and fallback behavior.
|
|
1197
993
|
|
|
1198
|
-
**
|
|
1199
|
-
|
|
994
|
+
**Does this replace Prisma Client?**
|
|
995
|
+
No. It extends Prisma Client.
|
|
1200
996
|
|
|
1201
|
-
**
|
|
1202
|
-
|
|
997
|
+
**What gets accelerated?**
|
|
998
|
+
Supported read queries only.
|
|
1203
999
|
|
|
1204
|
-
**
|
|
1205
|
-
|
|
1000
|
+
**What about writes?**
|
|
1001
|
+
Writes continue through Prisma.
|
|
1206
1002
|
|
|
1207
|
-
**
|
|
1208
|
-
|
|
1003
|
+
**Do I need `@optimize`?**
|
|
1004
|
+
No. It is optional. It only reduces the overhead of repeated hot query shapes.
|
|
1209
1005
|
|
|
1210
|
-
**
|
|
1211
|
-
|
|
1006
|
+
**Does `$batch` work with SQLite?**
|
|
1007
|
+
Not currently.
|
|
1212
1008
|
|
|
1213
|
-
**
|
|
1214
|
-
|
|
1009
|
+
**Is it safe to use in production?**
|
|
1010
|
+
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
1011
|
|
|
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.
|
|
1012
|
+
## Migration
|
|
1218
1013
|
|
|
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.
|
|
1014
|
+
### Before
|
|
1221
1015
|
|
|
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
|
|
1016
|
+
```ts
|
|
1017
|
+
const prisma = new PrismaClient()
|
|
1018
|
+
const users = await prisma.user.findMany()
|
|
1242
1019
|
```
|
|
1243
1020
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
Benchmark your own queries:
|
|
1021
|
+
### After
|
|
1247
1022
|
|
|
1248
|
-
```
|
|
1023
|
+
```ts
|
|
1024
|
+
import { PrismaClient } from '@prisma/client'
|
|
1249
1025
|
import { speedExtension, type SpeedClient } from './generated/sql'
|
|
1026
|
+
import postgres from 'postgres'
|
|
1250
1027
|
|
|
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>
|
|
1028
|
+
const sql = postgres(process.env.DATABASE_URL!)
|
|
1029
|
+
const basePrisma = new PrismaClient()
|
|
1265
1030
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
users: batch.user.count(),
|
|
1270
|
-
posts: batch.post.count(),
|
|
1271
|
-
}))
|
|
1031
|
+
export const prisma = basePrisma.$extends(
|
|
1032
|
+
speedExtension({ postgres: sql }),
|
|
1033
|
+
) as SpeedClient<typeof basePrisma>
|
|
1272
1034
|
|
|
1273
|
-
|
|
1035
|
+
const users = await prisma.user.findMany()
|
|
1274
1036
|
```
|
|
1275
1037
|
|
|
1276
|
-
##
|
|
1277
|
-
|
|
1278
|
-
PRs welcome! Priority areas:
|
|
1038
|
+
## Examples
|
|
1279
1039
|
|
|
1280
|
-
-
|
|
1281
|
-
-
|
|
1282
|
-
-
|
|
1283
|
-
-
|
|
1284
|
-
- Edge runtime compatibility
|
|
1285
|
-
- Documentation improvements
|
|
1040
|
+
- `examples/generator-mode`
|
|
1041
|
+
- `tests/e2e/postgres.test.ts`
|
|
1042
|
+
- `tests/e2e/sqlite.e2e.test.ts`
|
|
1043
|
+
- `tests/sql-injection/batch-transaction.test.ts`
|
|
1286
1044
|
|
|
1287
|
-
|
|
1045
|
+
## Development
|
|
1288
1046
|
|
|
1289
1047
|
```bash
|
|
1290
1048
|
git clone https://github.com/multipliedtwice/prisma-to-sql
|
|
@@ -1294,52 +1052,6 @@ npm run build
|
|
|
1294
1052
|
npm test
|
|
1295
1053
|
```
|
|
1296
1054
|
|
|
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
1055
|
## License
|
|
1344
1056
|
|
|
1345
1057
|
MIT
|
|
@@ -1349,8 +1061,3 @@ MIT
|
|
|
1349
1061
|
- [NPM Package](https://www.npmjs.com/package/prisma-sql)
|
|
1350
1062
|
- [GitHub Repository](https://github.com/multipliedtwice/prisma-to-sql)
|
|
1351
1063
|
- [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
|
-
````
|