📖 Guide
PostgreSQL Reference
Comprehensive PostgreSQL reference — from connection basics to advanced features like JSONB, full-text search, window functions, and performance tuning.
167 commands across 14 categories
Connection & DatabasesData TypesTable ManagementCRUD OperationsIndexesJSON/JSONB OperationsArray OperationsWindow FunctionsCTEs & Recursive QueriesFull-Text SearchRoles & PermissionsExtensionsPerformanceBackup & Restore
Connection & Databases
| Command | Description |
|---|---|
psql -h host -p port -U user -d dbnamee.g. psql -h localhost -p 5432 -U postgres -d myapp | Connect to a PostgreSQL database with psql |
psql postgres://user:pass@host:port/dbe.g. psql postgres://admin:secret@db.example.com:5432/production | Connect using a connection URI |
createdb dbnamee.g. createdb -U postgres myapp | Create a new database from the command line |
dropdb dbnamee.g. dropdb -U postgres old_database | Drop (delete) a database from the command line |
\l | List all databases |
\c dbnamee.g. \c myapp | Connect to / switch to a different database |
\dt | List all tables in the current schema |
\dt+ | List all tables with size and description |
\d tablenamee.g. \d users | Describe a table — columns, types, indexes, constraints |
\dn | List all schemas |
\df | List all functions |
\di | List all indexes |
\x | Toggle expanded display (vertical output for wide rows) |
\timing | Toggle query execution timing display |
\i filename.sqle.g. \i /path/to/migration.sql | Execute SQL commands from a file |
Data Types
| Command | Description |
|---|---|
SERIAL / BIGSERIALe.g. id SERIAL PRIMARY KEY | Auto-incrementing integer (4-byte / 8-byte); shorthand for sequence + DEFAULT |
INTEGER / BIGINT / SMALLINTe.g. age INTEGER NOT NULL | Integer types: 4-byte (±2B), 8-byte (±9.2E18), 2-byte (±32K) |
NUMERIC(precision, scale)e.g. price NUMERIC(10, 2) | Exact decimal — use for money/financial data (no rounding errors) |
TEXT / VARCHAR(n)e.g. name TEXT NOT NULL | Variable-length string; TEXT has no limit, VARCHAR(n) enforces max length |
BOOLEANe.g. is_active BOOLEAN DEFAULT true | true/false/null — accepts TRUE, FALSE, 't', 'f', 1, 0 |
TIMESTAMP / TIMESTAMPTZe.g. created_at TIMESTAMPTZ DEFAULT NOW() | Date + time; TIMESTAMPTZ stores in UTC and converts to session timezone |
DATE / TIME / INTERVALe.g. birth_date DATE
login_time TIME
retention INTERVAL '30 days' | Date only, time only, or a time duration |
UUIDe.g. id UUID DEFAULT gen_random_uuid() PRIMARY KEY | 128-bit universally unique identifier |
JSONBe.g. metadata JSONB DEFAULT '{}'::jsonb | Binary JSON — indexed, supports operators; preferred over JSON for most uses |
JSONe.g. raw_payload JSON | Text JSON — preserves whitespace/key order, no indexing. Use JSONB instead in most cases |
ARRAYe.g. tags TEXT[] DEFAULT '{}' | Array of any type — use TYPE[] syntax |
ENUMe.g. CREATE TYPE status AS ENUM ('active', 'inactive', 'banned');
user_status status DEFAULT 'active' | Custom enumerated type — create with CREATE TYPE |
BYTEAe.g. avatar BYTEA | Binary data (byte array) — for small blobs; use large objects for big files |
INET / CIDR / MACADDRe.g. client_ip INET | Network address types with built-in operators |
TSVECTOR / TSQUERYe.g. search_vector TSVECTOR | Full-text search types — preprocessed document / search query |
Table Management
| Command | Description |
|---|---|
CREATE TABLE name ( ... );e.g. CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
); | Create a new table with column definitions and constraints |
CREATE TABLE IF NOT EXISTS name ( ... ); | Create a table only if it doesn't already exist |
CREATE TABLE ... (LIKE source INCLUDING ALL);e.g. CREATE TABLE users_backup (LIKE users INCLUDING ALL); | Create a table with the same structure as another table |
ALTER TABLE name ADD COLUMN col type;e.g. ALTER TABLE users ADD COLUMN phone TEXT; | Add a new column to an existing table |
ALTER TABLE name DROP COLUMN col;e.g. ALTER TABLE users DROP COLUMN phone; | Remove a column from a table |
ALTER TABLE name ALTER COLUMN col TYPE newtype;e.g. ALTER TABLE users ALTER COLUMN age TYPE SMALLINT; | Change a column's data type |
ALTER TABLE name RENAME COLUMN old TO new;e.g. ALTER TABLE users RENAME COLUMN name TO full_name; | Rename a column |
ALTER TABLE name ADD CONSTRAINT ... ;e.g. ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id); | Add a constraint to an existing table |
DROP TABLE name;e.g. DROP TABLE IF EXISTS temp_data CASCADE; | Delete a table and all its data permanently |
TRUNCATE TABLE name;e.g. TRUNCATE TABLE logs RESTART IDENTITY CASCADE; | Delete all rows quickly (faster than DELETE, resets sequences with RESTART IDENTITY) |
PRIMARY KEYe.g. id BIGSERIAL PRIMARY KEY | Unique, non-null constraint identifying each row |
UNIQUEe.g. email TEXT UNIQUE NOT NULL | Ensure all values in a column (or combo) are unique |
NOT NULL | Column cannot contain NULL values |
DEFAULT valuee.g. created_at TIMESTAMPTZ DEFAULT NOW() | Set a default value for inserts that omit the column |
CHECK (condition)e.g. age INTEGER CHECK (age >= 0 AND age <= 150) | Enforce a boolean condition on column values |
FOREIGN KEY ... REFERENCES ...e.g. user_id BIGINT REFERENCES users(id) ON DELETE CASCADE | Enforce referential integrity between tables |
CRUD Operations
| Command | Description |
|---|---|
INSERT INTO table (cols) VALUES (vals);e.g. INSERT INTO users (email, name) VALUES ('user@example.com', 'Alice'); | Insert a single row |
INSERT INTO ... VALUES ... RETURNING *;e.g. INSERT INTO users (email, name) VALUES ('bob@test.com', 'Bob') RETURNING id, created_at; | Insert and return the created row(s) |
INSERT INTO ... SELECT ...;e.g. INSERT INTO users_archive SELECT * FROM users WHERE created_at < '2024-01-01'; | Insert rows from a query result |
INSERT INTO ... ON CONFLICT ... DO UPDATE SET ...;e.g. INSERT INTO users (email, name) VALUES ('user@test.com', 'New Name')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name; | Upsert — insert or update on conflict (unique/pk violation) |
INSERT INTO ... ON CONFLICT ... DO NOTHING;e.g. INSERT INTO tags (name) VALUES ('rust') ON CONFLICT (name) DO NOTHING; | Skip insert if a conflict occurs (idempotent insert) |
SELECT cols FROM table WHERE condition;e.g. SELECT id, name, email FROM users WHERE is_active = true ORDER BY created_at DESC; | Query rows from a table |
SELECT DISTINCT ON (col) ...e.g. SELECT DISTINCT ON (user_id) * FROM orders ORDER BY user_id, created_at DESC; | Return one row per distinct value of the specified column(s) |
UPDATE table SET col = val WHERE condition;e.g. UPDATE users SET name = 'Alice Smith' WHERE id = 1; | Update existing rows |
UPDATE ... FROM ... WHERE ...;e.g. UPDATE orders SET status = 'shipped'
FROM shipments
WHERE orders.id = shipments.order_id AND shipments.shipped_at IS NOT NULL; | Update with a join (PostgreSQL-specific syntax) |
UPDATE ... SET ... RETURNING *;e.g. UPDATE users SET is_active = false WHERE last_login < NOW() - INTERVAL '1 year' RETURNING id, email; | Update and return the modified rows |
DELETE FROM table WHERE condition;e.g. DELETE FROM sessions WHERE expires_at < NOW(); | Delete rows matching a condition |
DELETE FROM ... USING ... WHERE ...;e.g. DELETE FROM orders USING users WHERE orders.user_id = users.id AND users.is_banned = true; | Delete with a join |
DELETE FROM ... RETURNING *;e.g. DELETE FROM expired_tokens WHERE expires_at < NOW() RETURNING token_id; | Delete and return the deleted rows |
Indexes
| Command | Description |
|---|---|
CREATE INDEX name ON table (column);e.g. CREATE INDEX idx_users_email ON users (email); | Create a B-tree index (default) — good for equality and range queries |
CREATE UNIQUE INDEX name ON table (column);e.g. CREATE UNIQUE INDEX idx_users_email_unique ON users (email); | Create a unique index — enforces uniqueness and speeds up lookups |
CREATE INDEX ... USING GIN (column);e.g. CREATE INDEX idx_users_metadata ON users USING GIN (metadata); | GIN index — for JSONB, arrays, full-text search, trigram similarity |
CREATE INDEX ... USING GiST (column);e.g. CREATE INDEX idx_locations_point ON locations USING GiST (coordinates); | GiST index — for geometric/spatial data, ranges, full-text search |
CREATE INDEX ... USING BRIN (column);e.g. CREATE INDEX idx_logs_created ON logs USING BRIN (created_at); | BRIN index — very compact, for naturally-ordered data (timestamps, serial IDs) |
CREATE INDEX ... ON table (col) WHERE condition;e.g. CREATE INDEX idx_active_users ON users (email) WHERE is_active = true; | Partial index — index only rows matching a condition (smaller, faster) |
CREATE INDEX ... ON table (expression);e.g. CREATE INDEX idx_users_lower_email ON users (LOWER(email)); | Expression index — index on a computed value |
CREATE INDEX ... ON table (col1, col2);e.g. CREATE INDEX idx_orders_user_status ON orders (user_id, status); | Composite index — for queries filtering on multiple columns |
CREATE INDEX CONCURRENTLY ...;e.g. CREATE INDEX CONCURRENTLY idx_orders_date ON orders (created_at); | Build index without locking the table for writes (slower but non-blocking) |
DROP INDEX name;e.g. DROP INDEX IF EXISTS idx_users_email; | Remove an index |
REINDEX INDEX name;e.g. REINDEX INDEX CONCURRENTLY idx_users_email; | Rebuild a corrupted or bloated index |
JSON/JSONB Operations
| Command | Description |
|---|---|
column -> 'key'e.g. SELECT metadata -> 'address' FROM users; | Get JSON object field by key (returns JSON) |
column ->> 'key'e.g. SELECT metadata ->> 'city' FROM users WHERE metadata ->> 'city' = 'Vienna'; | Get JSON object field as TEXT (for comparison/casting) |
column -> indexe.g. SELECT tags -> 0 FROM posts; | Get JSON array element by index (0-based) |
column #> '{path,to,key}'e.g. SELECT metadata #> '{address,city}' FROM users; | Get nested JSON value by path (returns JSON) |
column #>> '{path,to,key}'e.g. SELECT metadata #>> '{address,zip}' FROM users; | Get nested JSON value by path as TEXT |
column @> '{"key": "value"}'e.g. SELECT * FROM users WHERE metadata @> '{"role": "admin"}'; | Contains — does the JSONB contain this structure? (GIN-indexable) |
column <@ '{...}'e.g. SELECT * FROM users WHERE metadata <@ '{"role": "admin", "level": 5}'; | Contained by — is the JSONB contained within this value? |
column ? 'key'e.g. SELECT * FROM users WHERE metadata ? 'phone'; | Does the JSONB contain this top-level key? |
column ?| array['k1','k2']e.g. SELECT * FROM users WHERE metadata ?| array['phone', 'mobile']; | Does the JSONB contain ANY of these keys? |
column ?& array['k1','k2']e.g. SELECT * FROM users WHERE metadata ?& array['email', 'phone']; | Does the JSONB contain ALL of these keys? |
jsonb_set(target, path, new_value)e.g. UPDATE users SET metadata = jsonb_set(metadata, '{address,city}', '"Berlin"'); | Set a value at a path in JSONB (returns new JSONB) |
column || '{"key": "value"}'::jsonbe.g. UPDATE users SET metadata = metadata || '{"verified": true}'::jsonb; | Merge/concatenate JSONB objects (right side wins on conflicts) |
column - 'key'e.g. UPDATE users SET metadata = metadata - 'temp_field'; | Remove a key from JSONB |
jsonb_agg(expression)e.g. SELECT user_id, jsonb_agg(tag) AS tags FROM user_tags GROUP BY user_id; | Aggregate values into a JSONB array |
jsonb_object_agg(key, value)e.g. SELECT jsonb_object_agg(key, value) FROM settings; | Aggregate key-value pairs into a JSONB object |
jsonb_each(column) / jsonb_each_text(column)e.g. SELECT key, value FROM users, jsonb_each(metadata) WHERE id = 1; | Expand JSONB object into key-value rows |
jsonb_array_elements(column)e.g. SELECT jsonb_array_elements(tags) AS tag FROM posts WHERE id = 1; | Expand JSONB array into rows |
jsonb_typeof(column)e.g. SELECT jsonb_typeof(metadata -> 'score') FROM users; | Return the type of a JSONB value (object, array, string, number, boolean, null) |
Array Operations
| Command | Description |
|---|---|
ARRAY['a','b','c'] or '{a,b,c}'e.g. SELECT ARRAY['postgres', 'mysql', 'sqlite']; | Array literal syntax |
column[index]e.g. SELECT tags[1] FROM posts; | Access array element (1-based in PostgreSQL!) |
column[start:end]e.g. SELECT tags[1:3] FROM posts; | Array slice (inclusive on both ends) |
array_length(column, dimension)e.g. SELECT array_length(tags, 1) FROM posts; | Get the length of an array |
ANY(array)e.g. SELECT * FROM posts WHERE 'rust' = ANY(tags); | Check if a value matches any array element |
ALL(array)e.g. SELECT * FROM scores WHERE 80 <= ALL(grades); | Check if a condition is true for all array elements |
array_agg(expression)e.g. SELECT user_id, array_agg(tag ORDER BY tag) FROM user_tags GROUP BY user_id; | Aggregate values from multiple rows into an array |
unnest(array)e.g. SELECT unnest(tags) AS tag FROM posts WHERE id = 1; | Expand an array into individual rows |
column @> ARRAY['value']e.g. SELECT * FROM posts WHERE tags @> ARRAY['rust', 'async']; | Array contains — does the array contain all these elements? |
column && ARRAY['value']e.g. SELECT * FROM posts WHERE tags && ARRAY['rust', 'go', 'python']; | Array overlap — do the arrays share any elements? |
array_cat(a, b)e.g. SELECT array_cat(ARRAY[1,2], ARRAY[3,4]); -- {1,2,3,4} | Concatenate two arrays |
array_append(array, element)e.g. UPDATE posts SET tags = array_append(tags, 'new-tag') WHERE id = 1; | Append an element to an array |
array_remove(array, element)e.g. UPDATE posts SET tags = array_remove(tags, 'deprecated') WHERE id = 1; | Remove all occurrences of an element from an array |
Window Functions
| Command | Description |
|---|---|
ROW_NUMBER() OVER (...)e.g. SELECT name, department, salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rank
FROM employees; | Assign a unique sequential number to each row within a partition |
RANK() OVER (...)e.g. SELECT name, score, RANK() OVER (ORDER BY score DESC) FROM students; | Like ROW_NUMBER but same-valued rows get the same rank (with gaps) |
DENSE_RANK() OVER (...)e.g. SELECT name, score, DENSE_RANK() OVER (ORDER BY score DESC) FROM students; | Like RANK but without gaps in ranking |
LAG(col, offset, default) OVER (...)e.g. SELECT date, revenue,
revenue - LAG(revenue) OVER (ORDER BY date) AS daily_change
FROM daily_sales; | Access a previous row's value (default offset = 1) |
LEAD(col, offset, default) OVER (...)e.g. SELECT date, temperature,
LEAD(temperature) OVER (ORDER BY date) AS next_day_temp
FROM weather; | Access a following row's value |
SUM(col) OVER (...)e.g. SELECT date, amount,
SUM(amount) OVER (ORDER BY date) AS running_total
FROM transactions; | Running/cumulative sum within a window |
AVG(col) OVER (... ROWS BETWEEN ...)e.g. SELECT date, price,
AVG(price) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7d
FROM stock_prices; | Moving average with a specified window frame |
FIRST_VALUE(col) OVER (...)e.g. SELECT name, department, salary,
FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary DESC) AS top_earner
FROM employees; | Get the first value in the window frame |
NTILE(n) OVER (...)e.g. SELECT name, salary,
NTILE(4) OVER (ORDER BY salary) AS quartile
FROM employees; | Divide rows into n roughly equal groups (buckets) |
PERCENT_RANK() OVER (...)e.g. SELECT name, salary,
PERCENT_RANK() OVER (ORDER BY salary) AS percentile
FROM employees; | Relative rank as a percentage (0 to 1) |
CTEs & Recursive Queries
| Command | Description |
|---|---|
WITH cte_name AS ( ... ) SELECT ...;e.g. WITH active_users AS (
SELECT * FROM users WHERE is_active = true
)
SELECT * FROM active_users WHERE created_at > '2024-01-01'; | Common Table Expression — named temporary result set for readability |
WITH cte1 AS (...), cte2 AS (...) SELECT ...;e.g. WITH user_orders AS (
SELECT user_id, COUNT(*) AS order_count FROM orders GROUP BY user_id
),
top_users AS (
SELECT * FROM user_orders WHERE order_count > 10
)
SELECT u.name, t.order_count FROM users u JOIN top_users t ON u.id = t.user_id; | Chain multiple CTEs — each can reference the previous ones |
WITH RECURSIVE cte AS ( base UNION ALL recursive ) ...e.g. WITH RECURSIVE org_tree AS (
SELECT id, name, manager_id, 1 AS depth
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, t.depth + 1
FROM employees e JOIN org_tree t ON e.manager_id = t.id
)
SELECT * FROM org_tree ORDER BY depth; | Recursive CTE — for tree/graph traversal, hierarchical data |
WITH ... INSERT/UPDATE/DELETEe.g. WITH deleted AS (
DELETE FROM sessions WHERE expires_at < NOW() RETURNING user_id
)
SELECT COUNT(DISTINCT user_id) AS affected_users FROM deleted; | Writable CTEs — combine data modifications with queries |
WITH RECURSIVE ... (cycle detection)e.g. WITH RECURSIVE graph AS (
SELECT id, parent_id, ARRAY[id] AS path
FROM nodes WHERE id = 1
UNION ALL
SELECT n.id, n.parent_id, g.path || n.id
FROM nodes n JOIN graph g ON n.parent_id = g.id
WHERE n.id != ALL(g.path) -- cycle detection
)
SELECT * FROM graph; | Use CYCLE clause (PG14+) or manual tracking to prevent infinite loops |
Recursive series generatione.g. WITH RECURSIVE dates AS (
SELECT DATE '2024-01-01' AS d
UNION ALL
SELECT d + 1 FROM dates WHERE d < '2024-12-31'
)
SELECT d FROM dates; | Generate a series of values using recursion |
Full-Text Search
| Command | Description |
|---|---|
to_tsvector('english', text)e.g. SELECT to_tsvector('english', 'The quick brown foxes jumped'); | Convert text to a tsvector (normalized, stemmed tokens with positions) |
to_tsquery('english', query)e.g. SELECT to_tsquery('english', 'quick & fox'); | Parse a search query into tsquery (supports & | ! <-> operators) |
plainto_tsquery('english', text)e.g. SELECT plainto_tsquery('english', 'quick brown fox'); | Convert plain text to tsquery (no special syntax needed, uses AND) |
websearch_to_tsquery('english', text)e.g. SELECT websearch_to_tsquery('english', '"quick fox" -lazy'); | Google-like search syntax (quotes for phrases, - for NOT, OR) |
tsvector @@ tsquerye.g. SELECT * FROM articles WHERE to_tsvector('english', body) @@ to_tsquery('english', 'database & performance'); | Match operator — does the document match the query? |
ts_rank(tsvector, tsquery)e.g. SELECT title, ts_rank(search_vector, query) AS rank
FROM articles, to_tsquery('english', 'postgres') AS query
WHERE search_vector @@ query
ORDER BY rank DESC; | Rank search results by relevance (higher = more relevant) |
ts_headline(config, text, tsquery)e.g. SELECT ts_headline('english', body, to_tsquery('english', 'database'),
'StartSel=<b>, StopSel=</b>, MaxWords=50')
FROM articles; | Generate a text snippet with highlighted search terms |
Stored tsvector column + GIN indexe.g. ALTER TABLE articles ADD COLUMN search_vector TSVECTOR;
UPDATE articles SET search_vector = to_tsvector('english', title || ' ' || body);
CREATE INDEX idx_articles_search ON articles USING GIN (search_vector); | Best practice: store pre-computed tsvector and index it for fast search |
Trigger to auto-update tsvectore.g. CREATE TRIGGER articles_search_update BEFORE INSERT OR UPDATE
ON articles FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(search_vector, 'pg_catalog.english', title, body); | Keep the search vector in sync with source columns |
Roles & Permissions
| Command | Description |
|---|---|
CREATE ROLE name WITH LOGIN PASSWORD 'pass';e.g. CREATE ROLE app_user WITH LOGIN PASSWORD 'secret123'; | Create a new role (user) that can log in |
CREATE ROLE name;e.g. CREATE ROLE readonly; | Create a group role (cannot log in, used for grouping permissions) |
GRANT role TO user;e.g. GRANT readonly TO app_user; | Add a user to a group role |
GRANT SELECT ON table TO role;e.g. GRANT SELECT, INSERT, UPDATE ON users TO app_user; | Grant specific privileges on a table |
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO role;e.g. GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO admin_role; | Grant all privileges on all tables in a schema |
REVOKE privilege ON table FROM role;e.g. REVOKE DELETE ON users FROM app_user; | Remove privileges from a role |
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO role;e.g. ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly; | Set default permissions for future tables |
\du | List all roles and their attributes in psql |
Row-Level Security (RLS)e.g. ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_docs ON documents
FOR ALL TO app_user
USING (owner_id = current_setting('app.user_id')::int); | Restrict which rows a role can access |
Extensions
| Command | Description |
|---|---|
CREATE EXTENSION IF NOT EXISTS name;e.g. CREATE EXTENSION IF NOT EXISTS pg_trgm; | Install a PostgreSQL extension |
\dx | List installed extensions in psql |
pg_trgme.g. CREATE EXTENSION pg_trgm;
CREATE INDEX idx_users_name_trgm ON users USING GIN (name gin_trgm_ops);
SELECT * FROM users WHERE name % 'Jonh'; -- fuzzy match | Trigram similarity for fuzzy text matching (LIKE, ILIKE, similarity()) |
uuid-ossp / gen_random_uuid()e.g. SELECT gen_random_uuid(); -- built-in, no extension needed in PG13+ | Generate UUIDs; gen_random_uuid() is built-in since PG13 |
postgise.g. CREATE EXTENSION postgis;
SELECT ST_Distance(
ST_MakePoint(16.3738, 48.2082)::geography, -- Vienna
ST_MakePoint(13.4050, 52.5200)::geography -- Berlin
) / 1000 AS km; | Geospatial extension — geometry/geography types, spatial queries |
pg_stat_statementse.g. CREATE EXTENSION pg_stat_statements;
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20; | Track query execution statistics — find slow/frequent queries |
citexte.g. CREATE EXTENSION citext;
CREATE TABLE users (email CITEXT UNIQUE); | Case-insensitive text type — avoids LOWER() everywhere |
Performance
| Command | Description |
|---|---|
EXPLAIN query;e.g. EXPLAIN SELECT * FROM users WHERE email = 'test@test.com'; | Show the query execution plan (estimated cost, no execution) |
EXPLAIN ANALYZE query;e.g. EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@test.com'; | Execute the query and show actual timing + row counts |
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) query;e.g. EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM orders WHERE user_id = 42; | Detailed plan with buffer usage in JSON format (paste into explain.dalibo.com) |
Seq Scan vs Index Scan | Seq Scan reads entire table; Index Scan uses an index. Seq Scan on large tables = problem |
VACUUM table;e.g. VACUUM VERBOSE users; | Reclaim storage from dead tuples (happens automatically but can run manually) |
VACUUM FULL table;e.g. VACUUM FULL users; | Rewrite entire table to reclaim space (locks table, use sparingly) |
ANALYZE table;e.g. ANALYZE users; | Update table statistics used by the query planner |
VACUUM ANALYZE table;e.g. VACUUM ANALYZE orders; | Vacuum and analyze in one command |
pg_stat_user_tablese.g. SELECT relname, seq_scan, idx_scan, n_live_tup, n_dead_tup, last_vacuum, last_analyze
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC; | Table-level stats: seq scans, index scans, dead tuples, last vacuum/analyze |
pg_stat_user_indexese.g. SELECT indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes
WHERE idx_scan = 0 ORDER BY pg_relation_size(indexrelid) DESC; | Index usage stats — find unused indexes |
pg_size_pretty(pg_total_relation_size('table'))e.g. SELECT pg_size_pretty(pg_total_relation_size('users')); | Get table size including indexes and TOAST data |
pg_stat_activitye.g. SELECT pid, state, query, query_start, NOW() - query_start AS duration
FROM pg_stat_activity
WHERE state = 'active' ORDER BY duration DESC; | View currently running queries and connections |
pg_cancel_backend(pid) / pg_terminate_backend(pid)e.g. SELECT pg_cancel_backend(12345); -- graceful
SELECT pg_terminate_backend(12345); -- force kill | Cancel a running query or terminate a connection |
Backup & Restore
| Command | Description |
|---|---|
pg_dump dbname > file.sqle.g. pg_dump -U postgres myapp > myapp_backup.sql | Dump a database to a SQL file (plain text) |
pg_dump -Fc dbname > file.dumpe.g. pg_dump -U postgres -Fc myapp > myapp_backup.dump | Custom format dump — compressed, supports selective restore |
pg_dump -Fd dbname -f dir/e.g. pg_dump -U postgres -Fd -j 4 myapp -f myapp_backup/ | Directory format — parallel dump, one file per table |
pg_dump -t tablename dbnamee.g. pg_dump -U postgres -t users -t orders myapp > tables_backup.sql | Dump only specific table(s) |
pg_dump --schema-only dbnamee.g. pg_dump -U postgres --schema-only myapp > schema.sql | Dump only the schema (DDL), no data |
pg_dump --data-only dbnamee.g. pg_dump -U postgres --data-only myapp > data.sql | Dump only the data, no schema |
pg_dumpall > file.sqle.g. pg_dumpall -U postgres > full_backup.sql | Dump all databases including roles and tablespaces |
psql dbname < file.sqle.g. psql -U postgres myapp < myapp_backup.sql | Restore from a plain SQL dump |
pg_restore -d dbname file.dumpe.g. pg_restore -U postgres -d myapp -j 4 myapp_backup.dump | Restore from custom/directory format dump |
pg_restore --clean -d dbname file.dumpe.g. pg_restore -U postgres --clean --if-exists -d myapp myapp_backup.dump | Drop objects before recreating them during restore |
pg_restore -l file.dumpe.g. pg_restore -l myapp_backup.dump | grep users | List the contents of a custom format dump (for selective restore) |
COPY table TO/FROM filee.g. COPY users TO '/tmp/users.csv' WITH (FORMAT csv, HEADER);
COPY users FROM '/tmp/users.csv' WITH (FORMAT csv, HEADER); | High-performance bulk data export/import (CSV, binary) |
📖 Free, searchable command reference. Bookmark this page for quick access.