Denormalization Was Never the Problem. It Was How You Did It.

You’ve been writing triggers, materialized views, or application-level aggregation for years. You know the feeling: a subtle bug in a sync function, a race condition at 3 AM, a derived column that silently drifts out of sync. That knot in your stomach when someone asks, “Is the data consistent?”

Let me tell you something that might make you uncomfortable: Denormalization isn’t the enemy. Ad-hoc, unprincipled denormalization is.

I’ve been building databases for over two decades, and I’ve seen the same story play out over and over: a team normalizes everything perfectly, then realizes they need derived values — counts, statuses, aggregations — and the only options are fragile triggers, brittle application code, or worst of all, a spaghetti of custom scripts. The industry has taught us that denormalization is a necessary evil, a trade-off you accept with a grimace.

But what if you could make denormalization principled? Deterministic? Safe enough to declare right in your schema?

That’s exactly what Denorm does. It’s a Postgres extension that lets you define derived columns declaratively — as part of your schema, not as an afterthought. It moves data along foreign keys: down from parent to child, aggregate from child to parent, within rows. It traps cycles, guarantees idempotency, and eliminates race conditions. And the best part? You can get it running in 10 to 20 minutes.

Think about the last time you had to manually maintain a user_count column on an organization table. You wrote a trigger on insert, delete, and update. You hoped you didn’t miss a case. You tested it. Then six months later, a new developer added a bulk import and the count went sideways. Sound familiar?

Denorm takes that entire class of bugs and makes them irrelevant. Every derived column you’ve ever written was a bug waiting to happen. Now you can write them once, declaratively, and move on.

Here’s how it works. You define a table:

table us_states
  column state_code char(2) primary key
  column state_name varchar(20)
  column children_count integer count child_table

table child_table
  column child_id serial primary key
  fk us_states references us_states pushes child_table
  column state_code is us_states.state_code
  column state_name sync us_states.state_name

That’s it. The children_count is automatically maintained as children are added or removed. The state_name stays synced with the parent. No triggers. No app logic. No sleepless nights.

The trade-off is honest: writes become more expensive so reads, modeling, and your entire stack become simpler. It’s a pay-me-now, pay-less-later architecture. And for any system where reads vastly outnumber writes — which is almost every production system — that’s a bargain you take every time.

But here’s the twist that most people miss: The real problem isn’t that you’re denormalizing. It’s that you’re not doing it declaratively. The moment you make derived values part of the schema, you force the database to guarantee consistency. You stop relying on fallible humans and fragile code. You turn a chore into a declaration.

I built the first version of this in 2002, in PHP, because I was tired of the same pain. Twenty years later, the industry still hasn’t learned that denormalization can be principled. But now, with Denorm, you can finally stop treating it as a necessary evil and start treating it as a design tool.

If you’re a Postgres engineer who has ever had to untangle a data-inconsistency bug, or write a trigger you hated, or explain to a manager why the counts don’t match — this is for you. You don’t have to choose between normalization and sanity. You can have both.

Check out the README, install it in 10 minutes, and see if it doesn’t change how you think about schema design.

FAQ

Q: Doesn't this just add complexity to writes? Isn't that a bad thing?

A: Yes, writes become more expensive. But for most systems, writes are a fraction of reads. The trade-off is a massive simplification of read logic, application code, and data consistency. It's a classic pay-now, save-later trade-off that makes sense for any production database.

Q: What's the practical benefit I'd see in my first week using Denorm?

A: You'll stop writing triggers and sync scripts. You'll eliminate entire classes of bugs related to race conditions and stale derived data. Your schema will be self-documenting. And you'll spend less time debugging data inconsistencies and more time building features.

Q: Isn't denormalization always a code smell? How can this be principled?

A: Denormalization is a smell when it's done ad-hoc, without guarantees. Denorm makes it deterministic, idempotent, and schema-enforced. The key is that derived values become part of the database's contract, not application logic. That's not a smell — it's engineering discipline.

📎 Source: View Source