Skip to main content

PostgreSQL Cheatsheet

Comprehensive quick reference for PostgreSQL commands, SQL syntax, database operations, performance optimization, and administration. Essential guide for PostgreSQL 14+.

Table of Contents


Prerequisites

PostgreSQL Version: This cheatsheet targets PostgreSQL 14+. Some features require specific versions (noted inline).

System Requirements:

  • Linux: Most distributions supported
  • macOS: 10.15+ (via Homebrew or Postgres.app)
  • Windows: Windows 10/11 (via installer or WSL)

Prerequisites:

  • Basic SQL knowledge
  • Command-line familiarity
  • Understanding of relational database concepts

Installation & Version

Installation Commands πŸ”§

Terminal window
# Check PostgreSQL version
psql --version
# β†’ psql (PostgreSQL) 15.3
# Connect and check server version
psql -U postgres -c "SELECT version();"
# β†’ PostgreSQL 15.3 on x86_64-pc-linux-gnu
# Check PostgreSQL service status (Linux)
sudo systemctl status postgresql
# or
sudo service postgresql status
# Start PostgreSQL service (Linux)
sudo systemctl start postgresql
# or
sudo service postgresql start
# Stop PostgreSQL service (Linux)
sudo systemctl stop postgresql
# or
sudo service postgresql stop
# Restart PostgreSQL service (Linux)
sudo systemctl restart postgresql

Platform-Specific Installation πŸ“¦

Terminal window
# Ubuntu/Debian
sudo apt update
sudo apt install postgresql postgresql-contrib
# macOS (using Homebrew)
brew install postgresql@15
brew services start postgresql@15
# macOS (using Postgres.app)
# Download from: https://postgresapp.com/
# Windows
# Download installer from: https://www.postgresql.org/download/windows/
# Verify installation
psql --version

Configuration Files πŸ“

/etc/postgresql/[version]/main/postgresql.conf
# Main configuration file location
# macOS (Homebrew): /opt/homebrew/var/postgresql@15/postgresql.conf
# Windows: C:\Program Files\PostgreSQL\[version]\data\postgresql.conf
# View configuration file location
psql -U postgres -c "SHOW config_file;"
# β†’ /etc/postgresql/15/main/postgresql.conf
# View data directory
psql -U postgres -c "SHOW data_directory;"
# β†’ /var/lib/postgresql/15/main
# Reload configuration without restart
sudo systemctl reload postgresql
# or via psql
psql -U postgres -c "SELECT pg_reload_conf();"

Connection & Authentication

Connection Methods πŸ”Œ

Terminal window
# Connect to default database (postgres)
psql -U postgres
# Connect to specific database
psql -U postgres -d mydb
# Connect with host and port
psql -h localhost -p 5432 -U postgres -d mydb
# Connect using connection string
psql postgresql://username:password@localhost:5432/dbname
# Connect without password prompt (use .pgpass file)
psql -U postgres -d mydb
# Create ~/.pgpass file: hostname:port:database:username:password
# Connect and execute command
psql -U postgres -d mydb -c "SELECT version();"
# Connect and execute SQL file
psql -U postgres -d mydb -f script.sql
# Connect with output formatting
psql -U postgres -d mydb -A -t -c "SELECT * FROM users;"
# -A: unaligned output, -t: tuples only

psql Commands (Meta-Commands) πŸ’»

-- List all databases
\l
-- or
\list
-- Connect to database
\c mydb
-- or
\connect mydb
-- List all tables in current database
\dt
-- List all tables including system tables
\dt+
-- Describe table structure
\d table_name
-- or
\dt+ table_name
-- List all schemas
\dn
-- List all users/roles
\du
-- Show current database
SELECT current_database();
-- Show current user
SELECT current_user;
-- Show current schema
SHOW search_path;
-- Set schema search path
SET search_path TO schema_name, public;
-- List all functions
\df
-- List all views
\dv
-- Show table size
\d+ table_name
-- Show database size
\l+
-- Exit psql
\q
-- or
exit
-- Show help
\?
-- Help for SQL commands
\h SELECT

Authentication Methods πŸ”

Terminal window
# View pg_hba.conf (host-based authentication)
# Location: Usually in data directory or /etc/postgresql/[version]/main/
cat /etc/postgresql/15/main/pg_hba.conf
# Common authentication methods:
# trust: No password required
# md5: Password encrypted with MD5
# scram-sha-256: Password encrypted with SCRAM-SHA-256 (PostgreSQL 10+)
# peer: Use OS username
# ident: Use ident protocol
# Example pg_hba.conf entries:
# local all all trust
# host all all 127.0.0.1/32 scram-sha-256
# host all all ::1/128 scram-sha-256

Database Operations

Create & Drop Databases πŸ—„οΈ

-- Create database
CREATE DATABASE mydb;
-- Create database with owner
CREATE DATABASE mydb OWNER username;
-- Create database with encoding
CREATE DATABASE mydb
WITH ENCODING 'UTF8'
LC_COLLATE='en_US.UTF-8'
LC_CTYPE='en_US.UTF-8'
TEMPLATE template0;
-- Create database if not exists (PostgreSQL 9.1+)
-- Note: No IF NOT EXISTS for CREATE DATABASE, use:
SELECT 'CREATE DATABASE mydb'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'mydb')\gexec
-- Drop database
DROP DATABASE mydb;
-- Drop database if exists
DROP DATABASE IF EXISTS mydb;
-- Rename database
ALTER DATABASE oldname RENAME TO newname;
-- List all databases
SELECT datname FROM pg_database;
-- Show database size
SELECT pg_size_pretty(pg_database_size('mydb'));
# β†’ 15 MB
-- Show all database sizes
SELECT
datname,
pg_size_pretty(pg_database_size(datname)) AS size
FROM pg_database
ORDER BY pg_database_size(datname) DESC;

Database Information πŸ“Š

-- Show current database
SELECT current_database();
-- Show database encoding
SHOW server_encoding;
# β†’ UTF8
-- Show timezone
SHOW timezone;
# β†’ UTC
-- Set timezone
SET timezone TO 'America/New_York';
-- Show all settings
SHOW ALL;
-- Show specific setting
SHOW max_connections;
# β†’ 100
-- Set configuration parameter (session level)
SET work_mem = '256MB';
-- Set configuration parameter (database level)
ALTER DATABASE mydb SET work_mem = '256MB';

