Relational Databases and SQL to the Rescue

Relational Databases and SQL to the Rescue

Before I really understood relational databases, I stored structured data the way a lot of beginners do — in flat files, nested dictionaries, or ad-hoc JSON blobs that I’d parse manually every time I needed to answer even a simple question about my own data. It worked, sort of, until it didn’t. The moment my data had real relationships — users who placed orders, orders that contained products, products that belonged to categories — my flat-file approach fell apart. That’s when relational databases, and SQL as the language to work with them, genuinely rescued me from a mess of my own making. I want to walk through that same journey here, using SQLite as the practical example throughout.

The Problem With Unstructured or Ad-Hoc Data Storage

Let’s say I’m building a simple e-commerce system and I decide to store everything in a single JSON file:

{
  "users": [
    {"id": 1, "name": "Ahmad", "orders": [{"product": "Keyboard", "price": 49.99}]}
  ]
}

This looks fine for a moment. But very quickly, real problems appear:

  • Duplication — if “Keyboard” appears in a hundred orders, its price is duplicated a hundred times. Update the price, and I now have to hunt down every occurrence.
  • No enforced structure — nothing stops me from accidentally saving an order without a price, or with a price stored as a string in one place and a number in another.
  • Painful querying — “show me all users who bought a keyboard in the last 30 days” turns into writing custom parsing code instead of a simple query.
  • No safe concurrent access — two processes writing to the same JSON file at the same time can easily corrupt it.

This is exactly the set of problems relational databases were designed to solve, decades ago, and it’s exactly why they’ve remained the dominant approach to structured data storage ever since.

What Makes a Database “Relational”

A relational database organizes data into tables (formally called “relations”), where each table represents one type of entity, and relationships between entities are expressed through shared keys rather than nested duplication.

CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL
);

CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    price REAL NOT NULL
);

CREATE TABLE orders (
    id INTEGER PRIMARY KEY,
    user_id INTEGER NOT NULL,
    product_id INTEGER NOT NULL,
    ordered_at TEXT DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (product_id) REFERENCES products(id)
);

Notice how the product’s price lives in exactly one place — the products table. Every order simply references a product_id. Update the price once, and every future query reflects that change automatically, with no duplication to hunt down.

Normalization: Why This Structure Matters

This idea of eliminating duplication by splitting data into properly related tables is called normalization. I didn’t fully appreciate it until I felt the pain of not doing it. The core principle is simple: each piece of information should be stored in exactly one place, and everything else should reference it rather than copy it.

-- Bad: price duplicated across every order row
CREATE TABLE bad_orders (
    id INTEGER PRIMARY KEY,
    product_name TEXT,
    product_price REAL
);

-- Good: price lives in one place, orders reference it
CREATE TABLE good_orders (
    id INTEGER PRIMARY KEY,
    product_id INTEGER REFERENCES products(id)
);

Normalization isn’t just an academic exercise — it directly prevents entire categories of bugs and inconsistencies that I used to run into constantly with ad-hoc data storage.

Enforcing Data Integrity With Constraints

Relational databases let me declare rules about my data upfront, so the database itself rejects invalid data instead of relying on my application code to catch every mistake.

CREATE TABLE accounts (
    id INTEGER PRIMARY KEY,
    email TEXT NOT NULL UNIQUE,
    balance REAL NOT NULL DEFAULT 0 CHECK (balance >= 0)
);

Here, the database itself guarantees that email is never null or duplicated, and that balance never goes negative — no matter what bug might exist somewhere in my application code.

PRAGMA foreign_keys = ON;

In SQLite specifically, I always remember to enable foreign key enforcement explicitly, since it’s disabled by default for backward-compatibility reasons.

SQL: The Language That Makes This All Usable

Having well-structured, related tables is only half the story — SQL is the language that lets me actually use that structure to answer real questions, without writing custom parsing or looping code.

SELECT users.name, products.name AS product, orders.ordered_at
FROM orders
JOIN users ON orders.user_id = users.id
JOIN products ON orders.product_id = products.id
WHERE products.price > 30
ORDER BY orders.ordered_at DESC;

