I Reduced Prisma Database Operations by 60% Without Upgrading My Database
How profiling, indexing, smarter queries, and a few architectural changes helped me scale PostgreSQL before spending a single extra dollar.
When you’re building with Prisma and PostgreSQL, the database eventually becomes your biggest bottleneck.
A query that executes in 5 ms with a few hundred rows can suddenly take seconds once your tables grow into the millions. Many teams immediately reach for a larger database instance or a more expensive hosting plan.
I almost did the same.
While building Sellary.in, my platform’s database operations suddenly jumped to 5,000–6,000 operations per day. At that rate, I was going to exceed Prisma’s free tier before the month ended.
Instead of upgrading, I spent a weekend profiling my queries and optimizing the database layer.
The result?
✅ Over 60% fewer database operations
✅ Faster API responses
✅ Lower infrastructure costs
Most importantly, I learned that good database architecture scales much better than expensive hardware.
Here are the techniques that made the biggest difference.
1. Profile Before You Optimize
The biggest mistake developers make is optimizing blindly.
Never assume a query is slow.
Measure it.
The two tools I relied on the most were:
- EXPLAIN ANALYZE — shows PostgreSQL’s execution plan, scanned rows, index usage, and execution time.
- Prisma Query Logging — reveals the exact SQL generated by Prisma.
EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE seller_id = 123;A query might look perfectly fine in Prisma, yet PostgreSQL may still perform a Sequential Scan over an entire table.
Without profiling, you’re simply guessing.
Other useful tools
- pg_stat_statements
- Prisma Query Insights
- Vercel dashboards
2. Index the Queries You Actually Run
Indexes are easily the highest-impact optimization you can make.
Without an index:
SELECT *
FROM orders
WHERE seller_id = 123;PostgreSQL may inspect every row.
Adding an index changes everything.
CREATE INDEX idx_orders_seller
ON orders(seller_id);If your application frequently filters using multiple columns, prefer composite indexes.
CREATE INDEX idx_orders_seller_status
ON orders(seller_id, status);Now this query becomes dramatically faster.
WHERE seller_id = 123
AND status = 'PAID'Rule of thumb
Index the columns you actually filter, sort, or join on — not every column in your database.
3. Eliminate the N+1 Query Problem
This is one of the most common performance killers in ORM-based applications.
Imagine loading 100 orders.
Then loading each user individually.
const orders = await prisma.order.findMany();for (const order of orders) {
await prisma.user.findUnique({
where: {
id: order.userId
}
});
}That becomes 101 database queries.
Instead, let Prisma fetch everything together.
const orders = await prisma.order.findMany({
include: {
user: true
}
});One query.
Much lower latency.
Much lower database load.
4. Fetch Only the Columns You Need
One of the easiest optimizations is reducing the amount of data transferred.
Avoid this:
await prisma.user.findMany();It returns every column.
Instead:
await prisma.user.findMany({
select: {
id: true,
name: true
}
});If your table contains JSON blobs, markdown, images, or large text fields, this optimization alone can significantly reduce response times.
5. Replace Heavy Aggregations With Counters
Analytics dashboards often execute queries like:
COUNT(*)
SUM(amount)
AVG(...)every single page refresh.
That works with 100 rows.
It doesn’t work with millions.
Instead, maintain aggregate values during writes.
For example:
Instead of calculating total sales every request…
SELECT SUM(amount)
FROM orders;Store the value directly.
User
-------------
totalSales
totalOrders
totalRevenueUpdate these counters whenever a purchase completes.
Reads become O(1) instead of scanning thousands — or millions — of rows.
This single change reduced a large portion of my database operations.
6. Paginate Everything
Never assume your tables will stay small.
Avoid:
await prisma.product.findMany();Instead:
await prisma.product.findMany({
take: 20,
skip: 20
});For very large datasets, prefer cursor pagination.
Cursor pagination avoids the performance degradation that comes with deep OFFSET queries and scales much better for infinite scrolling.
7. Cache Expensive Reads
The fastest query is the one you never execute.
If your data rarely changes, cache it.
Get Aman Bind’s stories in your inbox
Join Medium for free to get updates from this writer.
Examples include:
- product pages
- public storefronts
- application settings
- category lists
Popular caching layers include:
- Upstash Redis
- Next.js Cache
- LRU Cache
Moving hot reads out of PostgreSQL dramatically reduces database pressure.
8. Batch Writes Instead of Looping
Network latency adds up quickly.
Avoid:
for (const item of items) {
await prisma.item.create({
data: item
});
}Instead:
await prisma.item.createMany({
data: items
});One network round-trip is always cheaper than hundreds.
9. Use Transactions for Related Operations
When creating an order you might also:
- update inventory
- update balances
- generate receipts
These operations belong together.
await prisma.$transaction([
prisma.order.create(...),
prisma.user.update(...),
prisma.balance.update(...)
]);Transactions provide consistency while allowing PostgreSQL to optimize execution internally.
10. Reuse Prisma Client
Creating a new Prisma Client on every request is expensive, especially during development.
Instead, use a singleton.
import { PrismaClient } from "@prisma/client";const globalForPrisma =
globalThis as unknown as {
prisma: PrismaClient | undefined;
};export const prisma =
globalForPrisma.prisma ??
new PrismaClient();if (process.env.NODE_ENV !== "production")
globalForPrisma.prisma = prisma;
This prevents unnecessary client creation during hot reloads.
11. Use Connection Pooling
Serverless environments can create thousands of concurrent function invocations.
Without connection pooling, your database may run out of available connections before CPU becomes the bottleneck.
Solutions include:
- Prisma Accelerate
- PgBouncer
Connection pooling allows thousands of requests to share a much smaller number of PostgreSQL connections.
12. Optimize JSONB Queries
PostgreSQL’s JSONB is incredibly powerful.
However, querying nested JSON fields without indexes often results in full table scans.
If you frequently filter inside JSON documents, create a GIN index.
This turns slow JSON lookups into fast indexed searches.
13. Prefer findUnique() Whenever Possible
If you’re querying a primary key or unique field, don’t use findFirst().
findUnique()is more efficient because Prisma understands uniqueness and can internally batch requests more effectively.
Use:
await prisma.user.findUnique({
where: {
id
}
});instead of:
findFirst()whenever possible.
14. Archive Instead of Soft Deleting Forever
Soft deletes are convenient.
isDeleted = trueBut after years of data growth, every query filters deleted records.
Large tables become bloated.
For high-volume systems, periodically move deleted records into archive tables.
Smaller tables mean smaller indexes.
Smaller indexes mean faster queries.
15. Let PostgreSQL Clean Up After Itself
PostgreSQL uses MVCC.
Deleted rows aren’t immediately removed.
Instead, they become “dead tuples.”
That’s why autovacuum matters.
A healthy autovacuum configuration:
- removes dead tuples
- updates planner statistics
- improves index efficiency
- keeps query plans accurate
Ignoring vacuum maintenance eventually slows everything down.
Final Thoughts
Most database performance problems cannot be solved by buying larger servers.
They’re solved by better architecture.
While optimizing Sellary, I didn’t change hosting providers.
I didn’t upgrade my database.
I simply:
- profiled every expensive query
- added the right indexes
- eliminated unnecessary reads
- cached repeated requests
- replaced expensive aggregations
- enforced query limits
The result was over 60% fewer database operations, faster APIs, and infrastructure that could support significantly more traffic before requiring a larger database.
Before upgrading your database, spend a weekend understanding how it’s actually being used.
You might be surprised how much performance is hiding in the queries you already have.
Have you found another Prisma or PostgreSQL optimization that made a measurable difference? I’d love to hear about it in the comments.








