Back to blog

Spreadsheet vs Database: When a Workbook Has Become a System

Spreadsheet vs database: what Excel validation lets through, the 2026 limits, an inventory macro for any workbook and a migration order that keeps its rules.

Luka Abramovic21 min read

Isometric spreadsheet rows reorganized into a structured database with linked records.

Size matters less in the spreadsheet vs database question than what happens when someone enters a wrong value. A database built with the rule refuses it, whoever or whatever sends it. Excel's data validation only checks what people type: Microsoft's documentation says that when data is copied or filled, the validation messages don't appear (More on data validation). One pasted column can break every rule on a sheet without a warning. To see which values already do, go to the Data tab, click the arrow next to Data Validation and choose Circle Invalid Data. Each red circle is a value the sheet's own rules say shouldn't be there.

This guide is for the workbook that runs part of a business, such as quoting, job tracking or stock. Limits are as of September 2026. It ends with an inventory macro, a decision table and a migration order that moves the rules before the data.

Where Excel and a database enforce eight kinds of rule, from allowed values and data types to unique IDs, transactions and history, with when to keep the spreadsheet and when to move to a database.
At a glance. The sections below give the detail and the sources.

1. What a database enforces and a spreadsheet only suggests

A database checks its rules at the moment of writing, for every writer: a person, an import or another system. A spreadsheet checks some rules, for some writers, some of the time.

Where each kind of rule is enforced
RuleIn ExcelWhat gets past itIn a database
Allowed values, such as a status list or a positive amountData validationCopied and filled data, with no messageA CHECK constraint or a lookup table, applied to every insert and update
A value is a number, a date or textExcel guesses the type as you type or open a file, and converts it: 00123 becomes 123, JAN1 becomes a date, and a 16-digit number keeps 15 digitsAnything that looks like another type. Switching the conversions off (File > Options > Data) doesn't bring back digits already lost.Column types. A value of the wrong type is rejected, not converted.
Required fieldsNothing, unless a formula or macro checksAny blankNOT NULL
Unique IDsOnly a custom validation formula, such as =COUNTIF($A:$A,A2)=1, which checks typed entriesPasted rows, and two people typing "the next number" at the same timeA primary key or UNIQUE constraint, with IDs the database generates
Every job belongs to a real customerA lookup formula into another sheetDeleting or renaming the customer's row: the lookup breaks and nothing stops the deleteA foreign key: the delete is refused while jobs still point at the customer
Rules written in codeA Worksheet_Change macroCells changed by recalculation, which don't fire the event; anyone editing in a browser, since VBA runs in desktop Excel; anyone with macros disabledConstraints and triggers, which apply to every connection
Changes that belong togetherA macro that updates three sheetsA macro that stops halfway, leaving two of the three updatedA transaction: all of it or none of it
HistoryShow Changes lists who changed which cells and when. Version History restores whole files."What did this job say on the 1st?" means opening old copies of the fileRow history you can query as of any moment (below)

Sources: Microsoft on the Worksheet.Change event ("does not occur when cells change during a recalculation"), VBA and Office Scripts (VBA is built into desktop Excel; Office Scripts have no events) and Show Changes. Our bulk import guide covers the conversions that damage imported IDs, with a check for each.

Test the validation you already have

  1. On every sheet with drop-down lists or input rules, run Circle Invalid Data to see where the invalid values sit. The inventory macro in section 4 counts them for each rule.
  2. For each circled value, ask who entered it and how. Paste and fill are the two routes validation doesn't cover.
  3. If the sheet stays a spreadsheet, close the easier route. Microsoft's fix is File > Options > Advanced, clear Enable fill handle and cell drag-and-drop, then protect the sheet. That option belongs to each person's Excel, not to the file (Application.CellDragAndDrop), so every editor has to clear it. Pasting into unlocked cells still works, so repeat the check every week.

Row history, which a spreadsheet can't give you

In SQL Server, a system-versioned temporal table copies the earlier version of a row into a history table every time the row is updated or deleted, stamped with the transaction's time in UTC. The table needs a primary key, and the feature is in every edition, including the free Express (temporal tables, SQL Server 2025 editions):

CREATE TABLE dbo.Jobs (
    JobNo      varchar(20)   NOT NULL PRIMARY KEY,
    Status     varchar(20)   NOT NULL,
    QuoteTotal decimal(12,2) NULL,
    ValidFrom  datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    ValidTo    datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.JobsHistory));

