Back to Home
    PostgreSQLPostgreSQL v16Intermediate

    PostgreSQL

    PostgreSQL cheat sheet: psql CLI, users & roles, backup & restore, VACUUM, locks, JSONB, CTEs, and indexes.

    15 min read
    postgresqlsqldatabaseperformancedevopsbackup

    JSONB

    1 topic

    JSONB Operators

    sql
    -- Store JSON
    CREATE TABLE events (
      id SERIAL PRIMARY KEY,
      payload JSONB NOT NULL
    );
    
    -- Query JSONB
    SELECT payload->>'name' FROM events;       -- Text
    SELECT payload->'address'->>'city' FROM events;
    SELECT * FROM events WHERE payload->>'type' = 'click';
    
    -- JSONB containment
    SELECT * FROM events WHERE payload @> '{"type": "click"}';
    
    -- JSONB array
    SELECT * FROM events WHERE payload->'tags' ? 'urgent';
    
    -- Index for JSONB queries
    CREATE INDEX idx_events_payload ON events USING GIN (payload);
    CREATE INDEX idx_events_type ON events ((payload->>'type'));

    ⚡ GIN index makes @> and ? operators fast on JSONB columns

    💡 ->> returns text, -> returns JSON

    CTEs & Recursive Queries

    1 topic

    WITH / CTE

    sql
    -- CTE
    WITH monthly_revenue AS (
      SELECT
        DATE_TRUNC('month', created_at) AS month,
        SUM(amount) AS revenue
      FROM orders
      WHERE status = 'completed'
      GROUP BY 1
    )
    SELECT month, revenue,
      revenue - LAG(revenue) OVER (ORDER BY month) AS growth
    FROM monthly_revenue;
    
    -- Recursive CTE (tree traversal)
    WITH RECURSIVE tree AS (
      SELECT id, parent_id, name, 0 AS depth
      FROM categories WHERE parent_id IS NULL
      UNION ALL
      SELECT c.id, c.parent_id, c.name, t.depth + 1
      FROM categories c
      JOIN tree t ON t.id = c.parent_id
    )
    SELECT * FROM tree ORDER BY depth;

    💡 CTEs make complex queries readable by naming sub-results

    ⚡ Recursive CTEs are the standard way to query tree/graph structures

    Indexes & Performance

    1 topic

    Index Types

    sql
    -- B-tree (default — equality, range, sort)
    CREATE INDEX idx_users_email ON users (email);
    
    -- Partial index (smaller, faster for filtered queries)
    CREATE INDEX idx_active_users ON users (created_at)
    WHERE status = 'active';
    
    -- Composite index
    CREATE INDEX idx_orders_user_date ON orders (user_id, created_at DESC);
    
    -- GIN for JSONB / arrays / full-text
    CREATE INDEX idx_docs_fts ON docs USING GIN (to_tsvector('english', body));
    
    -- Explain query plan
    EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';
    
    -- Find missing indexes (slow queries)
    SELECT * FROM pg_stat_user_tables ORDER BY seq_scan DESC LIMIT 10;

    💡 EXPLAIN ANALYZE is the first tool for performance issues

    ⚡ Partial indexes are often 10x smaller and faster than full indexes

    psql CLI

    2 topics

    Connect & Navigate

    bash
    # Connect
    psql -h localhost -p 5432 -U postgres -d mydb
    psql "postgresql://user:pass@host:5432/mydb"
    
    # Inside psql
    \l                  -- list databases
    \c mydb             -- connect to database
    \dt                 -- list tables
    \dt schema.*        -- list tables in schema
    \d users            -- describe table (columns, indexes, constraints)
    \d+ users           -- verbose description
    \di                 -- list indexes
    \dv                 -- list views
    \df                 -- list functions
    \dn                 -- list schemas
    \du                 -- list roles/users
    
    \x                  -- toggle expanded output (useful for wide rows)
    \timing             -- show query execution time
    \e                  -- open query in $EDITOR
    \i /path/to/file.sql  -- execute SQL file
    \o /path/out.txt    -- write output to file
    \q                  -- quit

    💡 \x auto is the best default — switches to expanded only when output is wide

    ⚡ \timing on helps you spot slow queries during interactive sessions

    Run Queries Non-Interactively

    bash
    # Execute a single command
    psql -U postgres -d mydb -c "SELECT count(*) FROM users;"
    
    # Execute a SQL file
    psql -U postgres -d mydb -f /path/to/migration.sql
    
    # Pipe SQL
    echo "SELECT now();" | psql -U postgres -d mydb
    
    # Suppress headers/footers (for scripting)
    psql -U postgres -d mydb -t -A -c "SELECT id FROM users WHERE active;"
    # -t  = tuples only (no header/footer)
    # -A  = unaligned output
    # -F, = comma-separated (combine with -A)
    
    # Save query result to CSV
    psql -U postgres -d mydb -c "\COPY users TO '/tmp/users.csv' CSV HEADER;"

    ⚡ -t -A -F, is the lightweight way to produce CSV without superuser COPY privileges

    Users & Roles

    2 topics

    Create & Manage Roles

    sql
    -- Create a login role
    CREATE USER app_user WITH PASSWORD 'secret';
    
    -- Create a role (no login — for grouping privileges)
    CREATE ROLE readonly;
    
    -- Grant role membership
    GRANT readonly TO app_user;
    
    -- Alter password
    ALTER USER app_user WITH PASSWORD 'newpass';
    
    -- Make superuser / remove superuser
    ALTER USER app_user SUPERUSER;
    ALTER USER app_user NOSUPERUSER;
    
    -- List roles
    \du
    SELECT rolname, rolsuper, rolcanlogin FROM pg_roles;
    
    -- Drop user (must own no objects)
    DROP USER app_user;

    💡 Prefer roles for privilege grouping — assign users to roles rather than granting directly

    ⚠️ Superuser bypasses all permission checks; avoid in application accounts

    Grant & Revoke Privileges

    sql
    -- Grant table privileges
    GRANT SELECT, INSERT, UPDATE ON TABLE users TO app_user;
    GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_user;
    
    -- Grant on future tables (important!)
    ALTER DEFAULT PRIVILEGES IN SCHEMA public
      GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
    
    -- Grant schema usage
    GRANT USAGE ON SCHEMA public TO app_user;
    
    -- Grant sequence (needed for INSERT with SERIAL/BIGSERIAL)
    GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;
    
    -- Read-only role pattern
    GRANT USAGE ON SCHEMA public TO readonly;
    GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
    ALTER DEFAULT PRIVILEGES IN SCHEMA public
      GRANT SELECT ON TABLES TO readonly;
    
    -- Revoke
    REVOKE INSERT ON TABLE users FROM app_user;
    
    -- Show grants
    \dp users

    ⚡ ALTER DEFAULT PRIVILEGES ensures new tables automatically get the right grants — easy to forget

    💡 Create a readonly role and add monitoring/analytics users to it

    Backup & Restore

    2 topics

    pg_dump / pg_restore

    bash
    # Dump single database (plain SQL)
    pg_dump -U postgres mydb > mydb.sql
    
    # Dump in custom format (compressed, supports parallel restore)
    pg_dump -U postgres -Fc mydb > mydb.dump
    
    # Dump specific tables
    pg_dump -U postgres -t users -t orders mydb > subset.sql
    
    # Dump schema only (no data)
    pg_dump -U postgres --schema-only mydb > schema.sql
    
    # Dump data only
    pg_dump -U postgres --data-only mydb > data.sql
    
    # Restore plain SQL dump
    psql -U postgres -d mydb < mydb.sql
    
    # Restore custom format (faster, parallel)
    pg_restore -U postgres -d mydb -j 4 mydb.dump
    # -j 4 = 4 parallel workers
    
    # Restore to a new database
    createdb -U postgres mydb_new
    pg_restore -U postgres -d mydb_new mydb.dump

    ⚡ -Fc (custom format) is almost always better than plain SQL — smaller, faster restore, selective restore

    💡 -j N parallel restore can cut restore time by 4-8x on multi-core servers

    Dump All Databases

    bash
    # Dump all databases + global objects (roles, tablespaces)
    pg_dumpall -U postgres > full_backup.sql
    
    # Dump only global objects (roles)
    pg_dumpall -U postgres --globals-only > globals.sql
    
    # Restore full dump
    psql -U postgres < full_backup.sql
    
    # Scheduled backup with cron
    # crontab -e
    0 2 * * * pg_dump -U postgres -Fc mydb > /backups/mydb_$(date +\%F).dump
    # Keep last 7 days
    0 3 * * * find /backups -name '*.dump' -mtime +7 -delete
    
    # Test backup integrity
    pg_restore --list mydb.dump | head -20

    ⚡ pg_dumpall is the only way to back up roles and tablespaces — run it alongside per-db dumps

    ⚠️ Always test restores in a staging environment; an untested backup is not a backup

    Maintenance

    1 topic

    VACUUM & ANALYZE

    sql
    -- Reclaim dead row space (autovacuum does this automatically)
    VACUUM users;
    
    -- Vacuum + update planner statistics
    VACUUM ANALYZE users;
    
    -- Full vacuum: rewrites table, reclaims disk space (locks table!)
    VACUUM FULL users;
    
    -- Update query planner statistics only
    ANALYZE users;
    
    -- Reindex (fix bloated / corrupted indexes)
    REINDEX TABLE users;
    REINDEX INDEX idx_users_email;
    
    -- Non-blocking reindex (Postgres 12+)
    REINDEX TABLE CONCURRENTLY users;
    
    -- Check table bloat
    SELECT
      relname,
      n_dead_tup,
      n_live_tup,
      round(n_dead_tup * 100.0 / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_pct
    FROM pg_stat_user_tables
    ORDER BY n_dead_tup DESC
    LIMIT 10;

    ⚡ VACUUM FULL reclaims disk but locks the table — use during maintenance windows only

    💡 Autovacuum handles routine cleanup; manual VACUUM is for after bulk deletes or table bloat

    Monitoring & Connections

    2 topics

    Active Connections & Long Queries

    sql
    -- All active connections
    SELECT pid, usename, application_name, client_addr, state,
           wait_event_type, wait_event,
           now() - query_start AS duration,
           query
    FROM pg_stat_activity
    WHERE state != 'idle'
    ORDER BY duration DESC;
    
    -- Kill a specific connection
    SELECT pg_terminate_backend(pid)
    FROM pg_stat_activity
    WHERE pid = 12345;
    
    -- Kill all idle connections older than 10 minutes
    SELECT pg_terminate_backend(pid)
    FROM pg_stat_activity
    WHERE state = 'idle'
      AND now() - state_change > INTERVAL '10 minutes';
    
    -- Connection count by database
    SELECT datname, count(*) AS connections
    FROM pg_stat_activity
    GROUP BY datname
    ORDER BY connections DESC;

    💡 pg_stat_activity is the primary window into what's running on your server

    ⚠️ Use pg_terminate_backend carefully — it immediately drops the connection

    Locks & Blocking Queries

    sql
    -- Find blocking queries
    SELECT
      blocked.pid          AS blocked_pid,
      blocked.query        AS blocked_query,
      blocking.pid         AS blocking_pid,
      blocking.query       AS blocking_query,
      now() - blocked.query_start AS blocked_duration
    FROM pg_stat_activity blocked
    JOIN pg_stat_activity blocking
      ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
    WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;
    
    -- Active locks
    SELECT relation::regclass, mode, granted, pid
    FROM pg_locks
    WHERE relation IS NOT NULL
    ORDER BY relation;
    
    -- Database size
    SELECT pg_database.datname,
           pg_size_pretty(pg_database_size(pg_database.datname)) AS size
    FROM pg_database
    ORDER BY pg_database_size(pg_database.datname) DESC;
    
    -- Table sizes
    SELECT relname,
           pg_size_pretty(pg_total_relation_size(relid)) AS total_size
    FROM pg_stat_user_tables
    ORDER BY pg_total_relation_size(relid) DESC
    LIMIT 10;

    ⚡ pg_blocking_pids() is the fastest way to trace a lock chain

    💡 pg_size_pretty + pg_total_relation_size shows total size including indexes and TOAST

    Related Articles

    Background reading and deeper explanations for this sheet.

    Intro to FastAPI: Build a Production-Ready Python API with Real Examples

    FastAPI is one of the fastest ways to build modern APIs in Python without sacrificing code quality or developer experience. In this practical, beginner-friendly guide, you’ll learn how to create endpoints, validate data, handle errors, connect a database, secure routes, and prepare your FastAPI app for real-world deployment.

    keyword overlap

    Create a Generative Email Triage App with FastAPI, Postgres, LangGraph, and Azure Foundry

    Learn how to build a production-ready generative email triage app using FastAPI, Postgres, LangGraph, and Azure Foundry. This guide walks you through architecture, data modeling, workflow orchestration, and practical implementation details with real-world examples. By the end, you’ll have a beginner-friendly but deep blueprint you can run, extend, and deploy.

    keyword overlap

    FastAPI Interview Questions: Practical Cheat Sheet for Developers

    Preparing for FastAPI interviews can feel overwhelming if you only memorize definitions. This practical cheat sheet helps you answer common FastAPI interview questions with confidence using real-world examples, production-ready patterns, and beginner-friendly explanations with depth.

    keyword overlap

    How to Create an Agent Using LangGraph: A Practical Developer Cheat Sheet

    Want to build reliable AI agents instead of brittle prompt chains? This practical guide shows you how to create an agent using LangGraph step by step, from architecture and state design to tools, memory, and production hardening. You’ll get beginner-friendly explanations, real-world examples, and runnable code patterns you can adapt immediately.

    keyword overlap