Backend · Guide

ORMs & Migrations

Mapping objects to rows without losing sight of the SQL, and changing a schema under a running system.

— min read Backend

A Useful Abstraction You Must See Through

An ORM maps rows to objects and writes your SQL. It removes a great deal of tedium and hides the one thing you most need to see: how many queries a request actually runs.

The productive stance is to treat it as a query generator you supervise. Use it for the ordinary 90% — inserts, lookups, simple joins — read the SQL it emits, and drop to hand-written SQL for reporting queries and anything performance-critical. An ORM you cannot see through is the one that produces the mystery 4-second endpoint.

Good atBad at
CRUD and relationshipsComplex analytical queries
Type-safe field accessWindow functions, CTEs, hints
Migrations and schema historyMaking query count obvious
Portability across enginesEngine-specific features you want

The N+1 Query Problem

This is the single most common backend performance defect, and ORMs make it effortless to write by accident.

# one query for the orders …
orders = session.query(Order).limit(50).all()

for order in orders:
    print(order.customer.name)   # … and one more per order. 51 round trips.

# eager load instead: one query, or two
orders = (session.query(Order)
          .options(selectinload(Order.customer))
          .limit(50).all())
LoadingBehaviourUse when
LazyFetches the relation on first accessYou rarely touch it
Eager (join)One query with a joinTo-one relations
Eager (select-in)A second query with IN (…)To-many relations — avoids row multiplication
Log query counts per request in development and fail a test if an endpoint exceeds a threshold. N+1 is invisible on ten rows of seed data and catastrophic on ten thousand — the same bug the data section calls out for indexes.

Connections & Transactions

Opening a database connection is expensive, and databases cap how many they will accept. A pool keeps a fixed set open and lends them out; sizing it is a systems decision, not a default to leave alone.

SymptomCause
Requests queue while the database is idlePool too small
Database refuses connectionsPool × instances exceeds its limit
Pool exhausted under loadA connection held across an external API call
Intermittent stale-connection errorsNo recycle or health check on idle connections
Never hold a database connection across a slow external call. The pool is a shared resource; one endpoint awaiting a third-party API with a connection checked out will starve every other endpoint on the service.

Keep transactions short and explicit. A transaction open for the duration of a request holds locks far longer than the work needs, and the resulting contention appears as unrelated endpoints getting slower.

Migrations Without Downtime

Schema changes run against a live database while the old code is still serving traffic. That constraint — not the ORM — is what makes migrations hard.

RuleWhy
Migrations are code, in version controlReviewed, ordered and reproducible
Additive first, destructive laterOld and new code must both work mid-deploy
Never rename in one stepAdd, backfill, dual-write, switch, then drop
New columns nullable or defaultedA NOT NULL without a default fails on existing rows
Index concurrently on large tablesA plain CREATE INDEX takes a write lock
Backfill in batchesOne enormous UPDATE locks the table and bloats the log
The expand-and-contract pattern is the whole technique: expand the schema so old and new code both work, deploy the code, then contract by removing what only the old code used — three deploys, zero downtime.

Interview Questions

What is the N+1 query problem?

Fetching a list with one query, then issuing another query per item to load a relation. Eager loading — a join or a select-in — collapses it back to one or two queries.

When should you bypass the ORM?

For reporting and analytical queries, engine-specific features, and any hot path where the generated SQL is not what you want. Use it for ordinary CRUD and read what it emits.

How do you size a connection pool?

By what the database can accept divided across instances, and by how long connections are held. Too small queues requests; pool × instances beyond the server limit causes refusals.

Why not hold a connection across an external API call?

The pool is shared. One slow third-party call with a connection checked out starves every other endpoint on the service.

How do you rename a column with no downtime?

Expand and contract: add the new column, backfill it, dual-write, switch reads, deploy, then drop the old one — because old and new code run simultaneously during a deploy.

Why add new columns as nullable or with a default?

A NOT NULL column with no default fails against existing rows, and on a large table the rewrite takes a lock long enough to be an outage.

Quick Quiz

1. N+1 queries are fixed by…
2. Select-in loading is preferred over a join for…
3. Holding a pooled connection during an external API call…
4. Adding a NOT NULL column with no default to a populated table…
5. Expand-and-contract exists because…