Table Operations

Create Tables πŸ—οΈ

-- Basic table creation
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Table with multiple constraints
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
product_name VARCHAR(100) NOT NULL,
quantity INTEGER CHECK (quantity > 0),
price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create table with schema
CREATE TABLE public.products (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
-- Create table if not exists (PostgreSQL 9.1+)
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50)
);
-- Create table from query result
CREATE TABLE users_backup AS
SELECT * FROM users WHERE created_at > '2024-01-01';
-- Create temporary table
CREATE TEMP TABLE temp_data (
id INTEGER,
value TEXT
);
-- Create unlogged table (faster writes, no WAL, lost on crash)
CREATE UNLOGGED TABLE cache_data (
key VARCHAR(100) PRIMARY KEY,
value TEXT
);

Alter Tables πŸ”§

-- Add column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Add column with default value
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active';
-- Add column with NOT NULL constraint (requires default)
ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0;
-- Drop column
ALTER TABLE users DROP COLUMN phone;
-- Drop column if exists
ALTER TABLE users DROP COLUMN IF EXISTS phone;
-- Rename column
ALTER TABLE users RENAME COLUMN email TO email_address;
-- Change column type
ALTER TABLE users ALTER COLUMN age TYPE SMALLINT;
-- Change column type with conversion
ALTER TABLE users ALTER COLUMN age TYPE INTEGER USING age::INTEGER;
-- Set column default
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'inactive';
-- Drop column default
ALTER TABLE users ALTER COLUMN status 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;
-- Rename table
ALTER TABLE users RENAME TO customers;
-- Add constraint
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email);
-- Drop constraint
ALTER TABLE users DROP CONSTRAINT users_email_key;
-- Add foreign key constraint
ALTER TABLE orders
ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id);
-- Add check constraint
ALTER TABLE orders
ADD CONSTRAINT orders_quantity_check
CHECK (quantity > 0);

Drop Tables πŸ—‘οΈ

-- Drop table
DROP TABLE users;
-- Drop table if exists
DROP TABLE IF EXISTS users;
-- Drop table with cascade (drops dependent objects)
DROP TABLE users CASCADE;
-- Truncate table (delete all rows, faster than DELETE)
TRUNCATE TABLE users;
-- Truncate table and reset sequences
TRUNCATE TABLE users RESTART IDENTITY;
-- Truncate multiple tables
TRUNCATE TABLE users, orders, products;

Table Information πŸ“‹

-- List all tables
SELECT tablename FROM pg_tables WHERE schemaname = 'public';
-- Show table structure
\d users
-- Show detailed table information
\d+ users
-- Show table size
SELECT pg_size_pretty(pg_total_relation_size('users'));
# β†’ 1024 kB
-- Show table size breakdown
SELECT
pg_size_pretty(pg_relation_size('users')) AS table_size,
pg_size_pretty(pg_indexes_size('users')) AS indexes_size,
pg_size_pretty(pg_total_relation_size('users')) AS total_size;
-- Count rows in table
SELECT COUNT(*) FROM users;
-- Show table statistics
SELECT
schemaname,
tablename,
n_live_tup AS row_count,
n_dead_tup AS dead_rows
FROM pg_stat_user_tables
WHERE tablename = 'users';

Data Types

Numeric Types πŸ”’

-- Integer types
SMALLINT -- -32,768 to 32,767 (2 bytes)
INTEGER -- -2,147,483,648 to 2,147,483,647 (4 bytes)
BIGINT -- -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 (8 bytes)
SERIAL -- Auto-incrementing INTEGER (1 to 2,147,483,647)
BIGSERIAL -- Auto-incrementing BIGINT
-- Example
CREATE TABLE products (
id SERIAL PRIMARY KEY,
price INTEGER NOT NULL,
quantity SMALLINT DEFAULT 0,
views BIGINT DEFAULT 0
);
-- Decimal/Numeric types (exact precision)
DECIMAL(10, 2) -- 10 digits total, 2 after decimal
NUMERIC(10, 2) -- Same as DECIMAL
MONEY -- Currency type (deprecated, use NUMERIC)
-- Example
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
total DECIMAL(10, 2) NOT NULL,
tax NUMERIC(5, 2) DEFAULT 0.00
);
-- Floating point types (approximate)
REAL -- 6 decimal digits precision (4 bytes)
DOUBLE PRECISION -- 15 decimal digits precision (8 bytes)
-- Example
CREATE TABLE measurements (
id SERIAL PRIMARY KEY,
temperature REAL,
precision_value DOUBLE PRECISION
);

Character Types πŸ“

-- Character types
VARCHAR(n) -- Variable length, max n characters
CHAR(n) -- Fixed length, padded with spaces
TEXT -- Unlimited length
-- Example
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
code CHAR(5), -- Always 5 characters
bio TEXT -- Unlimited length
);
-- String functions
SELECT LENGTH('hello'); -- β†’ 5
SELECT UPPER('hello'); -- β†’ HELLO
SELECT LOWER('HELLO'); -- β†’ hello
SELECT SUBSTRING('hello' FROM 1 FOR 3); -- β†’ hel
SELECT REPLACE('hello', 'l', 'L'); -- β†’ heLLo
SELECT TRIM(' hello '); -- β†’ hello
SELECT CONCAT('hello', ' ', 'world'); -- β†’ hello world
SELECT 'hello' || ' ' || 'world'; -- β†’ hello world

Date & Time Types πŸ“…

