PostgreSQL Cheat Sheet
Connection
Section titled “Connection”# Connectpsql -h localhost -U username -d databasepsql "postgresql://user:pass@host:5432/dbname"
# Common flagspsql -c "SQL" # Execute single commandpsql -f file.sql # Execute filepsql -l # List databasespsql Commands
Section titled “psql Commands”Navigation
Section titled “Navigation”| Command | Description |
|---|---|
\l | List databases |
\c dbname | Connect to database |
\dt | List tables |
\dt+ | List tables with sizes |
\d tablename | Describe table |
\d+ tablename | Describe table with storage |
\dn | List schemas |
\di | List indexes |
\dv | List views |
\df | List functions |
\du | List roles |
\dp | List table privileges |
\ds | List sequences |
\dT | List data types |
Execution and Output
Section titled “Execution and Output”| Command | Description |
|---|---|
\x | Toggle expanded display |
\timing | Toggle query timing |
\e | Edit query in $EDITOR |
\i file.sql | Execute file |
\o file.txt | Send output to file |
\copy | Client-side COPY (no superuser) |
\! cmd | Execute shell command |
\set VAR value | Set psql variable |
\echo text | Print text |
\pset format html | Change output format |
\q | Quit |
Information
Section titled “Information”| Command | Description |
|---|---|
\? | Help on psql commands |
\h | Help on SQL commands |
\h ALTER | Help on ALTER statement |
\conninfo | Current connection info |
\encoding | Show client encoding |
Indexes
Section titled “Indexes”-- B-tree (default, most common)CREATE INDEX idx_users_email ON users (email);
-- Unique indexCREATE UNIQUE INDEX idx_users_email ON users (email);
-- Composite index (column order matters)CREATE INDEX idx_orders_user_date ON orders (user_id, created_at);
-- Partial index (only index rows matching WHERE)CREATE INDEX idx_active_users ON users (email) WHERE active = true;
-- Expression indexCREATE INDEX idx_users_lower_email ON users (lower(email));
-- GIN index (arrays, JSONB, full-text)CREATE INDEX idx_tags ON posts USING GIN (tags);CREATE INDEX idx_data ON events USING GIN (metadata jsonb_path_ops);
-- GiST index (geometric, range, full-text)CREATE INDEX idx_location ON places USING GIST (coordinates);
-- BRIN index (large tables with natural ordering)CREATE INDEX idx_created ON events USING BRIN (created_at);
-- Concurrent creation (no table lock)CREATE INDEX CONCURRENTLY idx_name ON table (column);
-- DropDROP INDEX idx_name;DROP INDEX CONCURRENTLY idx_name;
-- RebuildREINDEX INDEX idx_name;REINDEX TABLE tablename;Index Analysis
Section titled “Index Analysis”-- Check index usageSELECT schemaname, tablename, indexname, idx_scan, idx_tup_readFROM pg_stat_user_indexesORDER BY idx_scan;
-- Find unused indexes (zero scans)SELECT indexrelid::regclass AS index, relid::regclass AS table, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS sizeFROM pg_stat_user_indexesWHERE idx_scan = 0ORDER BY pg_relation_size(indexrelid) DESC;
-- Check if query uses an indexEXPLAIN ANALYZE SELECT * FROM users WHERE email = 'a@b.com';Window Functions
Section titled “Window Functions”-- Row numberSELECT name, department, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rankFROM employees;
-- Rank (ties get same rank, gaps after)SELECT name, salary, RANK() OVER (ORDER BY salary DESC) AS rankFROM employees;
-- Dense rank (no gaps)SELECT name, salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rankFROM employees;
-- Running totalSELECT date, amount, SUM(amount) OVER (ORDER BY date) AS running_totalFROM transactions;
-- Moving averageSELECT date, amount, AVG(amount) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avgFROM daily_sales;
-- Lead and lag (look ahead/behind)SELECT date, amount, LAG(amount) OVER (ORDER BY date) AS prev_amount, LEAD(amount) OVER (ORDER BY date) AS next_amountFROM transactions;
-- First/last in partitionSELECT department, name, salary, FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC) AS top_earner, LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS lowest_earnerFROM employees;
-- Ntile (divide into buckets)SELECT name, salary, NTILE(4) OVER (ORDER BY salary) AS quartileFROM employees;JSON and JSONB
Section titled “JSON and JSONB”-- Access field (returns JSON)SELECT data->'name' FROM events;
-- Access field (returns text)SELECT data->>'name' FROM events;
-- Nested accessSELECT data->'address'->>'city' FROM users;
-- Array elementSELECT data->0 FROM events;
-- Path accessSELECT data #> '{address,city}' FROM users;SELECT data #>> '{address,city}' FROM users; -- as text
-- ContainsSELECT * FROM events WHERE data @> '{"type": "click"}';
-- Key existsSELECT * FROM events WHERE data ? 'email';
-- Any key existsSELECT * FROM events WHERE data ?| array['email', 'phone'];
-- All keys existSELECT * FROM events WHERE data ?& array['email', 'phone'];
-- Build JSONSELECT jsonb_build_object('name', name, 'age', age) FROM users;
-- Aggregate to JSON arraySELECT jsonb_agg(name) FROM users;
-- Aggregate to JSON objectSELECT jsonb_object_agg(id, name) FROM users;
-- Expand JSON object to rowsSELECT * FROM jsonb_each('{"a": 1, "b": 2}');SELECT * FROM jsonb_each_text('{"a": 1, "b": 2}');
-- Update JSONB fieldUPDATE users SET data = jsonb_set(data, '{address,city}', '"NYC"');
-- Remove keyUPDATE users SET data = data - 'temporary_field';Admin Queries
Section titled “Admin Queries”Table and Database Size
Section titled “Table and Database Size”-- Database sizeSELECT pg_size_pretty(pg_database_size('mydb'));
-- Table sizes (with indexes)SELECT relname, pg_size_pretty(pg_total_relation_size(oid)) AS total, pg_size_pretty(pg_relation_size(oid)) AS table_only, pg_size_pretty(pg_indexes_size(oid)) AS indexesFROM pg_classWHERE relkind = 'r'ORDER BY pg_total_relation_size(oid) DESCLIMIT 20;
-- Schema sizeSELECT schemaname, pg_size_pretty(SUM(pg_total_relation_size(schemaname || '.' || tablename))) AS sizeFROM pg_tablesGROUP BY schemanameORDER BY SUM(pg_total_relation_size(schemaname || '.' || tablename)) DESC;Active Queries and Locks
Section titled “Active Queries and Locks”-- Running queriesSELECT pid, now() - pg_stat_activity.query_start AS duration, query, stateFROM pg_stat_activityWHERE state != 'idle'ORDER BY duration DESC;
-- Kill a querySELECT pg_cancel_backend(pid); -- GracefulSELECT pg_terminate_backend(pid); -- Force
-- Blocking locksSELECT blocked.pid AS blocked_pid, blocked.query AS blocked_query, blocking.pid AS blocking_pid, blocking.query AS blocking_queryFROM pg_stat_activity AS blockedJOIN pg_locks AS blocked_locks ON blocked.pid = blocked_locks.pidJOIN pg_locks AS blocking_locks ON blocked_locks.locktype = blocking_locks.locktype AND blocked_locks.relation = blocking_locks.relation AND blocked_locks.pid != blocking_locks.pidJOIN pg_stat_activity AS blocking ON blocking_locks.pid = blocking.pidWHERE NOT blocked_locks.granted;Statistics and Maintenance
Section titled “Statistics and Maintenance”-- Table statsSELECT relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum, last_analyzeFROM pg_stat_user_tablesORDER BY n_dead_tup DESC;
-- Cache hit ratio (should be > 99%)SELECT sum(heap_blks_read) AS heap_read, sum(heap_blks_hit) AS heap_hit, sum(heap_blks_hit) / GREATEST(sum(heap_blks_hit) + sum(heap_blks_read), 1) AS ratioFROM pg_statio_user_tables;
-- Index hit ratioSELECT relname, idx_blks_hit / GREATEST(idx_blks_hit + idx_blks_read, 1) AS hit_ratioFROM pg_statio_user_indexesORDER BY idx_blks_hit + idx_blks_read DESC;
-- Manual maintenanceVACUUM tablename; -- Reclaim dead tuplesVACUUM FULL tablename; -- Reclaim + compact (locks table)ANALYZE tablename; -- Update statisticsVACUUM ANALYZE tablename; -- BothRoles and Permissions
Section titled “Roles and Permissions”-- Create roleCREATE ROLE readonly LOGIN PASSWORD 'pass';
-- GrantGRANT CONNECT ON DATABASE mydb TO readonly;GRANT USAGE ON SCHEMA public TO readonly;GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
-- Default privileges (for future tables)ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;
-- RevokeREVOKE ALL ON DATABASE mydb FROM readonly;
-- Check permissionsSELECT grantee, privilege_typeFROM information_schema.role_table_grantsWHERE table_name = 'users';See Also
Section titled “See Also”- SQL — Portable SQL syntax: joins, CTEs, window functions
- Performance — Profiling and benchmarking, including database queries
- Docker — Running PostgreSQL in containers
- Kubernetes — Deploying PostgreSQL on k8s
- Data Models Lesson Plan — ER diagrams to model selection
- System Design Lesson Plan