PostgreSQL Reference
Production-grade relational database with rich types, JSONB, and advanced indexing.
Connection and CLI
Connect with psql
# Connect to local database
psql -d myapp
# Connect to remote host
psql -h db.example.com -U appuser -d myapp
# Connection string (libpq format)
psql "postgresql://appuser:pass@db.example.com:5432/myapp?sslmode=require"
psql meta-commands
psql -d myapp <<'SQL'
\l -- list databases
\dt -- list tables in current schema
\dt+ -- list tables with sizes
\d devices -- describe table (columns, indexes, constraints)
\di -- list indexes
\dv -- list views
\dn -- list schemas
\du -- list roles
\conninfo -- current connection info
\x -- toggle expanded display (one column per line)
\timing on -- show query execution time
SQL
Execute SQL from file or command line
psql -d myapp -f schema.sql
psql -d myapp -c "SELECT COUNT(*) FROM users;"
# CSV output
psql -d myapp -c "COPY (SELECT * FROM devices) TO STDOUT WITH CSV HEADER" > devices.csv
Database and Role Management
Create database and user
sudo -u postgres psql <<'SQL'
CREATE USER appuser WITH PASSWORD 'changeme';
CREATE DATABASE myapp OWNER appuser;
GRANT ALL PRIVILEGES ON DATABASE myapp TO appuser;
SQL
Grant granular permissions
-- Read-only role
CREATE ROLE readonly;
GRANT CONNECT ON DATABASE myapp TO readonly;
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;
-- Assign role to a user
GRANT readonly TO monitoring_user;
Data Types
PostgreSQL-specific types worth knowing
CREATE TABLE network_inventory (
id SERIAL PRIMARY KEY, -- auto-increment integer
device_id UUID DEFAULT gen_random_uuid(),
hostname VARCHAR(255) NOT NULL,
mgmt_ip INET, -- IP address with validation
mac_addr MACADDR, -- MAC address
subnet CIDR, -- network/prefix
config JSONB DEFAULT '{}', -- binary JSON (indexable)
tags TEXT[] DEFAULT '{}', -- array of strings
metadata HSTORE, -- key-value pairs
created_at TIMESTAMPTZ DEFAULT NOW() -- timestamp with timezone
);
Prefer TIMESTAMPTZ over TIMESTAMP. Prefer JSONB over JSON (supports indexing and equality checks). Prefer TEXT over VARCHAR unless you need a length constraint.
JSONB Operations
Query and manipulate JSON data
-- Extract text value
SELECT config->>'hostname' AS hostname FROM devices;
-- Extract nested value
SELECT config->'network'->>'vlan' FROM devices;
-- Filter on JSON field
SELECT * FROM devices WHERE config->>'status' = 'active';
-- Check key existence
SELECT * FROM devices WHERE config ? 'hostname';
-- Update a JSON field (merge)
UPDATE devices
SET config = config || '{"vlan": 20}'::jsonb
WHERE id = 1;
-- Remove a key
UPDATE devices
SET config = config - 'deprecated_field';
-- Index for fast JSON queries
CREATE INDEX idx_devices_config ON devices USING GIN (config);
Array Operations
Working with PostgreSQL arrays
-- Insert with array
INSERT INTO servers (hostname, tags) VALUES ('web-01', ARRAY['production', 'web']);
-- Query: contains element
SELECT * FROM servers WHERE 'production' = ANY(tags);
-- Query: contains all elements
SELECT * FROM servers WHERE tags @> ARRAY['web', 'production'];
-- Append to array
UPDATE servers SET tags = array_append(tags, 'monitored') WHERE hostname = 'web-01';
-- Unnest array to rows
SELECT hostname, unnest(tags) AS tag FROM servers;
Backup and Restore
pg_dump — logical backup
# Dump single database (custom format, compressed)
pg_dump -Fc myapp > myapp_$(date +%F).dump
# Dump as plain SQL
pg_dump myapp > myapp_$(date +%F).sql
# Dump schema only (no data)
pg_dump --schema-only myapp > schema.sql
# Dump specific table
pg_dump -t devices myapp > devices.sql
pg_restore — restore from custom format
# Restore to existing database
pg_restore -d myapp myapp_2026-04-10.dump
# Restore creating the database
pg_restore -C -d postgres myapp_2026-04-10.dump
pg_dumpall — all databases and roles
pg_dumpall > full_cluster_$(date +%F).sql
Performance and Analysis
EXPLAIN ANALYZE — actual execution metrics
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE customer_id = 42;
Key things in EXPLAIN output:
-
Seq Scan— full table scan. Consider adding an index. -
Index Scan/Index Only Scan— good, using an index. -
actual time— real wall-clock time in milliseconds. -
rows— actual rows vs estimatedrows. Large discrepancy means stale statistics.
Refresh statistics after bulk operations
ANALYZE devices; -- update statistics for one table
ANALYZE; -- update statistics for all tables
Find slow queries with pg_stat_statements
-- Enable extension (once)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Top 10 slowest queries by total time
SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
Table Size and Bloat
Check table and index sizes
-- Table sizes including indexes
SELECT relname AS table_name,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
pg_size_pretty(pg_relation_size(relid)) AS table_size,
pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) AS index_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC;
Database size
SELECT pg_size_pretty(pg_database_size('myapp'));
Useful System Queries
Active connections
SELECT pid, usename, datname, state, query_start, query
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY query_start;
Kill a stuck query
SELECT pg_cancel_backend(12345); -- graceful (SIGINT)
SELECT pg_terminate_backend(12345); -- forceful (SIGTERM)
List all indexes on a table
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'devices';