-- Date and time types
DATE -- Date only (YYYY-MM-DD)
TIME -- Time only (HH:MM:SS)
TIMESTAMP -- Date and time (YYYY-MM-DD HH:MM:SS)
TIMESTAMPTZ -- Timestamp with timezone
INTERVAL -- Time interval
-- Example
CREATE TABLE events (
id SERIAL PRIMARY KEY,
event_date DATE NOT NULL,
start_time TIME,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
duration INTERVAL
);
-- Date/time functions
SELECT CURRENT_DATE; -- β†’ 2024-01-15
SELECT CURRENT_TIME; -- β†’ 14:30:00
SELECT CURRENT_TIMESTAMP; -- β†’ 2024-01-15 14:30:00
SELECT NOW(); -- β†’ Current timestamp
SELECT EXTRACT(YEAR FROM NOW()); -- β†’ 2024
SELECT EXTRACT(MONTH FROM NOW()); -- β†’ 1
SELECT EXTRACT(DAY FROM NOW()); -- β†’ 15
SELECT DATE_TRUNC('day', NOW()); -- β†’ 2024-01-15 00:00:00
SELECT DATE_TRUNC('month', NOW()); -- β†’ 2024-01-01 00:00:00
SELECT AGE('2024-01-15', '2020-01-01'); -- β†’ 4 years 14 days

Boolean & Other Types βœ…

-- Boolean type
BOOLEAN -- true, false, or NULL
-- Example
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(100) NOT NULL,
completed BOOLEAN DEFAULT FALSE
);
-- Boolean operators
SELECT TRUE AND FALSE; -- β†’ false
SELECT TRUE OR FALSE; -- β†’ true
SELECT NOT TRUE; -- β†’ false
-- JSON types (PostgreSQL 9.2+)
JSON -- Textual JSON data
JSONB -- Binary JSON data (faster, indexable)
-- Example
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
metadata JSONB
);
-- JSON operations
INSERT INTO products (name, metadata)
VALUES ('Laptop', '{"brand": "Dell", "ram": 16}');
SELECT metadata->>'brand' FROM products; -- β†’ Dell
SELECT metadata->'ram' FROM products; -- β†’ 16
SELECT metadata @> '{"brand": "Dell"}'::jsonb; -- β†’ true
-- Array types
INTEGER[] -- Array of integers
TEXT[] -- Array of text
VARCHAR(50)[] -- Array of varchar
-- Example
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
tags TEXT[]
);
INSERT INTO users (name, tags)
VALUES ('John', ARRAY['admin', 'developer', 'user']);
SELECT tags[1] FROM users; -- β†’ admin
SELECT array_length(tags, 1) FROM users; -- β†’ 3

CRUD Operations

INSERT πŸ“₯

-- Basic insert
INSERT INTO users (username, email)
VALUES ('john_doe', 'john@example.com');
-- Insert multiple rows
INSERT INTO users (username, email) VALUES
('alice', 'alice@example.com'),
('bob', 'bob@example.com'),
('charlie', 'charlie@example.com');
-- Insert with default values
INSERT INTO users (username) VALUES ('dave');
-- email will use default, created_at will use CURRENT_TIMESTAMP
-- Insert from SELECT query
INSERT INTO users_backup (username, email)
SELECT username, email FROM users WHERE created_at > '2024-01-01';
-- Insert with RETURNING clause (get inserted row)
INSERT INTO users (username, email)
VALUES ('eve', 'eve@example.com')
RETURNING id, username, created_at;
-- Insert with ON CONFLICT (PostgreSQL 9.5+)
INSERT INTO users (username, email)
VALUES ('john_doe', 'john@example.com')
ON CONFLICT (username) DO NOTHING;
-- Insert with ON CONFLICT UPDATE
INSERT INTO users (username, email)
VALUES ('john_doe', 'newemail@example.com')
ON CONFLICT (username)
DO UPDATE SET email = EXCLUDED.email, updated_at = CURRENT_TIMESTAMP;

SELECT πŸ”

-- Basic select
SELECT * FROM users;
-- Select specific columns
SELECT id, username, email FROM users;
-- Select with WHERE clause
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE username = 'john_doe';
SELECT * FROM users WHERE created_at > '2024-01-01';
-- Select with multiple conditions
SELECT * FROM users
WHERE username = 'john_doe' AND email LIKE '%@example.com';
-- Select with OR condition
SELECT * FROM users
WHERE username = 'john_doe' OR username = 'alice';
-- Select with IN clause
SELECT * FROM users WHERE id IN (1, 2, 3);
SELECT * FROM users WHERE username IN ('john', 'alice', 'bob');
-- Select with NOT IN
SELECT * FROM users WHERE id NOT IN (1, 2, 3);
-- Select with BETWEEN
SELECT * FROM users WHERE id BETWEEN 1 AND 100;
SELECT * FROM users WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';
-- Select with LIKE (pattern matching)
SELECT * FROM users WHERE username LIKE 'john%'; -- Starts with 'john'
SELECT * FROM users WHERE username LIKE '%doe'; -- Ends with 'doe'
SELECT * FROM users WHERE username LIKE '%john%'; -- Contains 'john'
SELECT * FROM users WHERE username ILIKE 'JOHN%'; -- Case-insensitive
-- Select with IS NULL / IS NOT NULL
SELECT * FROM users WHERE email IS NULL;
SELECT * FROM users WHERE email IS NOT NULL;
-- Select with DISTINCT
SELECT DISTINCT username FROM users;
SELECT DISTINCT ON (username) * FROM users ORDER BY username, created_at DESC;
-- Select with ORDER BY
SELECT * FROM users ORDER BY username ASC;
SELECT * FROM users ORDER BY created_at DESC;
SELECT * FROM users ORDER BY username ASC, email DESC;
-- Select with LIMIT and OFFSET
SELECT * FROM users LIMIT 10;
SELECT * FROM users LIMIT 10 OFFSET 20; -- Skip first 20, get next 10
-- Select with aggregate functions
SELECT COUNT(*) FROM users;
SELECT COUNT(DISTINCT username) FROM users;
SELECT AVG(age) FROM users;
SELECT SUM(price) FROM orders;
SELECT MIN(created_at) FROM users;
SELECT MAX(created_at) FROM users;
-- Select with GROUP BY
SELECT status, COUNT(*)
FROM orders
GROUP BY status;
SELECT user_id, COUNT(*), SUM(price)
FROM orders
GROUP BY user_id;
-- Select with HAVING (filter groups)
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;
-- Select with CASE expression
SELECT
username,
CASE
WHEN age < 18 THEN 'Minor'
WHEN age < 65 THEN 'Adult'
ELSE 'Senior'
END AS age_group
FROM users;

