Skip to content
wannacode

Natural keys and denormalization

In short: A natural key comes from the data itself, like an email, a surrogate key is a number with no meaning, and denormalization is a copy of data kept on purpose.

Problem

Maria changed her email

If you design the customers yourself, the email looks like a good primary key. It is unique, every customer has one, it is what they log in with. Here is such a design: customers are keyed by email, and orders point to the email.

customers

  • PKemailtext
  • nametext *
  • citytext

orders

  • PKidinteger
  • FKcustomer_emailtext *→ customers
  • ordered_attext *

Maria changes her email:

SQLempty database

The database refuses the change, because three orders point to the old email. If you change those too, there are still the invoices already sent, the reports and every other system that has remembered the old key.

The idea

Natural and surrogate keys

A key taken from the world (an email, a phone number, a book's number) is a natural key. A number the database assigns itself, with no meaning outside it, is a surrogate key. The id in the shop is one.

A surrogate key never changes, because nothing in the world changes it. So the email changes on one row, and the orders do not notice:

SQLdatabase: shop

The email keeps its rule. It is no longer the key, but it is UNIQUE, so two customers cannot have the same email:

SQLdatabase: shop

What about a national ID number (in Bulgaria, the EGN)? No. It is personal data, and the key gets copied into every table that points to the customer. A customer from abroad usually does not have one. And the shop has no need to know it.

More precisely, if you are curious

ON UPDATE CASCADE in REFERENCES makes the database also change the rows that point to the old key. That helps inside the database, but not with the copies of the key outside it: in emails, in logs, in another system.

A natural key fits when the value really never changes and is known from outside. Examples are a currency code (EUR) or a country code (BG).

A UUID is another kind of surrogate key: a 128-bit number written as 550e8400-e29b-41d4-a716-446655440000. It can be made anywhere, without asking the database (in the app, on a phone with no internet), two databases can be merged without clashes, and the address /orders/1043 no longer tells how many orders the shop has. The cost is that it is longer (16 bytes against 8 for a big integer), and random UUIDs land in random places in the index and slow it down. That is why there are also time-ordered UUIDs (version 7). PostgreSQL has a uuid type and a gen_random_uuid() function. SQLite has no separate type and keeps it as text or as a BLOB.

Try it

A new email

Customer 12, Boris, writes that his new email is boris.m@example.com. Fix it.

SQLdatabase: shop

The idea

The price in the order row

order_items has unit_price, and products has price. That looks like a repeat that normalisation removes. Raise the price of the coffee:

SQLdatabase: shop

The old orders remember 14.90 €, while the product costs 15.90 €. These are not two copies of one fact, but two different facts: “what it costs today” and “what the customer paid”. If the order took its price from products, every price rise would change past bills too.

1. The coffee in products now costs 15.90 €. What should the old order 8 show?

The idea

Denormalisation

The order total is different. It really is computed from the items. But the list of orders on the website shows it for every order, and instead of adding up the items every time, you can keep it in orders:

SQLdatabase: shop

A deliberate copy of data that could be computed is called denormalisation. You do it when reads are much more frequent than changes and the computation is expensive.

See it

The total that lies

A colleague adds coffee to order 60 but forgets about total:

SQLdatabase: shop

The website shows 14 €, while the items add up to 28.90 €. Nothing in the database stopped this. The cost of the copy is that every change to the items must also change the total: in the program, in every place that writes to order_items, or with a trigger (the transactions module).

Exercise

Two tasks

1. Accounting suspects some totals are wrong. Find the orders where total is not equal to the sum of the items. Show the number, the stored total and the real one.

The orders with a wrong total:

SQLdatabase: shop

2. Fix the totals of the two orders by computing them again from the items.

Update total for orders 59 and 60:

SQLdatabase: shop

Test

Check yourself

1. Which one is a surrogate key?
2. The shop uses id as the key. Should the email still be UNIQUE?
3. Why does unit_price in order_items not break normalisation?
4. A total column in orders keeps the sum of the items. What is the cost?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·