Module P-9·23 min read

ORMs vs. raw SQL, read replicas, materialised views, managed databases, and a production-ready deployment checklist.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

P-9 — External Services, Caching Layers, and Deployment

The Practitioner phase has covered SQL patterns, indexes, transactions, schema design, JSONB, access control, full-text search, and performance tuning. This final module ties it together: how PostgreSQL sits inside a production system alongside caching layers, read replicas, ORMs, and managed database services — and what a production-ready deployment actually looks like.


ORMs vs. Raw SQL

Almost every application uses either an ORM or a query builder. Neither is universally better — they make different trade-offs.

What ORMs Get Right

ORMs (Prisma, TypeORM, SQLAlchemy, ActiveRecord) handle the mechanical work well:

  • Type-safe models that match your schema
  • Boilerplate CRUD without writing SQL
  • Migrations generated from schema changes
  • Relationship loading with include/eager_load
  • Connection management

For straightforward CRUD operations — creating users, updating records, simple filtered queries — ORMs are faster to write and harder to get wrong.

What ORMs Get Wrong

ORMs generate SQL you don't control. That becomes a problem in several situations:

Complex aggregations: An ORM asked to compute "revenue by country for the past 90 days, excluding refunded orders, grouped by week" will either generate inefficient SQL or require you to drop to raw SQL anyway.

Window functions: Most ORMs have poor or no support for ROW_NUMBER(), LAG(), LEAD(), RANK(). These require raw queries.

Batch operations: INSERT INTO ... SELECT ..., UPDATE ... FROM ..., DELETE ... USING ... — set-based operations that move data entirely inside PostgreSQL. ORMs tend to model these as row-at-a-time operations with round-trips per row.

Query plan control: You cannot tell an ORM to use a specific index, add LIMIT to a subquery, or restructure a JOIN order. When you need to fix a query plan, you need raw SQL.

The Right Pattern

Use the ORM for CRUD. Use raw SQL for analytics, complex aggregations, bulk operations, and anywhere the ORM-generated query is demonstrably slow.

Most ORMs have an escape hatch:

javascript

The practical rule: start with the ORM, switch to raw SQL the moment you're fighting the ORM to get the query you want.


Read Replicas

A read replica is a PostgreSQL server that receives a continuous stream of WAL from the primary and applies it, maintaining an identical copy of the data. It accepts SELECT queries but not writes.

When Read Replicas Help

  • Reporting and analytics queries that scan large tables and would compete with OLTP queries on the primary
  • Read-heavy workloads where most traffic is SELECT and write throughput isn't the bottleneck
  • Geographic distribution — a replica closer to users reduces read latency
  • Backup source — take backups from the replica to avoid impacting primary performance

When Read Replicas Don't Help

  • Write-heavy workloads — replicas don't offload writes
  • Applications that read their own writes (write then immediately read) — replication lag can return stale data
  • Connection count problems — adding a replica doubles your connection pool management complexity

Replication Lag

Replication is asynchronous by default. After a write on the primary, there is a small window (typically milliseconds, sometimes seconds under load) before the replica reflects it.

sql

For applications where reading stale data is acceptable (analytics, feeds, leaderboards), route reads to the replica freely. For operations where stale reads cause bugs (showing a user their just-submitted form, inventory counts), read from the primary.

Routing Reads

Most connection libraries and ORMs support read/write splitting:

javascript

Or at the infrastructure level, a load balancer like HAProxy or AWS RDS Proxy can route read traffic to replicas automatically.


Materialised Views

A materialised view stores the result of a query as a physical table on disk, refreshed on demand. It is the right tool when:

  • A complex query is run frequently but the underlying data changes infrequently
  • The query takes seconds to compute and you need millisecond response
  • You want to decouple expensive aggregation from query-time computation
sql

REFRESH MATERIALIZED VIEW CONCURRENTLY is critical in production — a regular refresh locks the view for the duration of the refresh. CONCURRENTLY builds a new version alongside the old one and swaps atomically, so readers never block. It requires a unique index.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.