UPDATE ✏️

-- Basic update
UPDATE users SET email = 'newemail@example.com' WHERE id = 1;
-- Update multiple columns
UPDATE users
SET email = 'newemail@example.com', updated_at = CURRENT_TIMESTAMP
WHERE id = 1;
-- Update with subquery
UPDATE orders
SET status = 'shipped'
WHERE user_id IN (SELECT id FROM users WHERE username = 'john_doe');
-- Update with JOIN (PostgreSQL 9.1+)
UPDATE orders o
SET status = 'cancelled'
FROM users u
WHERE o.user_id = u.id AND u.username = 'john_doe';
-- Update with RETURNING clause
UPDATE users
SET email = 'newemail@example.com'
WHERE id = 1
RETURNING id, username, email;
-- Update all rows (use with caution!)
UPDATE users SET updated_at = CURRENT_TIMESTAMP;
-- ⚠️ Always use WHERE clause unless you intend to update all rows

DELETE πŸ—‘οΈ

-- Basic delete
DELETE FROM users WHERE id = 1;
-- Delete with multiple conditions
DELETE FROM users
WHERE username = 'john_doe' AND email IS NULL;
-- Delete with subquery
DELETE FROM orders
WHERE user_id IN (SELECT id FROM users WHERE created_at < '2020-01-01');
-- Delete with JOIN (PostgreSQL 9.1+)
DELETE FROM orders o
USING users u
WHERE o.user_id = u.id AND u.username = 'john_doe';
-- Delete all rows (use with caution!)
DELETE FROM users;
-- ⚠️ Prefer TRUNCATE for deleting all rows (faster)
-- Delete with RETURNING clause
DELETE FROM users
WHERE id = 1
RETURNING id, username, email;

Indexes

Create Indexes πŸ“‡

-- Basic index
CREATE INDEX idx_users_username ON users(username);
-- Unique index
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);
-- Composite index (multiple columns)
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
-- Partial index (index subset of rows)
CREATE INDEX idx_active_users ON users(username)
WHERE status = 'active';
-- Expression index (index on expression result)
CREATE INDEX idx_users_lower_username ON users(LOWER(username));
-- Index with specific method
CREATE INDEX idx_users_email_hash ON users USING HASH(email);
CREATE INDEX idx_users_location_gist ON locations USING GIST(geom);
-- Concurrent index creation (doesn't block writes)
CREATE INDEX CONCURRENTLY idx_users_username ON users(username);
-- Full-text search index (GIN)
CREATE INDEX idx_products_description_gin
ON products USING GIN(to_tsvector('english', description));

Index Information πŸ“Š

-- List all indexes
SELECT indexname, tablename FROM pg_indexes WHERE schemaname = 'public';
-- Show indexes for a table
\d users
-- Show index size
SELECT pg_size_pretty(pg_relation_size('idx_users_username'));
# β†’ 64 kB
-- Show index usage statistics
SELECT
schemaname,
tablename,
indexname,
idx_scan AS index_scans,
idx_tup_read AS tuples_read,
idx_tup_fetch AS tuples_fetched
FROM pg_stat_user_indexes
WHERE tablename = 'users';
-- Check if index is being used
EXPLAIN ANALYZE SELECT * FROM users WHERE username = 'john_doe';

Drop Indexes πŸ—‘οΈ

-- Drop index
DROP INDEX idx_users_username;
-- Drop index if exists
DROP INDEX IF EXISTS idx_users_username;
-- Drop index concurrently (doesn't block reads)
DROP INDEX CONCURRENTLY idx_users_username;
-- Reindex (rebuild index)
REINDEX INDEX idx_users_username;
-- Reindex table (rebuild all indexes)
REINDEX TABLE users;
-- Reindex database
REINDEX DATABASE mydb;

Constraints

Primary Key πŸ”‘

-- Add primary key on creation
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50)
);
-- Add primary key constraint
ALTER TABLE users ADD PRIMARY KEY (id);
-- Composite primary key
CREATE TABLE order_items (
order_id INTEGER,
product_id INTEGER,
quantity INTEGER,
PRIMARY KEY (order_id, product_id)
);
-- Drop primary key
ALTER TABLE users DROP CONSTRAINT users_pkey;

Foreign Key πŸ”—

-- Add foreign key on creation
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total DECIMAL(10, 2)
);
-- Add foreign key with options
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
total DECIMAL(10, 2)
);
-- Add foreign key constraint
ALTER TABLE orders
ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id);
-- Foreign key with ON DELETE CASCADE
ALTER TABLE orders
ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- Foreign key with ON DELETE SET NULL
ALTER TABLE orders
ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
-- Drop foreign key
ALTER TABLE orders DROP CONSTRAINT orders_user_id_fkey;

Unique Constraint ✨

-- Add unique constraint on creation
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE,
email VARCHAR(100) UNIQUE
);
-- Add unique constraint
ALTER TABLE users ADD CONSTRAINT users_username_unique UNIQUE (username);
-- Composite unique constraint
CREATE TABLE user_roles (
user_id INTEGER,
role_id INTEGER,
UNIQUE (user_id, role_id)
);
-- Drop unique constraint
ALTER TABLE users DROP CONSTRAINT users_username_unique;

Check Constraint βœ“

-- Add check constraint on creation
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
price DECIMAL(10, 2) CHECK (price >= 0),
quantity INTEGER CHECK (quantity >= 0)
);
-- Add check constraint
ALTER TABLE products
ADD CONSTRAINT products_price_check CHECK (price >= 0);
-- Drop check constraint
ALTER TABLE products DROP CONSTRAINT products_price_check;

Not Null Constraint 🚫

-- Add NOT NULL on creation
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL
);
-- Add NOT NULL constraint
ALTER TABLE users ALTER COLUMN username SET NOT NULL;
-- Remove NOT NULL constraint
ALTER TABLE users ALTER COLUMN username DROP NOT NULL;

Joins & Queries

INNER JOIN πŸ”—

