Database Normalisation: The Hidden Cost of Bad Data Hygiene

When the same current address is copied into every order, changing that address becomes a reconciliation task. In the hypothetical design below, we would separate the customer's current details from the order record. The important question is which fact each record represents, not how many tables the diagram contains.
Database normalisation concerns structure. E. F. Codd's tutorial on normalised database structures describes simplifying database relations through normalisation. For an engineering team, we recommend starting with a narrower practical question: which repeated facts must change together, and what prevents them disagreeing?
Decide what the repeated value means
Consider a hypothetical logistics application. Each order contains a customer identifier and an address. Before moving the address into a customer table, ask the operations team whether it means the customer's current correspondence address or the delivery destination agreed for that order.
For current correspondence, our proposed design would store the address against the customer and reference that customer from orders. A correction would target that customer record. We would test that every screen requesting current contact details reads the corrected value rather than its own independently maintained copy.
For a delivery destination, we would preserve the address agreed when the order was placed. A later customer move should not change where that completed delivery appears to have gone. Treat this as an explicit business requirement, then test it. Do not remove a historical value merely because it matches a current one today.
We would also reject a brief that defines normalisation as splitting an address into postcode and city fields. Specify useful address fields if the workflow needs them, but review the relationships separately. Ask what identifies the customer, what identifies the order, and which record owns each kind of address.
Turn the relationship into a database rule
PostgreSQL documents that a primary key identifies rows with unique, non-null values. Its foreign-key constraints require referencing values to match a referenced row, subject to null handling. Those mechanisms give our proposed customer and order relationship something testable beyond a drawing.
For this example, make customer identity an explicit requirement. Try creating an order with an unknown customer identifier. Where an order must always have a customer, test a missing identifier too and require the database to reject it. Do not treat a successful screen-level check as proof that imports follow the same rule.
Agree the deletion behaviour before the migration. We would ask whether deleting a customer should be prohibited while orders exist, or whether a separate retention process should preserve historical order evidence. Put the chosen behaviour in an acceptance test. Do not let a developer infer the policy from the easiest relationship setting.
Keep structural checks separate from accuracy
Suppose our hypothetical application contains two customer records for the same organisation, each with a different identifier. Both satisfy the proposed identifier rule. The rule alone gives us no reason to prefer either postal address. We would therefore make duplicate resolution a separate task, with an owner and recorded evidence for the decision.
Before merging those customers, compare the orders attached to each. Require a reviewer to approve the reassignment and keep a record of the old identifiers. For uncertain matches, leave the records separate and flag the uncertainty. Do not use a similar name as permission to rewrite order history.
Use the same discipline for imported values. Write down which address fields are required for this workflow and what staff should do when they cannot establish an accurate value. Prefer an explicit unresolved case to an invented postcode inserted merely to satisfy a form.
Test updates as well as stored rows
Normalisation is not our acceptance test for concurrent editing. PostgreSQL's Read Committed isolation documentation explains that this is its default isolation level. An ordinary SELECT sees a snapshot at the start of the query, including changes previously made by its own transaction. Successive queries can therefore see different committed data.
For our example, test a customer edit while an order is being prepared. Specify whether the order should keep the address already confirmed or require a new confirmation. Ask the team to demonstrate the selected behaviour with overlapping operations, rather than assuming a tidy schema settles the question.
If a device can submit work after losing connectivity, add a stale-address scenario to that test. Decide what the server should accept and what should return for human review. Keep this decision separate from the question of where the authoritative current address is stored.
Measure the correction work before expanding
We recommend beginning with one relationship and recording the correction process before changing it. Note which records staff must inspect, which systems they update and what evidence closes the case. Repeat the same exercise against the proposed design. Treat any reduction as a result to demonstrate, not a benefit to assume.
For the migration, prepare a reconciliation report showing how old customer identifiers map to new ones. Compare order ownership and historical delivery addresses before approving the change. Require unresolved cases to remain visible. Agree how the team would return to the previous design if the comparison fails.
Keep performance acceptance separate too. Require representative reads and writes to meet the application's agreed response requirements before release. We would not attach a savings figure to this work without a measured operational change and a separate explanation of how that change affects the organisation's actual costs.
The useful outcome is a specific correction that has become easier to make without changing unrelated history. Ask the team to demonstrate that outcome before extending the approach to another relationship.
Update, October 2026
For teams asking coding agents to change a schema, we recommend putting these record meanings and acceptance tests in the brief before implementation. Our engineering article What the Agent Does Not Know to Look For describes preparing context with provenance and dates. Use that preparation to make the database assumptions available to a separate reviewer, rather than asking the implementer to decide what the records ought to mean.
Sources
- IBM Research, "Normalized data base structure: A brief tutorial", 1 December 1971.
- PostgreSQL Global Development Group, "PostgreSQL 17.0: Data Definition", 26 September 2024.
- PostgreSQL Global Development Group, "PostgreSQL 17.0: Concurrency Control", 26 September 2024.
- PostgreSQL Global Development Group, "PostgreSQL 17 Released!", 26 September 2024.
- Dendro Logic, "What the Agent Does Not Know to Look For", 6 September 2026. Referenced only in the dated update.
Want this in your team?
Tell us what you're building and where you want to get to, and we'll think it through with you.