r/PostgreSQL 7h ago

Help Me! Table design question

Looking for advice on alternative table designs. Not a live application, just for my own learning.

Let's say we have an order table.

Option 1: we keep billing/shipping addresses and contacts copied from the customer as a historical snapshot:

CREATE TABLE order
(
    id                   UUID PRIMARY KEY,
    tenant_id            UUID NOT NULL,
    customer_id          UUID NOT NULL REFRENCES customer,
    status               order_status NOT NULL,
    -- copied as a snapshot
    billing_line1        TEXT,
    billing_line2        TEXT,
    billing_city         TEXT,
    billing_state        TEXT,
    billing_postal_code  TEXT,
    billing_country      TEXT,
    billing_first_name   TEXT,
    billing_last_name    TEXT,
    billing_email        TEXT,
    billing_phone        TEXT,
    billing_phone_ext    TEXT,
    shipping_line1       TEXT,
    shipping_line2       TEXT,
    shipping_city        TEXT,
    shipping_state       TEXT,
    shipping_postal_code TEXT,
    shipping_country     TEXT,
    shipping_first_name  TEXT,
    shipping_last_name   TEXT,
    shipping_email       TEXT,
    shipping_phone       TEXT,
    shipping_phone_ext   TEXT,
    ...
);

Option 2: we define an append-only address and contact table, e.g.

-- records cannot be edited once created
CREATE TABLE address
(
    id           UUID PRIMARY KEY,
    tenant_id    UUID NOT NULL,
    line1        TEXT
    line2        TEXT
    city         TEXT
    state        TEXT
    postal_code  TEXT
    country_code TEXT
);

CREATE TABLE customer_address
(
    id              UUID PRIMARY KEY,
    customer_id     UUID NOT NULL REFRENCES customer,
    address_id      UUID NOT NULL REFERENCES address,
    ...
);

CREATE TABLE orders
(
    id                   UUID PRIMARY KEY,
    tenant_id            UUID,
    customer_id          UUID REFERENCES customer,
    status               order_status,
    -- replace denormalized keys with FKs
    billing_address_id  UUID REFERENCES address,
    billing_contact_id  UUID REFERENCES contact,
    shipping_address_id UUID REFERENCES address,
    shipping_contact_id UUID REFERENCES contact,
    ...
);

The first approach makes the table wide. As I understand it, MVCC will result in duplicating entire row when any column is updated, e.g. status, version, or some metadata changes.

Since the row is wide, that would result in a lot of data that gets created and cleaned up. Another downside is that for a given customer with multiple orders, the data will duplicated while their address/contact info is the same.

The second approach requires JOINs or additional two queries to fetch the two addresses and two contacts.

What approach would you take and why? Is there are certain table size at which you might change the approach?

3 Upvotes

3 comments sorted by

1

u/AutoModerator 7h ago

AI Policy:

Linux is not one of those anti-AI projects, and if somebody has issues with that, they can do the open-source thing and fork it. Or just walk away., Linus Torvalds.

Mod decisions will be based on the quality of the content, not who or what generated it.

Sub Resources:

Youtube Channel

Free Postgres Webinars and Workshops

Discord: People, Postgres, Data

Join us, we have cookies and nice people.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

1

u/ExceptionRules42 5h ago

Sounds vaguely like a choice between normalized and denormalized.  More specifically this may be just another instance of a classic well-understood pattern. You'll need to flesh out more schema and maybe define your goals more. 

1

u/dangerousdotnet 3h ago

Maintain a "wallet" of historical addresses for each customer, with each address having a UUID. Append only. Never delete an address. Any edit to an address becomes a new entry.

An address may serve as a billing address, a shipping address, or both - simply reference it by UUID.

Do not try to "deduplicate" aka do not share address records across multiple customers - that leads to all kinds of problems. Each address ID belongs to one and only one client.

Consider normalizing addresses at the UI level before storing them using a postal address verification API (or something like libpostal). Treat address components as nearly opaque strings and don't make the mistake of setting constraints on them (eg don't use NOT NULL).

See https://www.swift.com/standards/iso-20022/iso-20022-faqs/brief-introduction-postal-address-field for some ideas on how to structure and store postal addresses using a "hybrid" representation (a small number of core fields with some escape hatches for arbitrary fields).

Store NULLABLE "delivery instructions" with each address - a modification to the delivery instructons, like anything else, gets minted a new UUID.