Creating Tables — Data Types, Constraints and Defaults

A table’s definition is its schema — the list of columns, their data types, and the constraints that govern what values are valid. Choosing the right data type for each column is one of the most impactful database design decisions: the right type enables efficient storage, indexing, and querying; the wrong type wastes space, requires awkward casting, or silently allows invalid data. PostgreSQL has a rich type system — far beyond the standard integer, varchar, and boolean — that includes native support for JSON documents, arrays, date/time arithmetic, and UUIDs. Choosing well here pays dividends throughout the lifetime of the application.

CREATE TABLE Syntax

CREATE TABLE table_name (
    column_name  data_type  [constraints],
    ...
    [table_constraints]
);

-- Drop and recreate (development only — destroys all data)
DROP TABLE IF EXISTS posts;
CREATE TABLE posts ( ... );

-- Add a column to an existing table
ALTER TABLE posts ADD COLUMN excerpt TEXT;

-- Rename a column
ALTER TABLE posts RENAME COLUMN excerpt TO summary;

-- Drop a column
ALTER TABLE posts DROP COLUMN summary;

-- Change a column's type
ALTER TABLE posts ALTER COLUMN title TYPE VARCHAR(500);
Note: In PostgreSQL, TEXT and VARCHAR(n) are stored identically — there is no performance difference between them. PostgreSQL’s documentation recommends using TEXT for unrestricted strings and only adding a VARCHAR(n) length limit when there is a genuine business rule that limits the string (like a phone number format or a status code). Arbitrary length limits like VARCHAR(255) — inherited from MySQL’s historic limitations — serve no purpose in PostgreSQL.
Tip: Use TIMESTAMPTZ (timestamp with time zone) rather than TIMESTAMP for all datetime columns. TIMESTAMP stores a “local time” with no timezone information — when you insert a value, PostgreSQL accepts it as-is. TIMESTAMPTZ converts inserted values to UTC internally and reconverts on retrieval based on the session timezone. For a web API with global users or deployed on servers in different timezones, always use TIMESTAMPTZ.
Warning: Never use FLOAT or DOUBLE PRECISION for monetary values. Floating-point numbers cannot represent many decimal fractions exactly — 0.1 + 0.2 in floating point is 0.30000000000000004, not 0.3. For money: use NUMERIC(precision, scale) (e.g., NUMERIC(12, 2) for up to 10 digits before and 2 after the decimal) or store amounts as integers (cents). PostgreSQL also has a MONEY type but it is locale-dependent and generally avoided.

PostgreSQL Data Types Reference

Category Type Use For Notes
Integer SMALLINT Small counts -32768 to 32767
Integer INTEGER / INT IDs, counts -2B to 2B
Integer BIGINT Large IDs, timestamps ±9.2 quintillion
Auto-increment SERIAL Auto-inc INTEGER PK Shorthand for sequence
Auto-increment BIGSERIAL Auto-inc BIGINT PK Preferred for most PKs
Decimal NUMERIC(p, s) Money, precise math Exact, no float rounding
Float REAL / DOUBLE PRECISION Scientific data Not for money!
Text TEXT Unlimited strings No performance penalty
Text VARCHAR(n) Length-limited strings Only if limit is real
Text CHAR(n) Fixed-width (rare) Padded with spaces
Boolean BOOLEAN True/false flags TRUE/FALSE/’t’/’f’/’yes’/’no’
Date/Time DATE Calendar date only No time component
Date/Time TIMESTAMPTZ Date + time with TZ Stored as UTC
Date/Time INTERVAL Duration '7 days', '2 hours'
UUID UUID Universally unique IDs Use for public-facing IDs
JSON JSONB Semi-structured data Indexed, binary stored
JSON JSON Exact JSON storage Text, not indexed
Array TEXT[], INT[] Lists of values PostgreSQL-specific

Constraints

CREATE TABLE users (
    id         BIGSERIAL    PRIMARY KEY,             -- unique, not null, auto-inc
    email      TEXT         NOT NULL UNIQUE,          -- must be present, no duplicates
    name       VARCHAR(100) NOT NULL,                 -- must be present
    age        INTEGER      CHECK (age >= 0 AND age <= 150),  -- value range
    role       TEXT         NOT NULL DEFAULT 'user'
               CHECK (role IN ('user', 'editor', 'admin')),   -- allowed values
    created_at TIMESTAMPTZ  NOT NULL DEFAULT NOW()    -- defaults to current time
);

-- Named constraints (easier to identify in error messages)
CREATE TABLE posts (
    id        BIGSERIAL PRIMARY KEY,
    title     TEXT      NOT NULL,
    author_id BIGINT    NOT NULL,
    status    TEXT      NOT NULL DEFAULT 'draft',

    CONSTRAINT posts_title_not_empty CHECK (LENGTH(TRIM(title)) > 0),
    CONSTRAINT posts_status_valid    CHECK (status IN ('draft', 'published', 'archived')),
    CONSTRAINT posts_author_fk       FOREIGN KEY (author_id) REFERENCES users(id)
);

-- Multi-column unique constraint (e.g. no duplicate slug per author)
CREATE TABLE posts (
    ...
    author_id BIGINT NOT NULL,
    slug      TEXT   NOT NULL,
    UNIQUE (author_id, slug)   -- this pair must be unique, each individually may repeat
);

Common Mistakes

Mistake 1 — Using FLOAT for monetary values

❌ Wrong — floating point rounding errors:

price FLOAT   -- 0.1 + 0.2 = 0.30000000000000004

✅ Correct — use NUMERIC for exact arithmetic:

price NUMERIC(12, 2)   -- exact: 9999999999.99 max ✓

Mistake 2 — Using TIMESTAMP instead of TIMESTAMPTZ

❌ Wrong — timezone-naive, problematic across environments:

created_at TIMESTAMP DEFAULT NOW()   -- local time, no timezone

✅ Correct:

created_at TIMESTAMPTZ DEFAULT NOW()   -- stored as UTC ✓

Mistake 3 — Using JSON instead of JSONB

❌ Wrong — JSON stores text, cannot be indexed:

metadata JSON   -- text storage, no indexing support

✅ Correct — JSONB is binary, indexable, and faster to query:

metadata JSONB   -- binary, indexed, faster ✓

Quick Reference

Task SQL
Create table CREATE TABLE name (col type constraints, ...);
Drop table DROP TABLE IF EXISTS name;
Add column ALTER TABLE t ADD COLUMN col type;
Auto-increment PK id BIGSERIAL PRIMARY KEY
Required field col type NOT NULL
Unique field col type UNIQUE
Default value col type DEFAULT value
Value check col type CHECK (expression)

🧠 Test Yourself

You are designing a products table. Which data types would you choose for: product ID, product name, price (USD), in-stock flag, and creation timestamp?