Warming up the neural circuits...
A table is where your data actually lives — a structured grid of rows and columns. By the end of this module you will:
What it is: CREATE TABLE is a DDL command that defines a new table in your database. A table is a structured grid of rows and columns, similar to a spreadsheet, where each column has a specific data type and each row represents a single record.
Why we use it: Tables are the fundamental storage unit in a relational database. Without tables, there's nowhere to store data. The structure you define (columns, types, constraints) determines what data can be stored and how it's validated.
When we use it: At the start of every project when designing the database schema, or when adding new features that require new data storage.
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price INTEGER NOT NULL,
description TEXT,
in_stock BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW()
);| Column | Type | Rule | Purpose |
|---|---|---|---|
id | SERIAL | PRIMARY KEY | Auto-incrementing unique ID |
name | VARCHAR(100) | NOT NULL | Required text, max 100 chars |
price | INTEGER | NOT NULL | Required whole number |
description | TEXT | — | Optional long text |
in_stock | BOOLEAN | DEFAULT true | Defaults to true if not specified |
created_at |
What it is: Data types define what kind of data a column can hold — numbers, text, dates, booleans, and more. PostgreSQL has a rich set of native data types optimized for different use cases.
Why we use it: Choosing the right data type ensures data integrity (you can't accidentally store text in a number column), optimizes storage (integers take less space than text), and enables proper operations (you can do math on numbers but not on text).
When we use it: Every time you create a column, you must choose a data type. The choice affects storage, performance, and what operations are valid.
| Category | Type | Best For | Professional Tip |
|---|---|---|---|
| Whole Numbers | INTEGER | IDs, counts, age | Use BIGINT if you expect billions of rows |
| Money/Math | NUMERIC | Prices, interest rates | Never use floating points (REAL) for money |
| Short Text | VARCHAR(n) | Names, emails, titles | Limits help prevent garbage data entry |
| Long Text | TEXT | Descriptions, comments | In Postgres, TEXT has no performance penalty over VARCHAR |
| Truth | BOOLEAN | active, deleted, verified | Defaults are your best friend here |
| Time | TIMESTAMPTZ | created_at, updated_at | Always use WITH TIME ZONE to avoid timezone bugs |
| Auto IDs | SERIAL | Primary keys |
What it is: Constraints are rules enforced by the database that automatically validate data before it's inserted or updated. They act as a safety net, preventing invalid data from entering your system.
Why we use it: Without constraints, your application code would need to validate every piece of data — and bugs in that code would corrupt your database. Constraints provide a last line of defense at the database level.
When we use it: On every table, for every column that has rules about what data is valid. Common constraints include PRIMARY KEY, NOT NULL, UNIQUE, CHECK, and DEFAULT.
What it is: A PRIMARY KEY is a constraint that uniquely identifies each row in a table. It combines two rules: UNIQUE (no two rows can have the same value) and NOT NULL (the value cannot be empty).
Why we use it: Every table needs a way to uniquely identify rows — for updates, deletes, joins, and relationships. The primary key is the standard way to do this.
When we use it: On every table, typically on an auto-incrementing id column.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL
);What it is: The NOT NULL constraint prevents a column from accepting empty (NULL) values. It ensures that every row must have a value for this column.
Why we use it: Some data is essential — a user must have an email, an order must have a total. NOT NULL enforces this at the database level, preventing bugs where critical data is missing.
When we use it: On columns that are required for business logic — names, emails, prices, foreign keys, and other essential data.
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL,
total NUMERIC NOT NULL
);What it is: The UNIQUE constraint ensures that all values in a column are different — no two rows can have the same value. Unlike PRIMARY KEY, a UNIQUE column can accept NULL values (unless also marked NOT NULL).
Why we use it: Some data must be unique across all rows — emails, usernames, product SKUs, order numbers. UNIQUE prevents duplicate entries that would cause data integrity issues.
When we use it: On columns that represent natural unique identifiers — emails, usernames, slugs, serial numbers, etc.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(50) UNIQUE NOT NULL
);What it is: The CHECK constraint enforces a custom rule on a column. It defines a boolean expression that must be true for every row — otherwise the insert or update is rejected.
Why we use it: Some business rules can't be enforced by simple types or NOT NULL — for example, "price must be positive", "age must be 13+", "status must be one of these values". CHECK lets you enforce these rules at the database level.
When we use it: When you need to validate data against business rules — range checks, positive numbers, valid status values, etc.
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price NUMERIC CHECK (price > 0),
age INTEGER CHECK (age >= 13)
);What it is: The DEFAULT constraint provides a fallback value that's automatically used when no value is specified during insertion. It reduces the amount of data you need to provide in INSERT statements.
Why we use it: Many columns have sensible defaults — timestamps default to "now", boolean flags default to false, counters default to 0. Defaults reduce boilerplate and ensure consistency.
When we use it: On columns that have a common starting value — created_at timestamps, is_active flags, status fields, counters, etc.
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(300) NOT NULL,
published BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);What it is: Naming conventions are agreed-upon rules for how to name database objects — tables, columns, indexes, and constraints. They're not enforced by PostgreSQL, but they're essential for team collaboration.
Why we use it: Consistent naming makes code predictable and readable. When a new developer joins the team, they can understand the schema without asking questions. It also prevents bugs caused by inconsistent references (e.g., userId vs user_id).
When we use it: Always. Every database you create should follow these conventions from day one.
To code like a senior engineer, follow these industry standards:
| Convention | Example | |
|---|---|---|
| Tables | Plural, snake_case | users, order_items |
| Columns | snake_case | created_at, first_name |
| Primary keys | id | id |
| Foreign keys | referenced_table_id | user_id, order_id |
| Indexes | idx_table_column | idx_users_email |
| Constraints | pk_table, fk_table_ref | pk_users, fk_posts_users |
Why conventions matter: When your team grows from 1 to 10 developers, naming conventions prevent confusion. Every major tech company enforces these standards.
VARCHAR without a limit — Always specify a max length for data integrityREAL or FLOAT for money — Floating-point math causes rounding errors. Use NUMERIC.TIMESTAMPTZ — Without timezone info, your timestamps will be ambiguous across regionslearning_hub databasecourses with these columns:
id — auto-incrementing primary keytitle — required text, max 200 charactersinstructor — optional textprice — required number, must be positiveis_published — true/false, defaults to falsecreated_at — timestamp, auto-set to now\d coursesstudents with:
id — auto-incrementing primary keyname — required textemail — required, must be uniqueenrolled_at — timestamp, auto-set to nowNext up: Tables aren't set in stone. In the next module, you'll learn how to modify existing tables with ALTER TABLE — adding columns, changing types, and adding constraints.
TIMESTAMPDEFAULT NOW() |
| Auto-set to current time |
| Auto-increments (1, 2, 3...) |
| UUIDs | UUID | Distributed systems | Use gen_random_uuid() for unique IDs across servers |