Moving a Google Sheet into a database without breaking it
Using Google Sheets as a database: where it works, where it stops, and how to move a sheet into real tables without breaking it.
Updated September 2026
Uploading the CSV takes a minute. The reason people bounce off this is everything that happens next: the sheet arrives intact and wrong, because a spreadsheet stores decisions a database wants stated explicitly.
Can you use Google Sheets as a database?
For plenty of work, yes. One person, a few thousand rows, one kind of thing per tab, nothing else reading it — that is a database with an unusually good editor attached. Google publishes the hard ceiling: 10 million cells, or 18,278 columns, per spreadsheet. Divided by a 40-column sheet, that's 250,000 rows.
The ceiling is rarely what arrives first. Three other things do.
Nothing enforces what a column holds. A date column accepts next Tuesday,
a status column accepts Confimed, and neither is refused at the door. You find
both later, in a total that doesn't add up.
There are no relationships, only matched text. A VLOOKUP joins two tabs on a name somebody typed, so "Acme Ltd" and "Acme Ltd." are two clients, and renaming one breaks every formula pointing at it. That's the part people mean by relational, and it's the one a sheet can't hand you — what a relationship actually is.
Other software reads a sheet as a document. Google's Sheets API allows 60 read requests a minute per user, 300 per project, and each one returns a rectangle of cells; filtering, sorting and paging are yours to write on top. That's fine for a monthly pull and thin for something answering questions all day.
Once two of those are true, the sheet is doing a database's job without a database's guarantees. Here's the order that avoids the rework.
Before you import: three questions about the sheet
1 · Which columns are actually a second table?
This is the big one, and it's usually the only real work.
If your sheet has Client name, Client email and Client phone repeated on
every row, those three columns aren't columns — they're a Clients table
you've been storing a copy of on every line. Ten bookings for the same client
means ten copies of their email, and ten chances for one of them to be the old
one.
Look for any group of columns that repeat together and always change together. Each group is a table, and the original sheet becomes a table that links to it.
2 · Which columns are a type pretending to be text?
Scan for columns where every value is drawn from a small fixed set — Status
with Pending / Confirmed / Cancelled, Priority with High / Medium / Low. Those
want to be single-select fields, not text. The moment they are, filtering is
exact, colouring is possible, and nobody can type "Confimed".
Dates deserve their own look. A spreadsheet column that displays as a date may hold text in some rows — the ones somebody typed by hand, or pasted from an email. Sort the column: text and real dates sort differently, and the strays group together at one end where you can see them.
3 · Which formulas should stop being formulas?
Three different answers, and choosing right is what stops your data drifting:
- Arithmetic on the same row (
= nights * rate) stays a formula. - A VLOOKUP into another sheet becomes a link plus a lookup — you're not computing anything, you're pointing at a record.
- A SUMIF over many rows becomes a rollup on the other side of that link. (There's a longer version of this in lookup vs rollup.)
- A number that must never change — the price you actually charged, on the day you charged it — stops being derived at all and becomes a plain stored value. Recomputing history is not a feature.
Clean these five things first
Faster in the sheet than after the import, every time — and it's the same list that makes a sheet work as a database while it still is one:
- One header row. Delete the title row above it, the merged cells, the blank spacer row. An importer reads row 1 as your field names.
- No totals row at the bottom. It'll arrive as a record with the word "TOTAL" in the name column. That belongs to a view, not the data.
- Nothing to the right. Notes parked in column P become a field.
- No blank rows in the middle. They import as empty records.
- One sheet, one thing. If a tab holds two tables stacked with a gap, split them before exporting.
Then import
File → Download → Comma-separated values, one tab at a time. Our importer reads CSV and Excel, reads the header row as field names, guesses each column's type, and shows you every guess before it creates anything.
You can change any guess in that preview — each column has a type selector, and you can rename it or skip it entirely. Importing into a table you already have adds a third option, mapping the column onto an existing field.
Check the guesses. The ones that go wrong most often are:
- A reference like
CM-1041guessed as text — correct — versus one like001042where leading zeros matter and a number type would eat them. - A column of
Yes/Nothat you'd rather have as a checkbox.
Dates written with slashes have their own control. 03/04/2026 is 4 March
or 3 April depending on who wrote the file, and nothing in the value says which.
The importer looks for a day above 12 — the only real evidence a column
contains — and sets Read dates as to match. When no sample settles it, the
control defaults to month-first and says so on the row: "No day above 12 here,
so both readings fit — worth a look." Change it there if the sheet is
day-first. Reformatting the column to YYYY-MM-DD before you export removes the
question entirely.
Then build the links
The importer creates plain fields. Links and attachments are never on its list, because a cell in a CSV holds no record ids and no file bytes. So you import the second table (Clients, Suppliers, whatever the repeated columns became), and then turn the text column on the first table into a link. That's one dialog now, not an afternoon of retyping.
One thing first, on the second table: the field holding those names has to be its plain-text display field. That's what the values match against, and nothing else.
Then, on the column itself, click the arrow on its header and choose Link to
another table…. You pick the table to match against, name the new field, and
say whether a cell holding A, B means two links or one. Before it runs, the
dialog counts against your real data: how many records hold a value, how many
values already name a record over there, and how many name nothing yet — a
record is created for each of those.
Matching ignores capitals and spaces around the value, so acme ltd finds
Acme Ltd. It does not forgive the full stop, so duplicates and spelling
variants are still worth fixing in the sheet first.
Nothing is converted: the new link field appears beside the original column, and deleting that column is offered afterwards, once you've seen the result. That deletion is the moment the sheet stops being a sheet. The longer version is in linked records.
What you get for the afternoon
The columns can't drift type. The client's email exists once. A view replaces every duplicated tab. Other software can read it over an API instead of screen-scraping a share link. And you can still get a CSV back out any day you like — the whole database, one file, which is the part worth confirming before you start rather than after.
If none of that sounds like your week, the sheet is still doing its job — the six signs it has stopped are the thing to watch for.