-- What did job J-1042 say at midnight UTC on August 1?
SELECT * FROM dbo.Jobs FOR SYSTEM_TIME AS OF '2026-08-01' WHERE JobNo = 'J-1042';

It records what changed and when. To record who, the application writes the signed-in user into a column on every save.

2. Several editors: what co-authoring fixes and what it leaves

Co-authoring lets several people edit one workbook at the same time if it's stored in OneDrive, OneDrive for Business or SharePoint Online. With AutoSave on, changes appear in real time (Microsoft). A workbook on a file server doesn't get it, so people take turns or save their own copies. Three problems remain even with co-authoring:

  • Macros don't share memory. Excel synchronizes cells, not the values a macro or add-in holds in memory. A macro that read the next job number when the file opened can hand the same number to two people.
  • Appending rows can lose edits. Microsoft's add-in guidance (June 2026) says that when an add-in adds table rows while co-authors edit the same table or nearby cells, Excel detects a merge conflict: a yellow bar asks people to refresh, and recent changes might be lost. Test any tool that appends rows to a shared table with two people editing at once.
  • Nothing arbitrates. When two people change the same cell, the later edit wins and nothing checks whether its author saw the earlier one. A database can make the second write wait, and with a version number on each row, the application can refuse a save based on data that has since changed: optimistic concurrency.

For Sheets, Google documents a cap: up to 100 people with view, edit or comment access at once, beyond which only the owner and some editors can edit (Google Workspace). Few internal sheets get near it, so for Sheets the rules and history in section 1 decide.

Two checks for this week

  1. Count the writers. Open Show Changes on the busiest sheet and list everyone who edited it in the period shown. For a file on a file server, ask who saves it on a typical day. Our rule of thumb: two or more people adding or changing rows in the same table on most days is a database workload, however small the table.
  2. Look at how IDs are made. A number typed as "the last one plus one" collides when two people add rows at once. An ID computed by a formula, such as =MAX(A$1:A1)+1 or =ROW()-1, is worse: it recalculates when rows are sorted, inserted or deleted, so a job can change its number after it was quoted. The uniqueness check in our bulk import guide finds the collisions that already happened.

3. The limits, and which one arrives first

Limits that bear on the decision, as of September 2026
LimitValueWhat it means for the decision
Excel worksheet1,048,576 rows by 16,384 columns per sheet (Microsoft)Caps the biggest single table, usually a log of lines or transactions
Excel Data Model (Power Pivot)In 32-bit Office, 2 GB per workbook and 4 GB of memory (Microsoft)Holds data outside the grid for analysis. It solves size and does nothing for editing or rules.
Google Sheets20 million cells per spreadsheet, counting every tab, up from 10 million. Google announced general availability on September 10, 2026, and set September 28 as the start of the rollout to Scheduled Release domains (Google).The cap is in cells, so a wide table fills it faster than a long one
Office Scripts in Excel on the web5 MB per request and five million cells per range. From Power Automate, 1,600 script runs per user per day (Microsoft).If flows and scripts feed the workbook, it's already a system, and these are its caps
SQL Server Express, the free edition2025: 50 GB per database, the lesser of 1 socket or 4 cores, 1,410 MB of buffer pool memory. 2022: 10 GB. Neither includes SQL Server Agent (2025, 2022).The database engine is rarely the cost. Without Agent, scheduled jobs need another scheduler.

A worked example: rows against cells

An illustrative example. A job-costing workbook has a Lines tab 40 columns wide with 310,000 rows, growing by 9,000 rows a month. Its other tabs (customers, rates and lists) use 150,000 cells.

Months of room left in the same workbook (illustrative inputs)
Where it livesArithmeticMonths of room
Excel, 1,048,576 rows per sheet(1,048,576 − 310,000) ÷ 9,000About 82
Google Sheets, 20 million cells(20,000,000 − 310,000 × 40 − 150,000) ÷ (9,000 × 40)About 20
Google Sheets at the old cap of 10 million cells310,000 × 40 = 12.4 million cells on one tabNone. It wouldn't have fit.

Both leave room, and that's the trap: neither limit measures the problems in sections 1 and 2, and a pasted value or a lost edit can hit a workbook of 200 rows. Size alone is an analysis problem (section 5).

4. Inventory the workbook before you decide

Our legacy system examples show where a workbook hides rules inside its formulas, with a two-line check for hidden sheets and names. The macro below lists, in one pass, what the workbook reads, what runs inside it and what runs on its owner's PC, on a new sheet you can hand to anyone quoting a replacement.