-- Basic inner join
SELECT u.username, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- Inner join with WHERE
SELECT u.username, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.total > 100;
-- Multiple inner joins
SELECT u.username, o.total, p.name
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN products p ON o.product_id = p.id;

LEFT JOIN ⬅️

-- Left join (all rows from left table)
SELECT u.username, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- Left join with WHERE (filters NULLs)
SELECT u.username, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.total IS NOT NULL;
-- Left join to find users without orders
SELECT u.username
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

RIGHT JOIN ➑️

-- Right join (all rows from right table)
SELECT u.username, o.total
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
-- Right join to find orders without users
SELECT o.id, o.total
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id
WHERE u.id IS NULL;

FULL OUTER JOIN ↔️

-- Full outer join (all rows from both tables)
SELECT u.username, o.total
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;
-- Full outer join to find mismatches
SELECT u.username, o.total
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id
WHERE u.id IS NULL OR o.id IS NULL;

CROSS JOIN βœ–οΈ

-- Cross join (Cartesian product)
SELECT u.username, p.name
FROM users u
CROSS JOIN products p;
-- Equivalent to:
SELECT u.username, p.name
FROM users u, products p;

Self Join πŸ”„

-- Self join (join table to itself)
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

Subqueries πŸ”

-- Subquery in WHERE clause
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 100);
-- Subquery in SELECT clause
SELECT
username,
(SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) AS order_count
FROM users;
-- Subquery with EXISTS
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id AND o.total > 100
);
-- Correlated subquery
SELECT
username,
(SELECT MAX(total) FROM orders WHERE orders.user_id = users.id) AS max_order
FROM users;
-- Subquery in FROM clause (derived table)
SELECT u.username, order_stats.total
FROM users u
JOIN (
SELECT user_id, SUM(total) AS total
FROM orders
GROUP BY user_id
) order_stats ON u.id = order_stats.user_id;

Common Table Expressions (CTEs) πŸ“

-- Basic CTE
WITH active_users AS (
SELECT * FROM users WHERE status = 'active'
)
SELECT * FROM active_users;
-- Multiple CTEs
WITH
active_users AS (
SELECT * FROM users WHERE status = 'active'
),
user_orders AS (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
)
SELECT u.username, uo.order_count
FROM active_users u
LEFT JOIN user_orders uo ON u.id = uo.user_id;
-- Recursive CTE (for hierarchical data)
WITH RECURSIVE employee_hierarchy AS (
-- Base case
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive case
SELECT e.id, e.name, e.manager_id, eh.level + 1
FROM employees e
JOIN employee_hierarchy eh ON e.manager_id = eh.id
)
SELECT * FROM employee_hierarchy;

Functions & Stored Procedures

Built-in Functions πŸ› οΈ

-- String functions
SELECT LENGTH('hello'); -- β†’ 5
SELECT UPPER('hello'); -- β†’ HELLO
SELECT LOWER('HELLO'); -- β†’ hello
SELECT SUBSTRING('hello' FROM 1 FOR 3); -- β†’ hel
SELECT REPLACE('hello', 'l', 'L'); -- β†’ heLLo
SELECT TRIM(' hello '); -- β†’ hello
SELECT CONCAT('hello', ' ', 'world'); -- β†’ hello world
SELECT POSITION('world' IN 'hello world'); -- β†’ 7
-- Numeric functions
SELECT ABS(-10); -- β†’ 10
SELECT ROUND(3.14159, 2); -- β†’ 3.14
SELECT CEIL(3.1); -- β†’ 4
SELECT FLOOR(3.9); -- β†’ 3
SELECT MOD(10, 3); -- β†’ 1
SELECT POWER(2, 3); -- β†’ 8
SELECT SQRT(16); -- β†’ 4
-- Date functions
SELECT CURRENT_DATE; -- β†’ 2024-01-15
SELECT CURRENT_TIME; -- β†’ 14:30:00
SELECT CURRENT_TIMESTAMP; -- β†’ 2024-01-15 14:30:00
SELECT NOW(); -- β†’ Current timestamp
SELECT EXTRACT(YEAR FROM NOW()); -- β†’ 2024
SELECT DATE_TRUNC('day', NOW()); -- β†’ 2024-01-15 00:00:00
SELECT AGE('2024-01-15', '2020-01-01'); -- β†’ 4 years 14 days
-- Aggregate functions
SELECT COUNT(*) FROM users;
SELECT AVG(price) FROM products;
SELECT SUM(total) FROM orders;
SELECT MIN(created_at) FROM users;
SELECT MAX(created_at) FROM users;
SELECT STRING_AGG(username, ', ') FROM users;

User-Defined Functions πŸ“¦

-- Create function (returns scalar)
CREATE OR REPLACE FUNCTION get_user_count()
RETURNS INTEGER AS $$
BEGIN
RETURN (SELECT COUNT(*) FROM users);
END;
$$ LANGUAGE plpgsql;
-- Call function
SELECT get_user_count();
-- Function with parameters
CREATE OR REPLACE FUNCTION get_user_by_id(user_id INTEGER)
RETURNS TABLE(id INTEGER, username VARCHAR, email VARCHAR) AS $$
BEGIN
RETURN QUERY
SELECT users.id, users.username, users.email
FROM users
WHERE users.id = user_id;
END;
$$ LANGUAGE plpgsql;
-- Call function
SELECT * FROM get_user_by_id(1);
-- Function with default parameters
CREATE OR REPLACE FUNCTION greet_user(name VARCHAR DEFAULT 'Guest')
RETURNS TEXT AS $$
BEGIN
RETURN 'Hello, ' || name || '!';
END;
$$ LANGUAGE plpgsql;
-- Function with OUT parameters
CREATE OR REPLACE FUNCTION divide_numbers(
a NUMERIC,
b NUMERIC,
OUT result NUMERIC,
OUT remainder NUMERIC
) AS $$
BEGIN
result := a / b;
remainder := MOD(a, b);
END;
$$ LANGUAGE plpgsql;
-- Call function
SELECT * FROM divide_numbers(10, 3);

Stored Procedures 🎯

