Databases
Relational concepts, ACID, PostgreSQL, SQL.
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.
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:
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
- ACID: Atomicity, Consistency, Isolation, Durability.
- JOINs: INNER, LEFT, RIGHT, FULL OUTER.
- Indexes: O(log N) lookup; use EXPLAIN ANALYZE to verify usage.
- Transactions: BEGIN, COMMIT, ROLLBACK.
- Security: Always use parameterized queries. No string concatenation.
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:
- Understand the absolute necessity of data persistence and relational data models.
- Design normalized tables with robust keys and relationships.
- Grasp the ACID properties and how isolation, locking, and concurrency maintain data integrity.
- Master PostgreSQL syntax: SELECT, INSERT, UPDATE, DELETE, JOINs, GROUP BY, aggregations, subqueries, and CTEs.
- Apply advanced SQL like Window Functions for analytical queries.
- Optimize database performance using indexes, query plans, and EXPLAIN.
- Manage schemas through migrations and handle application scale via connection pooling.
▶ View Solution
Solution implementation.
Prerequisites
Before beginning this chapter, you should have:
- A solid grasp of backend architecture (Node.js/Express) and RESTful principles.
- Experience dealing with in-memory data structures (Arrays, Maps).
- Basic understanding of the file system and file I/O operations.
▶ 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`).
// 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:
- Concurrency: What if 1,000 requests try to write to `users.json` at the same time? File locks cause massive bottlenecks, or worse, data corruption.
- Memory Limitations: To query data, you have to load the entire JSON file into RAM. A 10GB file will crash your Node.js process.
- Search Inefficiency: Finding one user among millions requires iterating through the entire file (O(N) time).
- Integrity: Nothing prevents you from accidentally saving a string in a number field or duplicating an ID.
▶ 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:
- Network Layer: Node.js sends a SQL string over TCP/IP (usually port 5432).
- Process Model: Postgres spawns or reuses a backend process to handle the connection.
- Parser/Planner: The SQL string is parsed into an Abstract Syntax Tree. The Query Planner analyzes statistics to find the most efficient execution path (e.g., Sequential Scan vs. Index Scan).
- Execution: The Executor fetches data pages from memory (Shared Buffers). If they aren't in RAM, it performs disk I/O.
- Transactions/WAL: Modifications are first written to the Write-Ahead Log (WAL) to guarantee durability (ACID) before being flushed to the main data files.
▶ View Solution
Solution implementation.
Visual Explanation (ASCII diagrams)
+-------------------+ +-----------------------+ +-------------------+
| 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.
-- 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.
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.
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?
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?
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:
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.
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
// 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).