In desktop Excel for Windows

  1. Open a copy of the workbook, not the live file.
  2. Let the macro read the workbook's VBA: File > Options > Trust Center > Trust Center Settings > Macro Settings, then tick Trust access to the VBA project object model. Microsoft recommends clearing it again as soon as the macro has run (security notes).
  3. Press Alt+F11, choose Insert > Module and paste the code. Click inside InventoryWorkbook and press F5. The module you pasted appears in the list too; ignore it.
' Lists what makes a workbook a system on a new sheet. Run it on a copy.
Private inv As Worksheet
Private r As Long

Sub InventoryWorkbook()
    Dim wb As Workbook, ws As Worksheet, nm As Name, q As Object
    Dim cn As WorkbookConnection, links As Variant, rng As Range
    Dim comp As Object, ad As AddIn, i As Long, n As Long, s As String

    Set wb = ActiveWorkbook
    Set inv = wb.Worksheets.Add(Before:=wb.Sheets(1))
    inv.Name = "Inventory " & Format(Now, "yyyy-mm-dd hhnn")
    r = 1
    AddRow "Type", "Name", "Where", "Detail"

    For Each ws In wb.Worksheets
        If ws.Visible = xlSheetVeryHidden Then AddRow "Very hidden sheet", ws.Name, "", ""
        If ws.Visible = xlSheetHidden Then AddRow "Hidden sheet", ws.Name, "", ""
        If ws.ProtectContents Then AddRow "Protected sheet", ws.Name, "", ""
        Set rng = Nothing
        On Error Resume Next
        Set rng = ws.UsedRange.SpecialCells(xlCellTypeAllValidation)
        On Error GoTo 0
        If Not rng Is Nothing Then
            For i = 1 To rng.Areas.Count
                AddRow "Validation", ws.Name, rng.Areas(i).Address(False, False), _
                    RuleOf(rng.Areas(i).Cells(1, 1)) & " | " & CountInvalid(rng.Areas(i)) & " invalid now"
            Next i
        End If
        Set rng = Nothing
        On Error Resume Next
        Set rng = ws.UsedRange.SpecialCells(xlCellTypeAllFormatConditions)
        On Error GoTo 0
        If Not rng Is Nothing Then AddRow "Conditional formats", ws.Name, rng.Address(False, False), rng.CountLarge & " cells"
    Next ws

    For Each nm In wb.Names
        AddRow "Name", nm.Name, IIf(nm.Visible, "visible", "hidden"), nm.RefersTo
    Next nm

    links = wb.LinkSources(xlExcelLinks)
    If Not IsEmpty(links) Then
        For i = LBound(links) To UBound(links)
            AddRow "Reads workbook", "", "", links(i)
        Next i
    End If

    For Each q In wb.Queries
        AddRow "Power Query", q.Name, "", q.Formula
    Next q

    For Each cn In wb.Connections
        s = ""
        On Error Resume Next
        If cn.Type = xlConnectionTypeOLEDB Then s = cn.OLEDBConnection.Connection
        If cn.Type = xlConnectionTypeODBC Then s = cn.ODBCConnection.Connection
        On Error GoTo 0
        AddRow "Connection", cn.Name, "", s
    Next cn

    On Error Resume Next
    n = wb.VBProject.VBComponents.Count
    If Err.Number <> 0 Then
        AddRow "VBA", "", "", "Not readable: allow access in the Trust Center, or read it in the VBA editor"
        Err.Clear
    Else
        For Each comp In wb.VBProject.VBComponents
            n = 0
            n = comp.CodeModule.CountOfLines
            If n > 0 Then AddRow "VBA module", comp.Name, "", n & " lines"
            For i = 1 To n
                s = Trim(comp.CodeModule.Lines(i, 1))
                If s Like "Sub *" Or s Like "Private Sub *" Or s Like "Public Sub *" Then
                    If InStr(s, "Workbook_") > 0 Or InStr(s, "Worksheet_") > 0 Or InStr(s, "Auto_Open") > 0 Then
                        AddRow "Event macro", comp.Name, "line " & i, s
                    End If
                End If
            Next i
        Next comp
    End If
    On Error GoTo 0

    For Each ad In Application.AddIns
        If ad.Installed Then AddRow "Add-in on this PC", ad.Name, ad.FullName, ""
    Next ad
    s = Dir(Application.StartupPath & "\*.*")
    Do While s <> ""
        AddRow "Opens with Excel on this PC", s, Application.StartupPath, ""
        s = Dir
    Loop

    inv.Columns("A:C").AutoFit