That single query answers a question — “show me everyone who ordered a product over $30, most recent first” — that would take significant custom code to answer correctly against a flat JSON file, especially as the dataset grows.

Transactions: Keeping Data Consistent

One of the most underrated things relational databases give you is transactional guarantees. Imagine transferring money between two accounts:

BEGIN TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

COMMIT;

If anything fails partway through — a power loss, an application crash, a constraint violation — the entire transaction rolls back, and neither update takes effect. I genuinely can’t imagine building reliable financial or inventory logic without this guarantee, and it’s something a flat-file or ad-hoc storage approach simply doesn’t give you for free.

Why SQLite Specifically Makes This Approachable

I’ve used this same relational model in PostgreSQL and MySQL for larger projects, but SQLite has become my go-to tool for learning and prototyping relational database concepts, because there’s zero setup friction:

sqlite3 shop.db < schema.sql

One command, and I have a fully functional relational database with tables, foreign keys, and constraints all in place — no server to configure, no user accounts to create.

A Complete Example, Start to Finish

CREATE TABLE categories (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL UNIQUE
);

CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    price REAL NOT NULL CHECK (price >= 0),
    category_id INTEGER REFERENCES categories(id)
);

INSERT INTO categories (name) VALUES ('Electronics'), ('Books');

INSERT INTO products (name, price, category_id) VALUES
    ('Wireless Mouse', 25.99, 1),
    ('SQL Fundamentals', 39.50, 2);

SELECT products.name, categories.name AS category, products.price
FROM products
JOIN categories ON products.category_id = categories.id
ORDER BY products.price DESC;

Every part of this — the enforced uniqueness, the price constraint, the relationship between products and categories, the join that ties it all together — is exactly the kind of thing that flat-file storage forces you to reimplement badly, by hand, every single time.

Best Practices

  • Normalize your schema to eliminate duplicated data, but don’t over-normalize to the point where every query requires a dozen joins — practical balance matters.
  • Always define primary keys and foreign keys explicitly, and enable foreign key enforcement in SQLite with PRAGMA foreign_keys = ON;.
  • Use CHECK constraints to encode business rules directly into the schema wherever possible, rather than relying solely on application-level validation.
  • Wrap related multi-step writes in explicit transactions to guarantee consistency.
  • Reach for a relational database and SQL the moment your data has more than one clear relationship between entities — that’s the signal flat files stop scaling.

Frequently Asked Questions

When should I move from flat files to a relational database? As soon as your data involves relationships between different types of records — users and orders, products and categories — or you need reliable concurrent access and querying beyond simple lookups.

Is normalization always the right approach? Mostly, yes, but there are legitimate cases for controlled denormalization in performance-critical reporting tables. As a beginner, though, defaulting to normalized design will save you far more pain than it costs.

Why do foreign keys matter if I already validate data in my application? Because application bugs happen. Database-level constraints act as a last line of defense that catches inconsistencies even when application logic fails.

Is SQLite “real” enough to learn relational database concepts properly? Yes — it implements the vast majority of standard SQL and relational features, making it an excellent, low-friction environment for learning concepts that transfer directly to larger systems like PostgreSQL or MySQL.

What’s the biggest mistake beginners make with relational data modeling? Duplicating data instead of referencing it through foreign keys — it seems convenient at first, but it creates consistency problems that compound as the application grows.

Wrapping Up

Looking back, the shift from ad-hoc data storage to properly modeled relational databases was one of the most valuable turning points in how I think about building software. SQL isn’t just a query syntax to memorize — it’s the tool that makes a well-structured relational model actually usable in practice. Once you’ve felt the pain of maintaining duplicated, inconsistent, hard-to-query flat data, you never really want to go back.

Total
0
Shares

Leave a Reply

Previous Post
The SELECT command

The Complete Guide to SQL SELECT Statements: From Basics to Advanced

Next Post
Using sqlite3

Using sqlite3

Related Posts