🌙
☀️ Dark
PART 13

Databases

Relational concepts, ACID, PostgreSQL, SQL.

Intermediate 45 min read

PART 13 — DATABASES

Learning Objectives

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    handle VARCHAR(50) UNIQUE NOT NULL
);

CREATE TABLE tweets (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    content VARCHAR(280) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Fetch a timeline:
SELECT u.handle, t.content, t.created_at
FROM tweets t
JOIN users u ON t.user_id = u.id
ORDER BY t.created_at DESC
LIMIT 50;

Real Application Feature

Engineering Challenge

Scenario: You run an e-commerce store. During a flash sale, 500 users attempt to buy the last remaining item simultaneously.

Goal: Write a query strategy that prevents overselling while avoiding massive database deadlocks.

View Solution

Use row-level locking with FOR UPDATE or a check constraint.

sql
BEGIN;
-- Lock the specific row so other transactions wait
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- Application checks if stock > 0
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;

Even better, rely on atomic database checks without explicit locking:

sql
UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock > 0 RETURNING id;

If no rows are returned, the transaction failed because stock was 0.

Revision Sheet

Connections

The relational database serves as the ultimate source of truth. Moving forward, we will connect this persistent layer to our Node.js backends using ORMs/Query Builders (Chapter 14), scale read-heavy workloads with Redis (Chapter 15), and eventually deploy this stateful infrastructure securely to the Cloud (Chapter 18).

By the end of this chapter, you will be able to:

▶ View Solution

Solution implementation.

Prerequisites

Before beginning this chapter, you should have:

▶ View Solution

Solution implementation.

Why Does This Exist?

Imagine running a bank where all account balances are stored in a JavaScript variable. The moment the server reboots or crashes, the money disappears. Applications need state that outlives the process runtime. Furthermore, as data grows, scanning through flat files becomes incredibly slow. Databases exist to provide durable, highly concurrent, and lightning-fast retrieval and mutation of structured data.

▶ View Solution

Solution implementation.

The Problem Before the Solution

The naive solution to data persistence is simply writing JSON objects to a file (`users.json`).

yaml
▶ View Solution

Solution implementation.

// The Naive Approach
const fs = require('fs');
let users = JSON.parse(fs.readFileSync('users.json'));
users.push({ id: 1, name: "Alice", balance: 50 });
fs.writeFileSync('users.json', JSON.stringify(users));
▶ View Solution

Solution implementation.

▶ View Solution

Solution implementation.

Why the Old Approach Breaks

The file-based approach fails dramatically under real-world constraints:

▶ View Solution

Solution implementation.

History

In the 1960s, databases were hierarchical or network-based, meaning data was navigated via rigid pointers. In 1970, Edgar F. Codd published a paper on the Relational Model, proposing that data should be represented as mathematical relations (tables) interconnected by logical keys, heavily queried via declarative languages (SQL) rather than procedural code. PostgreSQL, starting in the 1980s as "POSTGRES" at UC Berkeley, evolved this idea into one of the world's most advanced open-source relational databases.

▶ View Solution

Solution implementation.

Mental Model (Analogy -> Reality)

The Analogy: Think of a massive Excel workbook. The workbook is the Database. The sheets are Tables. The columns are Columns (attributes), and the rows are Rows (records). If Sheet A lists "Users" and Sheet B lists "Orders," you can cross-reference an order to a user using an ID (a Foreign Key).

The Reality: Unlike Excel, a relational database is a robust server daemon running on an OS. It manages raw blocks of memory and disk on a B-Tree structure, enforcing strict schemas and data integrity rules, while safely handling thousands of simultaneous connections via sophisticated locking mechanisms.

▶ View Solution

Solution implementation.

Internal Working (Memory, stack, process, network, etc.)

When you send a query to PostgreSQL:

▶ View Solution

Solution implementation.

Visual Explanation (ASCII diagrams)

typescript
▶ View Solution

Solution implementation.


+-------------------+       +-----------------------+       +-------------------+
| Node.js Client    |       | Postgres Postmaster   |       |   Storage / WAL   |
| (pg connection)   | ===>  | (Parser & Planner)    | ===>  | (Data integrity)  |
+-------------------+       +-----------------------+       +-------------------+
                                       |
                                       v
                            +-----------------------+
                            | Shared Buffers (RAM)  |
                            | (Fast B-Tree Index)   |
                            +-----------------------+
    
▶ View Solution

Solution implementation.

▶ View Solution

Solution implementation.

Syntax

SQL (Structured Query Language) is declarative. You specify what you want, not how to get it.

sql
▶ View Solution

Solution implementation.

-- CREATE a table
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- INSERT data
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');

-- SELECT data
SELECT id, name FROM users WHERE id = 1;

-- UPDATE data
UPDATE users SET name = 'Alicia' WHERE id = 1;

-- DELETE data
DELETE FROM users WHERE id = 1;
▶ View Solution

Solution implementation.

▶ View Solution

Solution implementation.

Tiny Example

Let's find the total number of users who signed up.

sql
▶ View Solution

Solution implementation.

SELECT COUNT(*) FROM users;
▶ View Solution

Solution implementation.

▶ View Solution

Solution implementation.

Walkthrough

Let's construct a relational query. We want to fetch all orders for a specific user, along with the user's name.

sql
▶ View Solution

Solution implementation.

SELECT u.name, o.amount, o.created_at
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.id = 42
ORDER BY o.created_at DESC;
▶ View Solution

Solution implementation.

Step-by-step:
1. FROM users u: Start with the users table.
2. JOIN orders o ON ...: Match rows in the orders table where the foreign key `user_id` matches the user's primary key `id`.
3. WHERE u.id = 42: Filter down to just the user we care about.
4. ORDER BY ...: Sort the resulting joined data.
5. SELECT ...: Project only the columns we need to return over the network.

▶ View Solution

Solution implementation.

Break It

What happens if you run an UPDATE without a WHERE clause?

sql
▶ View Solution

Solution implementation.

UPDATE users SET is_admin = true;
▶ View Solution

Solution implementation.

Result: Every single user in the database is now an admin. This is catastrophic. Always use WHERE clauses or run dangerous queries inside a BEGIN; ... ROLLBACK; block first.

▶ View Solution

Solution implementation.

Debug It

Your query is running extremely slow. How do you debug it?

sql
▶ View Solution

Solution implementation.

EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'bob@example.com';
▶ View Solution

Solution implementation.

If the output says Seq Scan on users, it means Postgres is reading every row one by one. You fix this by adding an Index:

typescript
▶ View Solution

Solution implementation.

CREATE INDEX idx_users_email ON users(email);
▶ View Solution

Solution implementation.

▶ View Solution

Solution implementation.

Mini Project (20-30 min)

Project: Design a schema for a Twitter clone.

sql
▶ View Solution

Solution implementation.

Bigger Project (1-2 hours)

Design a PostgreSQL schema for a blog (users, posts, comments). Write raw SQL to create tables and insert sample data, enforcing foreign keys.

▶ View Solution
typescript
// Implementation for Databases
console.log("Bigger project solution");

Interview Questions

Easy: What is a Primary Key vs Foreign Key?

A Primary Key uniquely identifies a record in a table. A Foreign Key is a field in one table that uniquely identifies a row of another table, establishing a link.

Medium: Explain the difference between INNER JOIN and LEFT JOIN.

INNER JOIN returns rows when there is a match in both tables. LEFT JOIN returns all rows from the left table, and the matched rows from the right table (filling with NULLs if no match).

Hard: What is an index and how does a B-Tree index work under the hood?

An index is a data structure that improves data retrieval speed. A B-Tree (Balanced Tree) stores keys in a sorted tree structure, allowing O(log N) search, insert, and delete. It minimizes disk I/O by keeping nodes large enough to fit exactly into a disk block.

Senior: Describe the isolation levels in ACID and how they affect concurrency (Dirty Reads, Phantom Reads).

Read Uncommitted (allows dirty reads), Read Committed (default in PG, prevents dirty reads), Repeatable Read (prevents non-repeatable reads), and Serializable (highest level, strictly sequential behavior, prevents phantom reads but highly limits concurrency).

🏠 Curriculum NextVolume 2