End Sub

Private Sub AddRow(ByVal kind As String, ByVal itemName As String, ByVal location As String, ByVal detail As String)
    ' The apostrophe keeps values such as "=Sheet1!A1" as text
    inv.Cells(r, 1).Value = "'" & kind
    inv.Cells(r, 2).Value = "'" & itemName
    inv.Cells(r, 3).Value = "'" & location
    inv.Cells(r, 4).Value = "'" & Left(detail, 32000)
    r = r + 1
End Sub

Private Function RuleOf(ByVal c As Range) As String
    On Error Resume Next
    RuleOf = "type " & c.Validation.Type
    RuleOf = RuleOf & ": " & c.Validation.Formula1
End Function

Private Function CountInvalid(ByVal area As Range) As Long
    ' Validation.Value is False when a cell breaks its own rule
    Dim c As Range, ok As Boolean
    On Error Resume Next
    For Each c In area.Cells
        If Not IsEmpty(c.Value) Then
            ok = True
            ok = c.Validation.Value
            If Not ok Then CountInvalid = CountInvalid + 1
        End If
    Next c
End Function

How to read the new sheet:

  • Very hidden sheet. Set from code, and missing from the Unhide list. Check whether visible formulas or validation lists point at it.
  • Validation. The number is the rule type: 3 is a list and 7 a custom formula (XlDVType). "Invalid now" counts filled cells that break the rule today (Validation.Value), whether pasted past it or entered before it changed. A list that points at a hidden sheet is a rule someone maintains out of sight.
  • Conditional formats. Colors that mean something, such as red for overdue, are rules the new system has to state in words.
  • Reads workbook and Power Query. Every file and source this one reads. A path under C:\Users\ means the refresh works on one person's PC only.
  • Connection. Connection strings name servers and databases. If one contains a password, the password is stored in the file.
  • Event macro. Workbook_Open runs whenever the file opens; Worksheet_Change runs on edits, with the gaps listed in section 1.
  • Add-in and "opens with Excel". Code that lives on this PC rather than in the file: installed add-ins, and files in Excel's startup folder, which open whenever Excel starts. Nobody else has them, and they go when this PC does.

With an Office Script, in Excel on the web or on a Mac

Office Scripts run in Excel on the web, for Mac and for Windows (version 2210 or later), with OneDrive for Business and a Microsoft 365 business or education license (Microsoft). They can't read VBA, connection strings or a query's source, so this script covers the rest. Create a new script on the Automate tab and paste it:

// Lists hidden sheets, names, queries, links and validated ranges on a new sheet.
function main(workbook: ExcelScript.Workbook) {
  const rows: string[][] = [["Type", "Name", "Where", "Detail"]];
  const names: ExcelScript.NamedItem[] = [...workbook.getNames()];
  for (const sheet of workbook.getWorksheets()) {
    const visibility = sheet.getVisibility();
    if (visibility !== ExcelScript.SheetVisibility.visible) {
      rows.push(["Hidden sheet", sheet.getName(), "", visibility === ExcelScript.SheetVisibility.veryHidden ? "very hidden" : "hidden"]);
    }
    names.push(...sheet.getNames());
    const used = sheet.getUsedRange();
    const validated = used ? used.getSpecialCells(ExcelScript.SpecialCellType.dataValidations) : undefined;
    if (validated) rows.push(["Validated cells", sheet.getName(), validated.getAddress(), validated.getCellCount() + " cells"]);
  }
  for (const n of names) {
    rows.push(["Name", n.getName(), n.getVisible() ? "visible" : "hidden", "refers to " + n.getFormula()]);
  }
  for (const q of workbook.getQueries()) {
    rows.push(["Power Query", q.getName(), "loaded to " + q.getLoadedTo(), q.getRowsLoadedCount() + " rows, refreshed " + q.getRefreshDate()]);
  }
  for (const link of workbook.getLinkedWorkbooks()) {
    rows.push(["Linked workbook", "", "", link.getId()]);
  }
  const inventory = workbook.addWorksheet("Inventory");
  inventory.getRangeByIndexes(0, 0, rows.length, 4).setValues(rows);
}

Find the files that read this one

Excel shows which files a workbook reads, but nothing inside a workbook shows which files read it. An .xlsx or .xlsm file is a zip archive, and each link to another workbook is a small part inside it holding the linked file's path: xl/externalLinks/_rels/externalLink1.xml.rels, then externalLink2 and so on. So this PowerShell script can check a whole shared drive without opening anything in Excel:

