Skip to content
Open beta — everything’s free right now, and your rate is locked when it ends.

Tables, not tabs: database design for spreadsheet thinkers

You already model data — tabs, columns, color codes. Here's the same skill with different rules: one thing per table, a column per fact, a link where you'd have made a tab, and nothing stored that can be derived.

Updated August 2026

If you've run a serious spreadsheet, you already do data modeling. You decided what the columns were, what a row meant, which tab held what. Database design is the same activity with three rules added — and the rules are short enough to fit on this page.

One thing per table

A table holds one kind of thing, and you should be able to name it in a word: guests. Bookings. Orders. Products. The test is brutal and reliable — finish the sentence "each row is one ___". If the answer comes out "well, mostly bookings, but the top section is this year's rates", that's two tables sharing a grid, and they'll fight forever.

The reverse smell also counts. If two tabs answer the test with the same word — Bookings 2025 and Bookings 2026 are both "each row is one booking" — they're one table split by hand. Merge them, add a date field, and the split becomes a filtered view instead of a wall between years.

A row per thing, a column per fact

Each column states one fact about the row's thing: a booking's check-in date, an order's status. Two patterns break this rule, and both are worth catching early:

  • A fact about a different thing. Guest name, Guest email, Guest phone on every booking are facts about the guest, stored once per booking. Ten bookings, ten copies of the phone number, and the day it changes you update nine.
  • The same fact, once per column. Jan revenue, Feb revenue, Mar revenue is one fact — revenue — wearing twelve columns. The columns are hiding a date, and the cure is rows: one per month, with a month field. Rows are cheap in a database; columns are structure.

When a new tab should have been a linked table

The spreadsheet reflex, the moment guest details clutter the bookings tab, is to make a Guests tab and VLOOKUP across. The instinct is exactly right — that's a second kind of thing, so it gets its own table. What changes is the connection: instead of a formula matching names, a booking links to its guest. The link is a value in the cell, not a lookup that breaks when a name is misspelled or a column moves.

Same story in a shop: an Orders tab that repeats product names and prices per line wants a Products table, and each order links to the products in it. One product record; a hundred orders pointing at it; a price correction happens once.

The general rule: a new tab for a new kind of thing — as a linked table. A new tab for more of the same thing — never; that's a view. How links actually behave — which side holds them, what appears on the other table — is the subject of relationships, explained.

Naming

Small decisions you'll re-read for years:

  • Name the table for the thing, plural. Orders, not Order, and not Order tracking 2026 NEW — the table is the ongoing home of the things, not a document with a date on it.
  • Name a field for the fact, not the widget. Deliver by, not Delivery date dropdown v2. The type lives in the field itself, so the name doesn't have to carry it.
  • Don't number-prefix fields to force an order. Column order is arrangement, not data — drag them, or let each view arrange its own.

The multiplying status columns

A specific smell with a specific fix. The sheet grows a checkbox column per stage: Confirmed?, then Preparing?, then Shipped?, then Delivered?. Four columns, fifteen legal-looking combinations, one of which — everything checked at once — means nothing, and the truth ("where is this order?") smeared across all four.

Those four columns are one fact: status. The fix is a single select field with the stages as its choices — an order is "Preparing" or "Shipped", never both, and a board view can stack orders by it, which four checkboxes could never draw.

The honest boundary: merge columns only when they're stages of one lifecycle. Paid? isn't a stage of delivery — an order can be delivered and unpaid — so it stays its own checkbox. If two "statuses" can be true at the same time, they were different facts all along.

Don't store what you can derive

The last spreadsheet habit to unlearn: typing in what the data already knows. A Total typed next to Nights and Rate is correct until someone edits the nights and not the total — and nothing marks it stale. Derived values belong to fields that compute: same-row arithmetic in a formula, a linked record's value shown through a lookup, a group summed by a rollup. The full sorting of those three is lookup vs rollup and formula, lookup or rollup — the design rule here is only: if it can be computed, don't type it.

One exception, worth memorizing because invoices depend on it: a value that must not move when its inputs do — the price actually charged, on the day — is history, not derivation. Store that one deliberately.

Where this lands

Model the guests/bookings sheet by these rules and you get: a Guests table (a row per person), a Bookings table (a row per stay, linked to its guest, with a Status select), and every yearly, monthly or per-guest angle as a view. The shop version: Products, Orders, links between them, totals derived.

That's the whole craft. Two or three tables, honestly named, nothing stored twice, nothing typed that can be computed — most working databases are exactly this shape, and the ones that aren't usually wish they were. If you're building it in Tabla, your first database walks the same ground with the actual buttons.

Tabla is the database we build these on.

A no-code database with real Postgres underneath: every feature on every plan, a million records per database, and your whole database back out in one file, any day.

More guides