-- Create stored procedure (PostgreSQL 11+)
CREATE OR REPLACE PROCEDURE update_user_email(
user_id INTEGER,
new_email VARCHAR
)
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE users
SET email = new_email, updated_at = CURRENT_TIMESTAMP
WHERE id = user_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'User with id % not found', user_id;
END IF;
END;
$$;
-- Call procedure
CALL update_user_email(1, 'newemail@example.com');
-- Procedure with transaction control
CREATE OR REPLACE PROCEDURE transfer_funds(
from_account INTEGER,
to_account INTEGER,
amount DECIMAL
)
LANGUAGE plpgsql
AS $$
BEGIN
-- Debit from source account
UPDATE accounts SET balance = balance - amount WHERE id = from_account;
-- Credit to destination account
UPDATE accounts SET balance = balance + amount WHERE id = to_account;
-- If any update fails, transaction will rollback
END;
$$;

Triggers 🎬

-- Create trigger function
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Trigger for audit logging
CREATE OR REPLACE FUNCTION audit_user_changes()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO user_audit_log (user_id, action, changed_at)
VALUES (NEW.id, TG_OP, CURRENT_TIMESTAMP);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER users_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION audit_user_changes();
-- Drop trigger
DROP TRIGGER IF EXISTS update_users_updated_at ON users;

Transactions

Transaction Control πŸ’Ό

-- Start transaction
BEGIN;
-- Or
START TRANSACTION;
-- Commit transaction
COMMIT;
-- Rollback transaction
ROLLBACK;
-- Example transaction
BEGIN;
INSERT INTO users (username, email) VALUES ('john', 'john@example.com');
INSERT INTO orders (user_id, total) VALUES (1, 100.00);
COMMIT;
-- Transaction with rollback on error
BEGIN;
INSERT INTO users (username, email) VALUES ('john', 'john@example.com');
-- If error occurs, rollback
ROLLBACK;

Transaction Isolation Levels πŸ”’

-- Set transaction isolation level
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Available isolation levels:
-- READ UNCOMMITTED (not available in PostgreSQL, treated as READ COMMITTED)
-- READ COMMITTED (default)
-- REPEATABLE READ
-- SERIALIZABLE
-- Example
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM users WHERE id = 1;
-- Other operations
COMMIT;
-- Show current isolation level
SHOW transaction_isolation;
# β†’ read committed

Savepoints πŸ“

-- Create savepoint
BEGIN;
INSERT INTO users (username, email) VALUES ('john', 'john@example.com');
SAVEPOINT sp1;
INSERT INTO orders (user_id, total) VALUES (1, 100.00);
-- If error, rollback to savepoint
ROLLBACK TO SAVEPOINT sp1;
-- Continue transaction
INSERT INTO orders (user_id, total) VALUES (1, 200.00);
COMMIT;
-- Release savepoint
RELEASE SAVEPOINT sp1;

Locking πŸ”

-- Row-level locking
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- Row is locked until transaction commits
UPDATE users SET email = 'new@example.com' WHERE id = 1;
COMMIT;
-- Share lock (allows reads, prevents writes)
BEGIN;
SELECT * FROM users WHERE id = 1 FOR SHARE;
COMMIT;
-- Table-level locking
LOCK TABLE users IN EXCLUSIVE MODE;
-- Operations...
UNLOCK TABLE users;
-- Advisory locks (application-level)
SELECT pg_advisory_lock(12345);
-- Critical section...
SELECT pg_advisory_unlock(12345);

Performance & Optimization

EXPLAIN & ANALYZE πŸ“Š

-- Basic EXPLAIN
EXPLAIN SELECT * FROM users WHERE username = 'john_doe';
-- EXPLAIN with ANALYZE (executes query)
EXPLAIN ANALYZE SELECT * FROM users WHERE username = 'john_doe';
-- EXPLAIN with verbose output
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT * FROM users WHERE username = 'john_doe';
-- EXPLAIN with format options
EXPLAIN (FORMAT JSON) SELECT * FROM users WHERE username = 'john_doe';
EXPLAIN (FORMAT XML) SELECT * FROM users WHERE username = 'john_doe';

Query Optimization Tips ⚑

-- βœ… Use indexes on frequently queried columns
CREATE INDEX idx_users_username ON users(username);
-- βœ… Use EXPLAIN ANALYZE to identify slow queries
EXPLAIN ANALYZE SELECT * FROM users WHERE username = 'john_doe';
-- βœ… Use LIMIT when you don't need all rows
SELECT * FROM users LIMIT 10; -- βœ… Good
SELECT * FROM users; -- ❌ Bad if you only need 10 rows
-- βœ… Use specific columns instead of SELECT *
SELECT id, username FROM users; -- βœ… Good
SELECT * FROM users; -- ❌ Bad if you don't need all columns
-- βœ… Use WHERE to filter early
SELECT * FROM orders WHERE user_id = 1 AND total > 100; -- βœ… Good
-- βœ… Use JOINs instead of subqueries when possible
-- βœ… Good: JOIN
SELECT u.username, o.total
FROM users u
JOIN orders o ON u.id = o.user_id;
-- ❌ Bad: Subquery (usually slower)
SELECT username, (SELECT total FROM orders WHERE user_id = users.id)
FROM users;
-- βœ… Use EXISTS instead of COUNT(*) for existence checks
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders WHERE user_id = u.id); -- βœ… Good
SELECT * FROM users u
WHERE (SELECT COUNT(*) FROM orders WHERE user_id = u.id) > 0; -- ❌ Bad

Vacuum & Analyze 🧹

-- VACUUM (reclaims storage, updates statistics)
VACUUM;
-- VACUUM specific table
VACUUM users;
-- VACUUM FULL (rewrites table, requires exclusive lock)
VACUUM FULL users;
-- VACUUM ANALYZE (vacuum and update statistics)
VACUUM ANALYZE users;
-- ANALYZE (update statistics for query planner)
ANALYZE users;
-- ANALYZE all tables
ANALYZE;
-- Show table statistics
SELECT
schemaname,
tablename,
n_live_tup,
n_dead_tup,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
WHERE tablename = 'users';

Connection Pooling πŸ”„

