Databases & SQL for Developers
The database is the part of your stack that outlives everything else. Here is how to treat it that way.
Most developers meet the database late and treat it as a dumb bucket. We pick a framework, an ORM, a deployment target - and somewhere in there a database appears, configured by whoever read the docs first, schema accreted one feature at a time. It works. Then the product succeeds, two requests arrive at once, a report needs three tables joined, and suddenly the bucket is the bottleneck, the bug source, and the thing nobody wants to change. The uncomfortable truth is that your relational database will almost certainly outlive your current framework, your language, and possibly your company. It deserves to be designed, not accreted.
The schema is your real model
Code expresses your intent for one running process. The schema expresses it for every process, forever, including the ones you have not written yet and the ad-hoc query someone runs at 2 a.m. during an incident. That asymmetry is why pushing rules into the schema pays off so disproportionately.
A CHECK (price >= 0) constraint protects you against every code path that ever touches that table - the endpoint you wrote today, the batch job you write next year, the migration script someone pastes from a wiki. A NOT NULL, a UNIQUE, a foreign key: each one is a small invariant the database enforces on your behalf, billions of times, without a single line of application code. Validation in your service layer is good. Validation in the schema is permanent. The two are not redundant; the schema is the floor below which data cannot fall, no matter who is writing.
This is also the strongest argument for normalization. The point of third normal form is not academic tidiness; it is that every fact lives in exactly one place, so it cannot disagree with itself. Store a user's email in users and only there, and it can never be stale in posts. Store it in both and you have signed up to keep two copies in sync forever - a promise application code breaks the moment someone forgets. Normalize first. Denormalize later, deliberately, with a measured reason and a documented owner for the duplicated data. Doing it in that order means your duplication is a considered optimization rather than an accident you discover during an outage.
Correctness lives in transactions
The single biggest gap between a junior and a senior backend developer is how they think about concurrency. Single-user logic is trivial: read, compute, write. The instant two users touch the same row at the same moment, naive code starts losing data in ways that never reproduce on your laptop and always reproduce in production.
Transactions are the tool that makes this tractable. Wrapping related writes in BEGIN ... COMMIT buys you atomicity - all or nothing - which is the difference between "transfer failed cleanly" and "money vanished." Choosing an isolation level is choosing how much the database protects you from other transactions' half-done work. And knowing the lost-update pattern - two clients reading a stock count of one and both selling it - is knowing the bug that quietly oversells inventory and double-spends balances. The fixes are not exotic: let the database do the arithmetic (SET stock = stock - 1 WHERE stock > 0), or claim the row with SELECT ... FOR UPDATE before you decide. What matters is that you reach for them before the incident, not after.
Fast and safe are not afterthoughts
Performance work has a bad reputation because people do it by intuition. Don't. The database will tell you exactly what it is doing if you ask with EXPLAIN. A sequential scan on a large table is a missing index; an index scan is the fix; EXPLAIN ANALYZE proves the difference with real numbers. Most application slowness is not a clever problem - it is a missing index on a foreign key, or the N+1 pattern where an innocent loop fires fifty queries that one join would have answered. You find both by reading your query log in development, which costs nothing and surfaces the worst offenders immediately.
Safety is even less negotiable, and even simpler. Never build a query by gluing user input into a string. Pass values as parameters and the driver keeps data and code in separate lanes, which makes SQL injection - still one of the most common serious vulnerabilities on the web - structurally impossible rather than merely unlikely. Parameterized queries are not a security tax you grudgingly pay; they are also faster, because the database can reuse the plan. The secure path is the easy path. You just have to take it every time.
Treat it like code
The final shift is to stop treating the schema as a thing that exists and start treating it as a thing that evolves. Every change goes through a migration: version-controlled, ordered, reviewable, reversible, committed alongside the code that needs it. Make changes backward compatible so old and new versions of your app can run side by side during a deploy - add a nullable column, backfill it, enforce the constraint in a later step. Never edit a migration that has already run somewhere real.
None of this is glamorous, and none of it is hard once it is habit. A well-typed, normalized, constrained schema; transactions around anything that must be consistent; indexes guided by EXPLAIN; parameterized access; disciplined migrations. That is the whole craft. Do it deliberately and the database stops being the scary part of the stack and becomes the dependable one - the foundation everything else gets to stand on without thinking about it. Which, for the part of your system most likely to outlive all the rest, is exactly the right outcome.
No comments:
Post a Comment