PostgreSQL Cheat Sheet for Front-End Developers
A practical guide to PostgreSQL for developers who know React better than databases
Table of Contents
- Data Types
- CREATE TABLE
- Primary Keys
- Foreign Keys
- Constraints
- CRUD Operations
- Indexes
- ALTER TABLE
- DROP TABLE
- UUID Functions
Data Types
Think of these like TypeScript types:
-- Strings
VARCHAR(50) -- string with max length
TEXT -- string (unlimited)
-- Numbers
INTEGER -- number (whole numbers: -2147483648 to 2147483647)
BIGINT -- number (bigger whole numbers)
DECIMAL(10,2) -- number (10 digits total, 2 after decimal)
REAL -- number (floating point)
SERIAL -- number (auto-incrementing integer)
-- Booleans
BOOLEAN -- boolean (true/false)
-- Dates
TIMESTAMP -- Date (with time)
DATE -- Date (no time)
TIME -- Time (no date)
-- JSON
JSON -- any (stores JSON as text)
JSONB -- any (stores JSON binary, faster queries)
-- UUIDs
UUID -- string (validated UUID format)
-- Arrays
INTEGER[] -- number[] (array of integers)
TEXT[] -- string[] (array of text)
CREATE TABLE
Basic Table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
age INTEGER,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW()
);
Table with Foreign Key
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
title TEXT NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (user_id) REFERENCES users(id)
);
Junction Table (Many-to-Many)
CREATE TABLE user_roles (
user_id UUID,
role_id UUID,
assigned_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
Primary Keys
Single Primary Key (Most Common)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50)
);
Composite Primary Key (Multiple Columns)
CREATE TABLE user_roles (
user_id UUID,
role_id UUID,
PRIMARY KEY (user_id, role_id) -- BOTH together must be unique
);
Important:
- ❌ You CANNOT have multiple primary keys per table
- ✅ You CAN have a primary key made of multiple columns (composite key)
Foreign Keys
One-to-Many Relationship
-- One user has many posts
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50)
);
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
title TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
Many-to-Many Relationship
-- Users have many roles, roles have many users
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50)
);
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50)
);
-- Junction table
CREATE TABLE user_roles (
user_id UUID,
role_id UUID,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
Foreign Key with Cascade
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
title TEXT,
-- Delete posts when user is deleted
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
Constraints
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- NOT NULL: Must have a value
username VARCHAR(50) NOT NULL,
-- UNIQUE: No duplicates allowed
email VARCHAR(100) UNIQUE NOT NULL,
-- CHECK: Custom validation
age INTEGER CHECK (age >= 18),
price DECIMAL(10,2) CHECK (price > 0),
-- DEFAULT: Auto-fill if not provided
created_at TIMESTAMP DEFAULT NOW(),
is_active BOOLEAN DEFAULT true,
status VARCHAR(20) DEFAULT 'pending'
);
CRUD Operations
CREATE (INSERT)
-- Single insert
INSERT INTO users (username, email)
VALUES ('ryan', 'ryan@example.com');
-- Multiple inserts
INSERT INTO users (username, email)
VALUES
('alice', 'alice@example.com'),
('bob', 'bob@example.com'),
('charlie', 'charlie@example.com');
-- Insert and return the created row
INSERT INTO users (username, email)
VALUES ('dave', 'dave@example.com')
RETURNING *;
-- Insert and return specific columns
INSERT INTO users (username, email)
VALUES ('eve', 'eve@example.com')
RETURNING id, username;
READ (SELECT)
-- Get all rows
SELECT * FROM users;
-- Get specific columns
SELECT username, email FROM users;
-- Filter with WHERE
SELECT * FROM users WHERE username = 'ryan';
SELECT * FROM users WHERE age >= 18;
SELECT * FROM users WHERE is_active = true;
-- Multiple conditions (AND)
SELECT * FROM users
WHERE age >= 18 AND is_active = true;
-- Multiple conditions (OR)
SELECT * FROM users
WHERE username = 'ryan' OR username = 'alice';
-- IN clause (like array.includes())
SELECT * FROM users
WHERE username IN ('ryan', 'alice', 'bob');
-- Pattern matching (LIKE)
SELECT * FROM users WHERE email LIKE '%@gmail.com';
SELECT * FROM users WHERE username LIKE 'r%'; -- Starts with 'r'
SELECT * FROM users WHERE username LIKE '%an%'; -- Contains 'an'
-- NOT
SELECT * FROM users WHERE NOT is_active;
SELECT * FROM users WHERE email NOT LIKE '%@gmail.com';
-- NULL checks
SELECT * FROM users WHERE phone IS NULL;
SELECT * FROM users WHERE phone IS NOT NULL;
-- Order by
SELECT * FROM users ORDER BY created_at DESC;
SELECT * FROM users ORDER BY username ASC;
SELECT * FROM users ORDER BY age DESC, username ASC;
-- Limit and offset (pagination)
SELECT * FROM users LIMIT 10;
SELECT * FROM users LIMIT 10 OFFSET 20; -- Skip 20, get next 10
-- Count
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM users WHERE is_active = true;
-- Distinct
SELECT DISTINCT email FROM users;
UPDATE
-- Update one field
UPDATE users
SET email = 'newemail@example.com'
WHERE username = 'ryan';
-- Update multiple fields
UPDATE users
SET email = 'new@example.com', is_active = false
WHERE id = 'some-uuid-here';
-- Update with calculation
UPDATE users
SET age = age + 1
WHERE username = 'ryan';
-- Update all rows (BE CAREFUL!)
UPDATE users SET is_active = true;
-- Update and return modified rows
UPDATE users
SET email = 'new@example.com'
WHERE username = 'ryan'
RETURNING *;
DELETE
-- Delete specific row
DELETE FROM users WHERE username = 'ryan';
-- Delete with condition
DELETE FROM users WHERE created_at < '2020-01-01';
DELETE FROM users WHERE is_active = false;
-- Delete all rows (BE VERY CAREFUL!)
DELETE FROM users;
-- Delete and return what was deleted
DELETE FROM users
WHERE username = 'ryan'
RETURNING *;
Indexes
Indexes make queries faster (like database performance optimization):
-- Create index on frequently queried column
CREATE INDEX idx_users_email ON users(email);
-- Unique index (enforces uniqueness like UNIQUE constraint)
CREATE UNIQUE INDEX idx_users_username ON users(username);
-- Composite index (multiple columns)
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at);
-- Drop index
DROP INDEX idx_users_email;
-- Check if index exists
DROP INDEX IF EXISTS idx_users_email;
When to use indexes:
- ✅ Columns used in WHERE clauses frequently
- ✅ Foreign key columns
- ✅ Columns used in ORDER BY
- ✅ Columns used in JOIN conditions
- ❌ Small tables (overhead not worth it)
- ❌ Columns that change frequently (slows down writes)
ALTER TABLE
Modifying existing tables:
-- Add column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users ADD COLUMN bio TEXT DEFAULT '';
-- Drop column
ALTER TABLE users DROP COLUMN phone;
-- Rename column
ALTER TABLE users RENAME COLUMN username TO user_name;
-- Rename table
ALTER TABLE users RENAME TO app_users;
-- Change column type
ALTER TABLE users ALTER COLUMN age TYPE BIGINT;
-- Set default value
ALTER TABLE users ALTER COLUMN is_active SET DEFAULT true;
-- Remove default value
ALTER TABLE users ALTER COLUMN is_active DROP DEFAULT;
-- Add NOT NULL constraint
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
-- Remove NOT NULL constraint
ALTER TABLE users ALTER COLUMN email DROP NOT NULL;
-- Add constraint
ALTER TABLE users ADD CONSTRAINT check_age CHECK (age >= 18);
ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);
-- Drop constraint
ALTER TABLE users DROP CONSTRAINT check_age;
DROP TABLE
Deleting tables:
-- Delete table and all data
DROP TABLE users;
-- Delete if exists (no error if doesn't exist)
DROP TABLE IF EXISTS users;
-- Delete with cascade (also delete dependent tables/constraints)
DROP TABLE users CASCADE;
UUID Functions
Modern Way (PostgreSQL 13+)
-- Generate random UUID (v4)
SELECT gen_random_uuid();
-- Output: 550e8400-e29b-41d4-a716-446655440000
-- Use in table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50)
);
Legacy Way (Requires Extension)
-- Enable extension (only needed once per database)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Generate UUIDs
SELECT uuid_generate_v1(); -- Time-based (contains MAC address)
SELECT uuid_generate_v4(); -- Random (same as gen_random_uuid)
UUID Versions Explained
v1 (Time-based):
- Contains timestamp + MAC address
- ✅ Sortable by time
- ❌ Privacy leak (MAC address visible)
v4 (Random):
- Pure random
- ✅ Secure and unpredictable
- ✅ No privacy leaks
- ❌ Not sortable
Recommendation: Use gen_random_uuid() (v4) for modern apps!
Frontend Dev Mental Model
Think of databases like normalized Redux state:
// Frontend (Redux)
const state = {
users: {
byId: {
'123': { id: '123', username: 'ryan', email: 'ryan@example.com' }
},
allIds: ['123']
},
posts: {
byId: {
'456': { id: '456', userId: '123', title: 'Hello World' }
},
allIds: ['456']
}
};
// Backend (PostgreSQL)
CREATE TABLE users (
id UUID PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100)
);
CREATE TABLE posts (
id UUID PRIMARY KEY,
user_id UUID,
title TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
Tables = State slices
Foreign keys = References between slices
Indexes = Performance optimization
Quick Reference
Connection Info
- Host:
localhost - Port:
5432 - Database:
postgres - Username: Your macOS username
- Password: (leave blank for local development)
Common Commands
# Start PostgreSQL
brew services start postgresql@16
# Stop PostgreSQL
brew services stop postgresql@16
# Connect to database
psql -d postgres
# Check version
psql --version
# Run SQL from command line
psql -d postgres -c "SELECT * FROM users;"
psql Commands (Inside psql shell)
\l -- List all databases
\c dbname -- Connect to database
\dt -- List all tables
\d tablename -- Describe table structure
\du -- List all users
\q -- Quit psql
Best Practices
- Always use UUIDs for primary keys (unless you have a good reason not to)
- Add indexes on foreign keys (makes joins faster)
- Use NOT NULL for required fields (catch errors early)
- Use UNIQUE for fields that should be unique (like email, username)
- Use CHECK constraints for validation (enforce business rules at DB level)
- Use TIMESTAMP for dates (always include timezone info)
- Use JSONB over JSON (faster queries, better indexing)
- Use CASCADE carefully (can accidentally delete lots of data)
- Test queries before running in production (especially DELETE and UPDATE)
- Back up your data regularly (because shit happens)
Made with ❤️ for frontend devs learning backend stuff