-- Show current connections
SELECT
pid,
usename,
application_name,
client_addr,
state,
query_start,
state_change
FROM pg_stat_activity
WHERE datname = 'mydb';
-- Show connection count
SELECT COUNT(*) FROM pg_stat_activity WHERE datname = 'mydb';
-- Kill specific connection
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE pid = 12345;
-- Show max connections setting
SHOW max_connections;
# β†’ 100

Backup & Recovery

pg_dump (Backup) πŸ’Ύ

Terminal window
# Backup single database
pg_dump -U postgres -d mydb > backup.sql
# Backup with custom format (compressed, allows selective restore)
pg_dump -U postgres -d mydb -Fc -f backup.dump
# Backup with compression
pg_dump -U postgres -d mydb | gzip > backup.sql.gz
# Backup specific schema only
pg_dump -U postgres -d mydb -n public > backup.sql
# Backup specific tables only
pg_dump -U postgres -d mydb -t users -t orders > backup.sql
# Backup data only (no schema)
pg_dump -U postgres -d mydb --data-only > data_only.sql
# Backup schema only (no data)
pg_dump -U postgres -d mydb --schema-only > schema_only.sql
# Backup with verbose output
pg_dump -U postgres -d mydb -v > backup.sql

pg_restore (Restore) πŸ”„

Terminal window
# Restore from SQL file
psql -U postgres -d mydb < backup.sql
# Restore from custom format
pg_restore -U postgres -d mydb backup.dump
# Restore with verbose output
pg_restore -U postgres -d mydb -v backup.dump
# Restore specific tables
pg_restore -U postgres -d mydb -t users backup.dump
# Restore with clean (drop objects first)
pg_restore -U postgres -d mydb --clean backup.dump
# Restore schema only
pg_restore -U postgres -d mydb --schema-only backup.dump

pg_dumpall (Backup All) 🌐

Terminal window
# Backup all databases (including globals)
pg_dumpall -U postgres > all_databases.sql
# Backup globals only (users, roles, etc.)
pg_dumpall -U postgres --globals-only > globals.sql
# Restore all databases
psql -U postgres < all_databases.sql

Continuous Archiving & Point-in-Time Recovery πŸ“…

Terminal window
# Enable WAL archiving in postgresql.conf
wal_level = replica
archive_mode = on
archive_command = 'cp %p /path/to/archive/%f'
# Base backup
pg_basebackup -D /path/to/backup -Ft -z -P
# Restore from base backup and WAL
# 1. Copy base backup to data directory
# 2. Create recovery.conf (PostgreSQL 12+) or use postgresql.auto.conf
# 3. Start PostgreSQL server

User Management

Create & Manage Users πŸ‘€

-- Create user (role)
CREATE USER john WITH PASSWORD 'secure_password';
-- Create user with additional options
CREATE USER alice
WITH
PASSWORD 'secure_password'
CREATEDB
CREATEROLE
LOGIN;
-- Create superuser
CREATE USER admin WITH SUPERUSER PASSWORD 'secure_password';
-- Alter user password
ALTER USER john WITH PASSWORD 'new_password';
-- Alter user options
ALTER USER john WITH CREATEDB;
ALTER USER john WITH NOCREATEDB;
-- Rename user
ALTER USER john RENAME TO john_doe;
-- Drop user
DROP USER john;
-- Drop user if exists
DROP USER IF EXISTS john;
-- List all users
SELECT usename FROM pg_user;
-- Show user privileges
\du
-- or
SELECT * FROM pg_roles;

Roles & Privileges πŸ”

-- Create role
CREATE ROLE developers;
-- Grant privileges to role
GRANT SELECT, INSERT, UPDATE ON users TO developers;
GRANT ALL PRIVILEGES ON DATABASE mydb TO developers;
-- Grant role to user
GRANT developers TO john;
-- Revoke privileges
REVOKE SELECT ON users FROM developers;
-- Revoke role
REVOKE developers FROM john;
-- Create role with login
CREATE ROLE app_user WITH LOGIN PASSWORD 'password';
-- Grant schema usage
GRANT USAGE ON SCHEMA public TO developers;
-- Grant all privileges on all tables
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO developers;
-- Grant privileges on future tables
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL PRIVILEGES ON TABLES TO developers;
-- Show role memberships
SELECT
r.rolname AS role,
m.rolname AS member
FROM pg_roles r
JOIN pg_auth_members am ON r.oid = am.roleid
JOIN pg_roles m ON am.member = m.oid;

Database Privileges πŸ—„οΈ

-- Grant database privileges
GRANT CONNECT ON DATABASE mydb TO john;
GRANT CREATE ON DATABASE mydb TO developers;
-- Revoke database privileges
REVOKE CREATE ON DATABASE mydb FROM developers;
-- Show database privileges
SELECT
datname,
datacl
FROM pg_database
WHERE datname = 'mydb';

Best Practices

βœ… Do’s

-- βœ… Always use parameterized queries to prevent SQL injection
-- βœ… Good: Use placeholders
PREPARE get_user AS SELECT * FROM users WHERE id = $1;
EXECUTE get_user(1);
-- βœ… Use transactions for related operations
BEGIN;
INSERT INTO users (username) VALUES ('john');
INSERT INTO orders (user_id, total) VALUES (1, 100);
COMMIT;
-- βœ… Use appropriate data types
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- βœ… SERIAL for auto-increment
username VARCHAR(50) NOT NULL, -- βœ… VARCHAR with limit
email VARCHAR(100) UNIQUE, -- βœ… UNIQUE constraint
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- βœ… Default timestamp
);
-- βœ… Create indexes on foreign keys and frequently queried columns
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_users_username ON users(username);
-- βœ… Use EXPLAIN ANALYZE to optimize queries
EXPLAIN ANALYZE SELECT * FROM users WHERE username = 'john';
-- βœ… Use LIMIT when you don't need all rows
SELECT * FROM users LIMIT 10;
-- βœ… Use specific column names instead of SELECT *
SELECT id, username, email FROM users; -- βœ… Good
-- βœ… Use meaningful table and column names
CREATE TABLE user_accounts ( -- βœ… Good
account_id SERIAL PRIMARY KEY,
user_name VARCHAR(50)
);
-- βœ… Use constraints to enforce data integrity
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
quantity INTEGER CHECK (quantity > 0), -- βœ… Check constraint
user_id INTEGER REFERENCES users(id) -- βœ… Foreign key
);
-- βœ… Use VACUUM and ANALYZE regularly
VACUUM ANALYZE users;
-- βœ… Use connection pooling for applications
-- Use pgBouncer or similar tools
-- βœ… Backup databases regularly
-- Schedule pg_dump in cron or similar

