Who this module is for: You have completed all of Phase 1. You can create tables, query, filter, aggregate, join, and enforce constraints. Now you need to put it all together: design a schema for a real application, connect to it from application code, and understand the tools that keep schemas manageable as they evolve. This module closes the Foundation phase and prepares you for the Practitioner topics ahead.
From Concepts to Real Design
A schema for a real application starts with questions, not SQL:
What are the entities? The "things" your application manages — users, tasks, projects, comments
What attributes does each entity have? The data that describes each thing
What are the relationships? How entities relate — a task belongs to a project, a user can have many tasks
This process is called data modelling. You do not need formal methodology — just answer these three questions before writing any SQL.
Example: A Task Management Application
Entities:
Users (who uses the app)
Projects (containers for tasks)
Tasks (the work items)
Comments (discussion on tasks)
Attributes:
User: username, email, password hash, created date
Project: name, description, owner user, created date
Task: title, description, status, assignee, project, due date, created date
Comment: content, author, task, created date
Relationships:
A user owns zero or more projects (one-to-many)
A project has zero or more tasks (one-to-many)
A task belongs to exactly one project (many-to-one)
A task can be assigned to zero or one user (optional many-to-one)
A task has zero or more comments (one-to-many)
A user can be a member of multiple projects, and a project has multiple members (many-to-many)
The Complete Schema
sql
Verifying the Schema
sql
Seeding Test Data
sql
Querying the Schema
sql
Connecting From Application Code
PostgreSQL stores your data — but your application needs to connect to it. Here is the minimal setup for the two most common stacks.
Node.js with pg
bash
javascript
javascript
Never build SQL by string concatenation with user input:
javascript
Python with psycopg2
bash
python
Schema Migrations: Managing Change
Schemas change. You add columns, rename things, add constraints. In development, you can DROP DATABASE and start over. In production, you cannot — there is live data there.
Schema migrations are versioned SQL scripts that transform the schema from one state to another.
For a beginner Node.js project, node-pg-migrate is simple:
bash
bash
pg_dump — Your Safety Net
Before any schema change on a real database, take a backup:
bash
Get in the habit of pg_dump before anything that modifies schema structure.
What Phase 1 Has Given You
By completing Modules F-1 through F-7, you can:
✅ Install PostgreSQL and use psql confidently
✅ Design a schema from a real-world problem
✅ Use correct data types for every situation
✅ Write every fundamental SQL operation
✅ Filter, sort, and aggregate data
✅ Join data across multiple tables
✅ Enforce data integrity with constraints
✅ Connect from application code safely (parameterised queries)
✅ Understand schema migrations conceptually
This is the foundation that every PostgreSQL engineer builds on. Phase 2 — The Practitioner takes you from isolated scripts to real production application patterns: advanced SQL, indexing, transactions, JSON, full-text search, access control, and deployment.
Summary
Concept
Key Takeaway
Data modelling
Identify entities, attributes, and relationships before writing SQL
Junction tables
Model many-to-many relationships with a third table
Consistent timestamps
created_at, updated_at, soft-delete with archived_at / deleted_at
Parameterised queries
Always use $1, $2 placeholders — never string concatenation with user input
Schema migrations
Version your schema changes; never manually ALTER in production without a migration
pg_dump
Backup before any structural change
Phase 1 is complete. Phase 2 — The Practitioner — begins with Module P-1: Advanced SQL, covering CTEs, window functions, upserts, and the patterns every production engineer uses weekly.
Next: P-1 — Advanced SQL — The Patterns You Will Use Every Week →
Knowledge Check
A Senior Engineer is reviewing the taskmanager schema's foreign key ON DELETE actions. If a users record is attempted to be deleted, which of the following statements accurately describes the *immediate* production behavior based on the provided schema?
A junior developer implements a user search feature using string concatenation for SQL queries, similar to the ❌ SQL INJECTION VULNERABILITY example. During a security audit, it's identified that an attacker could input ' OR '1'='1 into the search field. What is the most critical implication of this vulnerability in a production environment?
A team needs to add a new priority_level column to the tasks table in a production environment. They are debating between manually running ALTER TABLE commands directly on the production database or using a schema migration tool like node-pg-migrate. As a Senior Principal Software Engineer, what is the primary reason to strongly advocate for using a migration tool?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
CREATEDATABASE taskmanager;\c taskmanager
-- Users of the applicationCREATETABLE users ( id BIGSERIAL PRIMARYKEY, username TEXTNOTNULLUNIQUE, email TEXTNOTNULLUNIQUE, password_hash TEXTNOTNULL,-- never store plain passwords display_name TEXT, created_at TIMESTAMPTZ NOTNULLDEFAULTNOW(), last_login_at TIMESTAMPTZ
);-- Projects group tasks togetherCREATETABLE projects ( id BIGSERIAL PRIMARYKEY, name TEXTNOTNULL, description TEXT, owner_id BIGINTNOTNULLREFERENCES users(id)ONDELETERESTRICT,-- Restrict: can't delete a user who owns projects created_at TIMESTAMPTZ NOTNULLDEFAULTNOW(), archived_at TIMESTAMPTZ -- NULL = active, non-NULL = archived);-- Many-to-many: project membership (who can see and work in a project)CREATETABLE project_members ( project_id BIGINTNOTNULLREFERENCES projects(id)ONDELETECASCADE, user_id BIGINTNOTNULLREFERENCES users(id)ONDELETECASCADE, role TEXTNOTNULLDEFAULT'member'CHECK(role IN('owner','admin','member','viewer')), joined_at TIMESTAMPTZ NOTNULLDEFAULTNOW(),PRIMARYKEY(project_id, user_id)-- a user can only be in a project once);-- Tasks are the work itemsCREATETABLE tasks ( id BIGSERIAL PRIMARYKEY, project_id BIGINTNOTNULLREFERENCES projects(id)ONDELETECASCADE,-- If a project is deleted, all its tasks are deleted too created_by BIGINTNOTNULLREFERENCES users(id)ONDELETERESTRICT, assigned_to BIGINTREFERENCES users(id)ONDELETESETNULL,-- assigned_to is optional; if the assignee is deleted, set to NULL title TEXTNOTNULL, description TEXT,statusTEXTNOTNULLDEFAULT'todo'CHECK(statusIN('todo','in_progress','done','cancelled')), priority INTEGERNOTNULLDEFAULT2CHECK(priority BETWEEN1AND5),-- 1=lowest, 5=highest due_date DATE, completed_at TIMESTAMPTZ,-- NULL until task is marked done created_at TIMESTAMPTZ NOTNULLDEFAULTNOW(), updated_at TIMESTAMPTZ NOTNULLDEFAULTNOW());-- Comments on tasksCREATETABLE comments ( id BIGSERIAL PRIMARYKEY, task_id BIGINTNOTNULLREFERENCES tasks(id)ONDELETECASCADE, author_id BIGINTNOTNULLREFERENCES users(id)ONDELETERESTRICT, content TEXTNOTNULLCHECK(LENGTH(TRIM(content))>0), created_at TIMESTAMPTZ NOTNULLDEFAULTNOW(), edited_at TIMESTAMPTZ -- NULL until edited);
-- List all tables\dt
-- Check the structure of each table\d users
\d projects
\d tasks
\d comments
\d project_members
-- See all foreign key relationshipsSELECT tc.table_name, kcu.column_name, ccu.table_name AS foreign_table, ccu.column_name AS foreign_column
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type ='FOREIGN KEY'ORDERBY tc.table_name;
-- Add usersINSERTINTO users (username, email, password_hash, display_name)VALUES('alice','alice@example.com','hash1','Alice Johnson'),('bob','bob@example.com','hash2','Bob Smith'),('carol','carol@example.com','hash3','Carol Davis');-- Add a projectINSERTINTO projects (name, description, owner_id)VALUES('Website Redesign','Redesign the company website',1);-- Project id = 1-- Add project membersINSERTINTO project_members (project_id, user_id, role)VALUES(1,1,'owner'),-- Alice is owner(1,2,'member'),-- Bob is a member(1,3,'viewer');-- Carol can view-- Add tasksINSERTINTO tasks (project_id, created_by, assigned_to, title,status, priority)VALUES(1,1,2,'Design homepage mockup','in_progress',4),(1,1,1,'Set up CI/CD pipeline','todo',3),(1,2,3,'Write content copy','todo',2),(1,1,2,'Review SEO strategy','done',2);-- Add a commentINSERTINTO comments (task_id, author_id, content)VALUES(1,2,'I will have the mockup ready by Friday.');
-- All tasks in the project with assignee namesSELECT t.title, t.status, t.priority, u.display_name AS assigned_to
FROM tasks t
LEFTJOIN users u ON t.assigned_to = u.id
WHERE t.project_id =1ORDERBY t.priority DESC, t.created_at;-- Project members and their rolesSELECT u.display_name, u.email, pm.role, pm.joined_at
FROM project_members pm
JOIN users u ON pm.user_id = u.id
WHERE pm.project_id =1;-- Tasks with comment countSELECT t.title, t.status,COUNT(c.id)AS comment_count
FROM tasks t
LEFTJOIN comments c ON t.id = c.task_id
WHERE t.project_id =1GROUPBY t.id, t.title, t.statusORDERBY t.created_at;-- Summary: tasks per status in this projectSELECTstatus,COUNT(*)AS count
FROM tasks
WHERE project_id =1GROUPBYstatus;
npminstall pg
// db.jsimportpgfrom'pg';const pool =newpg.Pool({connectionString: process.env.DATABASE_URL,// Or explicitly:// host: 'localhost',// port: 5432,// database: 'taskmanager',// user: 'postgres',// password: 'yourpassword',max:10,// connection pool sizeidleTimeoutMillis:30000,});exportasyncfunctionquery(text, params){const result =await pool.query(text, params);return result;}
// Using it in your applicationimport{ query }from'./db.js';// Get all tasks for a projectconst result =awaitquery('SELECT * FROM tasks WHERE project_id = $1 ORDER BY created_at',[projectId]);const tasks = result.rows;// Insert a new task (use parameterised queries — never string concatenation)const newTask =awaitquery(`INSERT INTO tasks (project_id, created_by, title, status, priority)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`,[projectId, userId, title,'todo',3]);const task = newTask.rows[0];
// ❌ SQL INJECTION VULNERABILITY — never do thisconst result =awaitquery(`SELECT * FROM users WHERE email = '${userInputEmail}'`);// An attacker can input: ' OR '1'='1 and read all users// ✅ ALWAYS use parameterised queriesconst result =awaitquery('SELECT * FROM users WHERE email = $1',[userInputEmail]);// $1 is a placeholder — user input is always treated as data, never as SQL
pip install psycopg2-binary
import psycopg2
import psycopg2.extras
conn = psycopg2.connect( host="localhost", database="taskmanager", user="postgres", password="yourpassword")conn.autocommit =True# or manage transactions manuallywith conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)as cur:# Parameterised query with %s placeholders cur.execute("SELECT * FROM tasks WHERE project_id = %s ORDER BY created_at",(project_id,)) tasks = cur.fetchall()# list of dict-like rows
-- migrations/001_initial_schema.sql-- Create the initial schema (the tables we defined above)-- migrations/002_add_task_labels.sqlCREATETABLE labels ( id BIGSERIAL PRIMARYKEY, name TEXTNOTNULLUNIQUE, color TEXTNOTNULLDEFAULT'#888888');CREATETABLE task_labels ( task_id BIGINTNOTNULLREFERENCES tasks(id)ONDELETECASCADE, label_id BIGINTNOTNULLREFERENCES labels(id)ONDELETECASCADE,PRIMARYKEY(task_id, label_id));-- migrations/003_add_task_position.sqlALTERTABLE tasks ADDCOLUMN position INTEGERNOTNULLDEFAULT0;CREATEINDEX idx_tasks_project_position ON tasks (project_id, position);
npminstall node-pg-migrate
# Create a new migrationnode-pg-migrate create add-task-labels
# Run all pending migrationsnode-pg-migrate up
# Roll back the last migrationnode-pg-migrate down
# Backup the entire databasepg_dump taskmanager > backup_$(date +%Y%m%d_%H%M%S).sql
# Restore from a backuppsql taskmanager < backup_20260517_103045.sql
# Backup with compression (for large databases)pg_dump -Fc taskmanager > backup.dump
pg_restore -d taskmanager backup.dump