| name | sql-batching |
| description | Prevent D1 "too many SQL variables" errors when using Drizzle ORM. Use this skill whenever writing database queries with `inArray()`, bulk inserts/updates, or any query with dynamic arrays. Critical for queries where array size is unbounded (user teams, registrations, IDs from prior queries). |
SQL Batching Pattern
CRITICAL: D1 has a 100 bound parameter limit per query (NOT 999 like standard SQLite). See: https://developers.cloudflare.com/d1/platform/limits/
Use @/utils/batch-query.ts utilities to batch queries.
The 100 Parameter Limit
D1's limit is 100 bound parameters per query, not SQLite's typical 999. This affects:
inArray()queries: each ID = 1 param- Bulk inserts: each column value = 1 param (including auto-generated columns!)
- Combined queries: all WHERE conditions + array items must be < 100
Error message: D1_ERROR: too many SQL variables at offset N: SQLITE_ERROR
When to Batch
Always batch when:
- Using
inArray()with arrays from user data or prior queries - Array size is unbounded (team memberships, registrations, results)
- Bulk inserts with dynamic row counts
- Any operation where total params could exceed 100
Safe to skip when:
- Array is hardcoded/constant AND small (< 50 items to leave headroom)
- Single-row inserts
- Array has guaranteed small upper bound AND you've calculated total params
Usage
import { autochunk, autochunkFirst } from "@/utils/batch-query"
// findMany - returns flattened results
const results = await autochunk(
{ items: ids, otherParametersCount: 1 }, // count other WHERE params
async (chunk) => db.query.table.findMany({
where: and(
eq(table.field, value), // this counts as 1 param
inArray(table.id, chunk),
),
}),
)
// findFirst - stops on first match
const result = await autochunkFirst(
{ items: ids },
async (chunk) => db.query.table.findFirst({
where: inArray(table.id, chunk),
}),
)
For Parallel Execution
Use chunk() + Promise.all when parallel is preferred:
import { chunk, SQL_BATCH_SIZE } from "@/utils/batch-query"
const results = (await Promise.all(
chunk(ids, SQL_BATCH_SIZE).map((batch) =>
db.select().from(table).where(inArray(table.id, batch))
)
)).flat()
Parameters
items: Array to batch (IDs, objects)otherParametersCount: Number of other bound params in query (eq conditions, etc.)SQL_BATCH_SIZE: 100 (D1's actual limit)
For Bulk Inserts
For bulk inserts, Drizzle includes ALL columns (including auto-generated ones). You MUST count every column in the table schema, not just the fields you're explicitly setting.
Counting Columns
Check the table schema and count:
commonColumns(if used):createdAt,updatedAt,updateCounter= 3 columns- All explicit columns:
id,fieldA,fieldB, etc. - Even nullable columns without defaults get a param (Drizzle sends
null)
Calculate Batch Size
// Formula: floor(100 / totalColumns)
// Always subtract 1-2 for safety margin
// Example: judgeHeatAssignmentsTable has 12 columns
// commonColumns (3) + id, heatId, membershipId, rotationId, versionId,
// laneNumber, position, instructions, isManualOverride (9) = 12 total
// Max rows: floor(100 / 12) = 8 rows
const INSERT_BATCH_SIZE = 8
const chunks: Item[][] = []
for (let i = 0; i < items.length; i += INSERT_BATCH_SIZE) {
chunks.push(items.slice(i, i + INSERT_BATCH_SIZE))
}
for (const chunk of chunks) {
await db.insert(table).values(chunk.map(item => ({ ... })))
}
Real-World Examples from Codebase
| Table | Columns | Max Batch Size |
|---|---|---|
judgeHeatAssignmentsTable |
12 | 8 rows |
competitionHeatsTable |
12 | 8 rows |
workoutMovements |
6 | 16 rows |
scoreRoundsTable |
9 | 11 rows |
Common Gotchas
- Don't trust old comments: Previous code assumed 999 limit - always verify
- Nullable columns still count: Even if you don't set them, Drizzle may send
null - Auto-generated columns count:
$defaultFn()columns still use a param slot - The error is cryptic:
too many SQL variables at offset Nmeans you hit 100
Debugging
If you hit the limit, count params in the SQL output:
Query: insert into "table" ("col1", "col2", ...) values (?, ?, ...), (?, ?, ...)
Count the ? marks - that's your actual param count.