PostgreSQL Best Practices
Contents
Quick Start
Installation & Version
Current version is PostgreSQL 18.3.
macOS
# Install via Homebrew
brew install postgresql@18
# Start service
brew services start postgresql@18
# Or start manually
pg_ctl -D /opt/homebrew/var/postgresql@18 start
# Connect
psql -U postgres -d postgresWindows
- Download PostgreSQL Windows installer
- Run
postgresql-18.x.x-1-windows-x64.exe - Choose installation directory and data directory
- Set password
- Default port: 5432
# After installation, connect using psql
psql -U postgresLinux (Ubuntu)
# Create PostgreSQL repository config file
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
# Import repository signing key
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg
# Update and install
sudo apt update
sudo apt install -y postgresql-18
# Start service
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Connect (switch to postgres user)
sudo -u postgres psqlDocker Compose Deployment
services:
postgres:
image: postgres:18-alpine
container_name: postgres
restart: unless-stopped
ports:
- "5432:5432"
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: password123
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d myapp"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:Start: docker compose up -d
Connect: psql -h localhost -U admin -d myapp
Basic Operations
-- View databases
\l
-- Connect to database
\c myapp
-- View tables
\dt
-- Basic CRUD
INSERT INTO users (name, email) VALUES ('Alice', '[email protected]');
SELECT * FROM users WHERE name = 'Alice';
UPDATE users SET age = 30 WHERE id = 1;
DELETE FROM users WHERE id = 1;Schema Design
Table Design Principles
-- Use appropriate data types
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
age INTEGER CHECK (age >= 0),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Add comments
COMMENT ON TABLE users IS 'User table';
COMMENT ON COLUMN users.id IS 'Unique user identifier';Relationship Design
-- Foreign key constraint
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status) WHERE status != 'completed';Indexing Strategies
Index Types
-- B-tree index (default)
CREATE INDEX idx_users_email ON users(email);
-- Composite index
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- Partial index
CREATE INDEX idx_users_active ON users(email) WHERE deleted_at IS NULL;
-- GIN index (JSONB)
CREATE INDEX idx_products_data ON products USING GIN(data);
-- GiST index (full-text search)
CREATE INDEX idx_articles_search ON articles USING GIST(to_tsvector('english', content));
-- Unique index
CREATE UNIQUE INDEX idx_users_username ON users(username);Index Principles
| Principle | Description |
|---|---|
| Selectivity | Index high-cardinality fields |
| Query Pattern | Consider WHERE, JOIN, ORDER BY |
| Avoid Redundancy | Don’t create function index on already indexed columns |
| Maintenance Cost | Indexes affect write performance |
Expression Indexes
-- Case-insensitive email
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- Query can use it directly
SELECT * FROM users WHERE LOWER(email) = LOWER('[email protected]');Query Optimization
EXPLAIN Analysis
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 5
ORDER BY order_count DESC
LIMIT 20;Optimization Tips
-- Avoid SELECT *
SELECT u.id, u.name FROM users u WHERE u.id = 1;
-- Use LIMIT
SELECT * FROM orders LIMIT 100;
-- Use EXISTS instead of IN
SELECT * FROM users u WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);
-- Use CTE (Common Table Expression)
WITH recent_orders AS (
SELECT user_id, SUM(total) as total
FROM orders
WHERE created_at > '2024-01-01'
GROUP BY user_id
)
SELECT u.name, r.total
FROM users u
JOIN recent_orders r ON u.id = r.user_id;JSONB Operations
PostgreSQL has built-in JSONB support:
-- Create JSONB column
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
data JSONB
);
-- Insert
INSERT INTO products (name, data) VALUES (
'Widget',
'{"color": "red", "size": "large", "tags": ["new", "sale"]}'
);
-- Query JSONB
SELECT * FROM products WHERE data->>'color' = 'red';
-- Use GIN index for queries
CREATE INDEX idx_products_data ON products USING GIN(data);
-- Containment query
SELECT * FROM products WHERE data @> '{"color": "red"}';
-- Any key query
SELECT * FROM products WHERE data ? 'tags';
-- JSONB iteration
SELECT id, jsonb_array_elements_text(data->'tags') as tag
FROM products;Partitioned Tables
Range Partitioning
-- Create partitioned table
CREATE TABLE orders (
id BIGSERIAL,
user_id BIGINT NOT NULL,
total DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
-- Create partitions
CREATE TABLE orders_2024_q1 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE orders_2024_q2 PARTITION OF orders
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
-- Default partition (captures unmatched data)
CREATE TABLE orders_default PARTITION OF orders DEFAULT;List Partitioning
CREATE TABLE products (
id SERIAL,
name VARCHAR(255),
category VARCHAR(50),
PRIMARY KEY (id, category)
) PARTITION BY LIST (category);
CREATE TABLE products_electronics PARTITION OF products
FOR VALUES IN ('electronics', 'gadgets');
CREATE TABLE products_clothing PARTITION OF products
FOR VALUES IN ('clothing', 'shoes');Transactions and Concurrency
Transaction Isolation Levels
-- READ COMMITTED (default)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- REPEATABLE READ
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- SERIALIZABLE
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;Row-level Locking
-- SELECT FOR UPDATE
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- Perform update
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- SKIP LOCKED (non-blocking)
SELECT * FROM orders
WHERE status = 'pending'
ORDER BY created_at
LIMIT 10
FOR UPDATE SKIP LOCKED;Performance Optimization
VACUUM and ANALYZE
-- Manual VACUUM
VACUUM ANALYZE users;
-- View table statistics
SELECT schemaname, tablename, n_live_tup, n_dead_tup
FROM pg_stat_user_tables
WHERE tablename = 'users';Connection Pooling
-- View current connections
SELECT count(*) FROM pg_stat_activity;
-- View max connections
SHOW max_connections;Configuration Parameters
-- Key parameters
ALTER SYSTEM SET shared_buffers = '4GB';
ALTER SYSTEM SET effective_cache_size = '12GB';
ALTER SYSTEM SET work_mem = '256MB';
ALTER SYSTEM SET maintenance_work_mem = '1GB';Backup and Recovery
Logical Backup
# Backup single database
pg_dump -U postgres -F c myapp > myapp.dump
# Restore
pg_restore -U postgres -d myapp myapp.dump
# Plain SQL export
pg_dump -U postgres myapp > myapp.sqlPhysical Backup
# Using pg_basebackup
pg_basebackup -h localhost -U replication -D /backup -Ft -z -PCommon Functions
-- String
SELECT CONCAT(first_name, ' ', last_name) FROM users;
SELECT UPPER(email);
SELECT LOWER(email);
SELECT TRIM(name);
-- Date
SELECT NOW();
SELECT CURRENT_DATE;
SELECT DATE_TRUNC('month', created_at);
SELECT AGE(created_at); -- Days since
-- Aggregation
SELECT COUNT(*), SUM(total), AVG(total), MIN(total), MAX(total)
FROM orders;
-- Window functions
SELECT
name,
department,
salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rank,
SUM(salary) OVER (PARTITION BY department) as dept_total
FROM employees;