# Lists every link from an .xlsx or .xlsm file under a folder to another workbook.
# It reads the files as zip archives, so nothing opens in Excel and no macros run.
Add-Type -AssemblyName System.IO.Compression.FileSystem
$root = '\\fileserver\shared'   # the folder to scan

Get-ChildItem -Path $root -Recurse -File -Include *.xlsx, *.xlsm -ErrorAction SilentlyContinue |
ForEach-Object {
    $file = $_.FullName
    $zip = $null
    try {
        $zip = [System.IO.Compression.ZipFile]::OpenRead($file)
        $parts = $zip.Entries | Where-Object { $_.FullName -like 'xl/externalLinks/_rels/*.rels' }
        foreach ($part in $parts) {
            $reader = New-Object System.IO.StreamReader($part.Open())
            $xml = [xml]$reader.ReadToEnd()
            $reader.Close()
            foreach ($rel in $xml.Relationships.Relationship) {
                [pscustomobject]@{ Workbook = $file; LinksTo = [uri]::UnescapeDataString($rel.Target) }
            }
        }
    } catch {
        Write-Warning "Could not read $file"
    } finally {
        if ($zip) { $zip.Dispose() }
    }
} | Export-Csv -Path .\workbook-links.csv -NoTypeInformation -Encoding UTF8

Open workbook-links.csv and filter the LinksTo column for your workbook's file name. Each row is a file that depends on yours keeping its name, place and layout. The linking file also keeps a copy of the values it last read, so a moved source can leave it showing old numbers instead of an error. A path with no folder, such as Prices.xlsx, is relative and works only while both files share a folder. The script covers .xlsx and .xlsm; older .xls files aren't zip archives.

This week: run the macro on a copy of your most important workbook and the link scan on the drive it lives on, then ask the workbook's owner to explain each row.

5. Keep it, harden it, put a database behind it, or replace it

Use the inventory and the checks from sections 1 and 2. Start at the top: the first row that matches decides.

Spreadsheet or database: four outcomes
What you foundDecisionFirst step
It drives prices, invoices, stock or compliance, needs row history, or exchanges data with other systems both waysReplace it with an application on a databaseSection 6, then the integrate, replace or build decision in our guide to custom versus off-the-shelf software
Several people add or change rows most days, other files or flows read it, or a rule must hold for every writerPut a database behind it and keep Excel in front for reportingTables with the constraints from section 1. People edit through a form or a small app, and Excel reads a database view through Power Query, so existing pivot tables keep working.
It's big or slow, but only for analysisKeep Excel. This isn't a database project.Load the large table into the Data Model with Power Query instead of the grid
One person edits it, nothing else reads it, and nobody asks what it said last monthKeep it and harden itThe validation check from section 1 every week, fill handle and drag-and-drop off on the editor's PC, formula columns protected, and the inventory saved next to the file

The third row is the one most people get wrong. A slow 600,000-row workbook feels like the strongest case for a database, but if one person edits it and everyone else only reads it, the problem is where the data is loaded, not where it's stored. In the Data Model, the table no longer counts against the grid's 1,048,576 rows.

For the second row, the database needn't be big or expensive: SQL Server Express is free (limits in section 3), and PostgreSQL's license allows use "without fee" (PostgreSQL). For a simple shared list with few rules, Microsoft Lists is a lighter step.

6. A migration order that moves the rules first

A workbook migration goes wrong when the data arrives without its rules: the new system accepts values the sheet would have caught, or prices differently because a markup lived in a formula nobody listed. So the rules move first.

Migration order, with the test that ends each step
StepWhat to doDone when
1. Freeze and countSave a dated, read-only copy. For each tab, record the row count, the total of each amount column and the latest date.The counts are written at the top of the rules register
2. InventoryRun the macro and the link scan from section 4Every row has an owner and a one-line purpose
3. Rules registerOne row per rule, in the template belowThe workbook's owner has marked each rule keep, change or retire
4. Golden casesPick 20 to 30 real past cases, including the unusual ones. Put their inputs through a copy of the workbook and save the outputs as values.The saved outputs match what was actually sent: the quote, the invoice or the schedule
5. Tables and constraintsTurn each "keep" rule into a column type, NOT NULL, CHECK, UNIQUE, foreign key or lookup table. Build nothing for retired rules.Each register row names where its rule now lives
6. Stage and check the dataLoad each tab as text and run the check below. Send each failing row back to its owner with its sheet row number.The check returns no rows, or every remaining row has a decision
7. Parallel runRun both for one full cycle, such as a month-end. Compare the step 1 counts and the golden cases.No unexplained differences
8. Cut overRemove edit permission on the workbook, move it to an archive folder, turn on row history and point reports at database viewsNobody can type into the old file