❌ Don’ts

-- ❌ Don't use SELECT * in production queries
SELECT * FROM users; -- ❌ Bad: fetches unnecessary columns
-- ❌ Don't forget to use transactions for related operations
INSERT INTO users (username) VALUES ('john');
INSERT INTO orders (user_id, total) VALUES (1, 100);
-- ❌ Bad: If second insert fails, first insert remains
-- ❌ Don't use string concatenation for SQL queries (SQL injection risk)
-- ❌ Bad: Vulnerable to SQL injection
-- SELECT * FROM users WHERE username = '$username';
-- ❌ Don't create indexes on every column
CREATE INDEX idx_users_id ON users(id); -- ❌ Bad: Primary key already indexed
CREATE INDEX idx_users_created_at ON users(created_at); -- ❌ Bad if rarely queried
-- ❌ Don't use TEXT for fixed-length strings
CREATE TABLE users (
username TEXT -- ❌ Bad: Use VARCHAR(50) if max length is known
);
-- ❌ Don't ignore NULL values without considering them
SELECT * FROM users WHERE email != 'test@example.com';
-- ❌ Bad: Doesn't include rows where email IS NULL
-- βœ… Good: SELECT * FROM users WHERE email IS NULL OR email != 'test@example.com';
-- ❌ Don't use functions in WHERE clauses without indexes
SELECT * FROM users WHERE LOWER(username) = 'john'; -- ❌ Bad: Can't use index
-- βœ… Good: CREATE INDEX idx_users_lower_username ON users(LOWER(username));
-- ❌ Don't forget to set up proper backup strategy
-- ❌ Bad: No backups configured
-- ❌ Don't use superuser for application connections
-- ❌ Bad: Application connects as postgres user
-- βœ… Good: Create dedicated user with minimal privileges
-- ❌ Don't ignore VACUUM warnings
-- PostgreSQL will warn when tables need vacuuming
-- ❌ Bad: Ignore warnings
-- βœ… Good: Run VACUUM ANALYZE regularly

Common Pitfalls

⚠️ NULL Handling

-- ⚠️ NULL comparison doesn't work with = or !=
SELECT * FROM users WHERE email = NULL; -- ❌ Returns no rows
SELECT * FROM users WHERE email IS NULL; -- βœ… Correct
SELECT * FROM users WHERE email != 'test@example.com';
-- ⚠️ Doesn't include rows where email IS NULL
SELECT * FROM users WHERE email IS NULL OR email != 'test@example.com'; -- βœ… Correct
-- ⚠️ NULL in arithmetic operations
SELECT 10 + NULL; -- β†’ NULL
SELECT COALESCE(price, 0) + 10 FROM products; -- βœ… Handle NULL

⚠️ String Comparison

-- ⚠️ String comparison is case-sensitive
SELECT * FROM users WHERE username = 'John'; -- Won't match 'john'
SELECT * FROM users WHERE username ILIKE 'john'; -- βœ… Case-insensitive
-- ⚠️ Trailing spaces matter
SELECT * FROM users WHERE username = 'john '; -- Won't match 'john'
SELECT * FROM users WHERE TRIM(username) = 'john'; -- βœ… Handle spaces

⚠️ Date/Time Issues

-- ⚠️ Timezone handling
SELECT CURRENT_TIMESTAMP; -- Includes timezone
SELECT NOW(); -- Includes timezone
SELECT CURRENT_DATE; -- No timezone
-- ⚠️ Date arithmetic
SELECT '2024-01-15'::DATE + INTERVAL '1 day'; -- βœ… Correct
SELECT '2024-01-15'::DATE + 1; -- ❌ Error: Can't add integer to date
-- ⚠️ Timestamp comparison
SELECT * FROM users WHERE created_at = '2024-01-15';
-- ⚠️ May not match due to time component
SELECT * FROM users WHERE DATE(created_at) = '2024-01-15'; -- βœ… Correct
SELECT * FROM users WHERE created_at::DATE = '2024-01-15'; -- βœ… Correct

⚠️ Transaction Issues

-- ⚠️ Uncommitted transactions block other operations
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- ⚠️ Other transactions trying to update this row will wait
-- ⚠️ If you forget to COMMIT, lock persists
COMMIT; -- βœ… Always commit or rollback
-- ⚠️ Long-running transactions
BEGIN;
-- Long operation...
-- ⚠️ Holds locks and prevents VACUUM
COMMIT; -- βœ… Keep transactions short

⚠️ Performance Issues

-- ⚠️ Missing indexes on foreign keys
CREATE TABLE orders (
user_id INTEGER REFERENCES users(id) -- ⚠️ No index on user_id
);
CREATE INDEX idx_orders_user_id ON orders(user_id); -- βœ… Add index
-- ⚠️ Full table scans on large tables
SELECT * FROM users WHERE LOWER(username) = 'john';
-- ⚠️ Can't use index on username
CREATE INDEX idx_users_lower_username ON users(LOWER(username)); -- βœ… Expression index
-- ⚠️ N+1 query problem
-- ❌ Bad: Multiple queries in loop
-- SELECT * FROM users;
-- For each user: SELECT * FROM orders WHERE user_id = ?
-- βœ… Good: Single query with JOIN
SELECT u.*, o.* FROM users u LEFT JOIN orders o ON u.id = o.user_id;

Critical Warning: Always test queries with EXPLAIN ANALYZE before deploying to production. Missing indexes or inefficient queries can cause severe performance degradation.

Critical Warning: Never run DROP TABLE or TRUNCATE without WHERE clause unless you’re absolutely certain. These operations cannot be easily undone.

Critical Warning: Always use transactions for related operations. If one operation fails, related operations should rollback to maintain data consistency.