Step 4 is the replay technique from our guide to legacy system challenges, applied to a workbook: the saved outputs become the tests the new system has to pass.

The rules register

This is the Rules sheet from our legacy system examples, with the three columns a migration adds: an example, the owner's decision and where the rule will live. The two rows are an illustrative example.

Rules register template
Rule, in one sentenceWhere it lives nowExampleKeep, change or retireWhere it will live
A job's status is Quoted, Booked, Done or LostValidation list on Jobs, fed from the very hidden sheet ListsJ-1045 says "done " with a trailing spaceKeepCHECK constraint
Quotes carry an 18% markup, 12% for customers on the trade listNumbers typed into the formulas in Jobs!K2:K4000The quote for J-1041Change: trade becomes 14%A pricing table the office manager edits

Stage the data as text, then check it

Before exporting a tab, add a last column headed sheet_row and fill it with =ROW(), so every problem points back to a row. Save the tab as CSV UTF-8 and load it into a staging table where every column is text, so nothing is refused or converted on the way in. Then one query lists every row the real tables would refuse, with its first problem. This is for PostgreSQL 16 or later, where pg_input_is_valid tests a value without raising an error (PostgreSQL docs). The example is a job tracker with day/month/year dates:

-- 1. Staging: every column as text, in the sheet's order, with the sheet row last
CREATE TABLE stage_jobs (
  job_no      text,
  customer    text,
  status      text,
  due_date    text,
  quote_total text,
  sheet_row   integer
);
\copy stage_jobs FROM 'jobs.csv' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8')

-- 2. Every row the real tables would refuse, with its first problem
SET datestyle = 'ISO, DMY';   -- the sheet's dates are day/month/year
SELECT * FROM (
  SELECT sheet_row, job_no,
    CASE
      WHEN coalesce(trim(job_no), '') = ''          THEN 'no job number'
      WHEN count(*) OVER (PARTITION BY job_no) > 1  THEN 'duplicate job number'
      WHEN coalesce(trim(customer), '') = ''        THEN 'no customer'
      WHEN status IS NULL OR status NOT IN ('Quoted', 'Booked', 'Done', 'Lost')
                                                    THEN 'status not in list: "' || coalesce(status, '') || '"'
      WHEN trim(due_date) <> '' AND NOT pg_input_is_valid(trim(due_date), 'date')
                                                    THEN 'not a date: ' || due_date
      WHEN trim(quote_total) <> '' AND NOT pg_input_is_valid(replace(quote_total, ',', ''), 'numeric(12,2)')
                                                    THEN 'not an amount: ' || quote_total
    END AS problem
  FROM stage_jobs
) checked
WHERE problem IS NOT NULL
ORDER BY sheet_row;

-- 3. The real tables: the rules register, made enforceable
CREATE TABLE customers (
  customer_id  integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name         text NOT NULL UNIQUE
);
CREATE TABLE jobs (
  job_no       text PRIMARY KEY,
  customer_id  integer NOT NULL REFERENCES customers,
  status       text NOT NULL CHECK (status IN ('Quoted', 'Booked', 'Done', 'Lost')),
  due_date     date,
  quote_total  numeric(12,2) CHECK (quote_total >= 0)
);

In SQL Server, TRY_CONVERT does the same job: TRY_CONVERT(date, due_date, 103) returns NULL instead of failing when the text isn't a valid dd/mm/yyyy date (TRY_CONVERT, date styles). The check is strict on purpose: "done " with a trailing space got past the sheet's list because it was pasted, and the CHECK constraint refuses it.

If a workbook in your business has become a system, run the inventory and bring the Inventory sheet, the link scan and the first page of your rules register. We'd start with the rules marked "keep" and the golden cases, because they decide what any replacement has to do, whether that's a database behind the sheet or a new application. Our custom business software work starts there.

Build with Adamant Code

Plan the next change to your existing system

Bring the workflow that depends on it, a recent failure and the records that must survive the change. We will discuss what to keep, improve or replace.

Explore software modernization
Discuss your existing software