PostgreSQL Cheat Sheet
A complete reference of PostgreSQL commands with examples and usage instructions. Find the command you need using the search bar or browse by category.
🐘 What is PostgreSQL?
PostgreSQL is a powerful, open-source object-relational database system with over 35 years of active development. Known for its reliability, feature robustness, and performance, PostgreSQL is the database of choice for many enterprises, startups, and government agencies worldwide.
📈 Key Features
- ACID Compliant: Full transaction support with serializable isolation
- Advanced Types: JSONB, arrays, UUID, hstore, range types
- Window Functions: ROW_NUMBER, RANK, LAG, LEAD, and more
- Full-Text Search: Built-in tsvector and tsquery support
- Extensible: Custom types, operators, functions, and extensions
🚀 PostgreSQL-Specific Features
- RETURNING: Get data back from INSERT/UPDATE/DELETE
- ILIKE: Case-insensitive pattern matching
- ON CONFLICT: Native upsert support
- JSONB: Binary JSON with indexing and operators
- EXPLAIN ANALYZE: Detailed query performance profiling
🌐 Who Uses PostgreSQL?
Tech Companies:
- Apple, Instagram, Spotify
- Reddit, Twitch, Discord
Cloud Platforms:
- AWS RDS, Aurora
- Supabase, Neon, Railway
Compatible Forks:
- CockroachDB, YugabyteDB
- TimescaleDB, Citus
SELECT
QueriesRetrieve rows from one or more tables
Syntax:
SELECT column1, column2 FROM table_name WHERE condition ORDER BY column LIMIT n;Examples:
SELECT * FROM users; Select all columns from usersSELECT name, email FROM users WHERE active = true; Filter rows by conditionSELECT * FROM users ORDER BY created_at DESC LIMIT 10; Latest 10 rowsSELECT DISTINCT country FROM users; Unique values onlyNotes:
PostgreSQL supports all standard SELECT clauses including DISTINCT ON, which returns the first row for each distinct value.
INSERT
QueriesAdd new rows to a table
Syntax:
INSERT INTO table_name (col1, col2) VALUES (val1, val2);Examples:
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'); Insert single rowINSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com'), ('Carol', 'carol@example.com'); Insert multiple rowsINSERT INTO users (name, email) VALUES ('Dave', 'dave@example.com') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name; Upsert with ON CONFLICTINSERT INTO users (name, email) VALUES ('Eve', 'eve@example.com') ON CONFLICT DO NOTHING; Skip insert on conflictNotes:
ON CONFLICT DO UPDATE (upsert) is PostgreSQL-specific. EXCLUDED refers to the row that was proposed for insertion.
UPDATE
QueriesModify existing rows in a table
Syntax:
UPDATE table_name SET col1 = val1 WHERE condition;Examples:
UPDATE users SET email = 'new@example.com' WHERE id = 1; Update one rowUPDATE products SET price = price * 1.1 WHERE category = 'electronics'; Increase price by 10%UPDATE users SET last_login = NOW() WHERE id = 42; Set timestamp to current timeNotes:
Always include a WHERE clause -- omitting it updates every row in the table.
DELETE
QueriesRemove rows from a table
Syntax:
DELETE FROM table_name WHERE condition;Examples:
DELETE FROM users WHERE id = 5; Delete a specific rowDELETE FROM logs WHERE created_at < NOW() - INTERVAL '30 days'; Delete rows older than 30 daysDELETE FROM users; Delete all rows (use with caution)Notes:
Use TRUNCATE for faster bulk deletion when you do not need row-level triggers to fire.
RETURNING
QueriesReturn data from rows affected by INSERT, UPDATE, or DELETE
Syntax:
INSERT INTO table_name (cols) VALUES (vals) RETURNING *;Examples:
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com') RETURNING id, created_at; Get auto-generated id after insertUPDATE users SET active = false WHERE last_login < NOW() - INTERVAL '1 year' RETURNING id, name; See which rows were updatedDELETE FROM sessions WHERE expires_at < NOW() RETURNING user_id; Return deleted rowsNotes:
RETURNING is PostgreSQL-specific. It avoids a separate SELECT after write operations.
WHERE
FilteringFilter rows based on a condition
Syntax:
SELECT * FROM table_name WHERE condition;Examples:
SELECT * FROM users WHERE active = true; Simple boolean filterSELECT * FROM orders WHERE status = 'shipped' AND total > 100; Multiple conditions with ANDSELECT * FROM users WHERE country = 'US' OR country = 'CA'; Either condition with ORNotes:
WHERE is evaluated before GROUP BY. Use HAVING to filter after aggregation.
IN / BETWEEN
FilteringFilter by a set of values or a range
Syntax:
SELECT * FROM table WHERE col IN (val1, val2);
SELECT * FROM table WHERE col BETWEEN low AND high;Examples:
SELECT * FROM users WHERE country IN ('US', 'CA', 'UK'); Match any value in the listSELECT * FROM orders WHERE created_at BETWEEN '2025-01-01' AND '2025-12-31'; Date range filterSELECT * FROM products WHERE price NOT BETWEEN 10 AND 50; Exclude a rangeNotes:
BETWEEN is inclusive of both endpoints. IN can also accept a subquery.
LIKE / ILIKE
FilteringPattern matching on text columns
Syntax:
SELECT * FROM table WHERE col LIKE 'pattern';
SELECT * FROM table WHERE col ILIKE 'pattern';Examples:
SELECT * FROM users WHERE name LIKE 'A%'; Names starting with A (case-sensitive)SELECT * FROM users WHERE email ILIKE '%@gmail.com'; Gmail addresses (case-insensitive)SELECT * FROM products WHERE name ILIKE '%phone%'; Contains phone (case-insensitive)Notes:
ILIKE is PostgreSQL-specific and performs case-insensitive matching. Use % for any characters, _ for a single character.
IS NULL / IS NOT NULL
FilteringCheck for NULL values
Syntax:
SELECT * FROM table WHERE col IS NULL;
SELECT * FROM table WHERE col IS NOT NULL;Examples:
SELECT * FROM users WHERE deleted_at IS NULL; Find active (non-deleted) rowsSELECT * FROM orders WHERE shipped_at IS NOT NULL; Find shipped ordersSELECT COALESCE(nickname, name, 'Anonymous') FROM users; Return first non-null valueNotes:
Never use = NULL or != NULL. Always use IS NULL / IS NOT NULL. COALESCE returns the first non-null argument.
INNER JOIN
JoinsReturn rows that have matching values in both tables
Syntax:
SELECT cols FROM t1 INNER JOIN t2 ON t1.id = t2.fk;Examples:
SELECT u.name, o.total
FROM users u
INNER JOIN orders o ON u.id = o.user_id; Only users who have ordersNotes:
INNER JOIN is the default JOIN type. Rows with no match in either table are excluded.
LEFT JOIN
JoinsReturn all rows from the left table, with NULLs for non-matching right rows
Syntax:
SELECT cols FROM t1 LEFT JOIN t2 ON t1.id = t2.fk;Examples:
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id; All users, NULL if no ordersSELECT u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL; Users who have never orderedRIGHT JOIN
JoinsReturn all rows from the right table, with NULLs for non-matching left rows
Syntax:
SELECT cols FROM t1 RIGHT JOIN t2 ON t1.id = t2.fk;Examples:
SELECT u.name, o.total
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id; All orders, even if the user was deletedNotes:
RIGHT JOIN is less common. Most queries can be rewritten as LEFT JOIN by swapping table order.
FULL OUTER JOIN
JoinsReturn all rows from both tables, with NULLs where there is no match
Syntax:
SELECT cols FROM t1 FULL OUTER JOIN t2 ON t1.id = t2.fk;Examples:
SELECT e.name, d.department_name
FROM employees e
FULL OUTER JOIN departments d ON e.dept_id = d.id; All employees and departments, including unmatchedNotes:
FULL OUTER JOIN is supported in PostgreSQL (unlike SQLite). Useful for finding orphan records on both sides.
CROSS JOIN
JoinsReturn the Cartesian product of two tables
Syntax:
SELECT cols FROM t1 CROSS JOIN t2;Examples:
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c; Generate all size-color combinationsNotes:
CROSS JOIN produces rows = (rows in t1) x (rows in t2). Use with caution on large tables.
COUNT / SUM / AVG
AggregatesAggregate functions for counting, summing, and averaging
Syntax:
SELECT COUNT(*), SUM(col), AVG(col) FROM table;Examples:
SELECT COUNT(*) FROM users; Total row countSELECT SUM(total) FROM orders WHERE user_id = 1; Total spent by user 1SELECT AVG(price) FROM products; Average product priceSELECT COUNT(DISTINCT country) FROM users; Number of unique countriesMIN / MAX
AggregatesFind the smallest or largest value in a column
Syntax:
SELECT MIN(col), MAX(col) FROM table;Examples:
SELECT MIN(price), MAX(price) FROM products; Price rangeSELECT MAX(created_at) FROM orders WHERE user_id = 1; Most recent order dateGROUP BY / HAVING
AggregatesGroup rows and filter groups with aggregate conditions
Syntax:
SELECT col, AGG(col2) FROM table GROUP BY col HAVING condition;Examples:
SELECT country, COUNT(*) AS total FROM users GROUP BY country; Count users by countrySELECT category, AVG(price) FROM products GROUP BY category HAVING AVG(price) > 50; Categories with avg price above 50SELECT user_id, SUM(total) AS spent FROM orders GROUP BY user_id HAVING SUM(total) > 1000 ORDER BY spent DESC; Top spendersSELECT DATE_TRUNC('month', created_at) AS month, COUNT(*) FROM orders GROUP BY month ORDER BY month; Orders per monthNotes:
HAVING filters after aggregation, WHERE filters before. DATE_TRUNC is PostgreSQL-specific.
ROW_NUMBER
Window FunctionsAssign a unique sequential integer to each row within a partition
Syntax:
SELECT ROW_NUMBER() OVER (PARTITION BY col ORDER BY col2) FROM table;Examples:
SELECT name, department,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rank
FROM employees; Rank employees within each department by salarySELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) sub WHERE rn = 1; Latest order per userNotes:
ROW_NUMBER always produces unique numbers. For ties, use RANK or DENSE_RANK.
RANK / DENSE_RANK
Window FunctionsAssign ranks to rows, handling ties differently
Syntax:
SELECT RANK() OVER (ORDER BY col) FROM table;
SELECT DENSE_RANK() OVER (ORDER BY col) FROM table;Examples:
SELECT name, score,
RANK() OVER (ORDER BY score DESC) AS rank,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM students; RANK skips numbers after ties, DENSE_RANK does notNotes:
With scores 100, 90, 90, 80: RANK gives 1,2,2,4 and DENSE_RANK gives 1,2,2,3.
LAG / LEAD
Window FunctionsAccess a row before or after the current row without a self-join
Syntax:
SELECT LAG(col, offset, default) OVER (ORDER BY col2) FROM table;
SELECT LEAD(col, offset, default) OVER (ORDER BY col2) FROM table;Examples:
SELECT date, revenue,
LAG(revenue) OVER (ORDER BY date) AS prev_revenue,
revenue - LAG(revenue) OVER (ORDER BY date) AS growth
FROM daily_sales; Compare each day to the previous daySELECT name, hired_at,
LEAD(name) OVER (ORDER BY hired_at) AS next_hire
FROM employees; Show who was hired nextNotes:
LAG looks backward, LEAD looks forward. The second argument is the offset (default 1), the third is the default value if no row exists.
WITH (CTE)
CTEsDefine a named temporary result set for reuse in a query
Syntax:
WITH cte_name AS (SELECT ...) SELECT * FROM cte_name;Examples:
WITH top_users AS (
SELECT user_id, SUM(total) AS spent
FROM orders
GROUP BY user_id
HAVING SUM(total) > 500
)
SELECT u.name, t.spent
FROM users u
JOIN top_users t ON u.id = t.user_id; Top spenders using a CTEWITH monthly AS (
SELECT DATE_TRUNC('month', created_at) AS month, SUM(total) AS revenue
FROM orders GROUP BY 1
)
SELECT month, revenue,
SUM(revenue) OVER (ORDER BY month) AS running_total
FROM monthly; Running total of monthly revenueNotes:
CTEs improve readability. In PostgreSQL 12+, CTEs can be inlined by the optimizer unless marked WITH ... AS MATERIALIZED.
WITH RECURSIVE
CTEsRecursive common table expression for hierarchical data
Syntax:
WITH RECURSIVE cte AS (
SELECT ... -- base case
UNION ALL
SELECT ... FROM cte WHERE ... -- recursive step
) SELECT * FROM cte;Examples:
WITH RECURSIVE org_chart 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, oc.depth + 1
FROM employees e
JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart ORDER BY depth; Traverse an employee hierarchyWITH RECURSIVE counter(n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM counter WHERE n < 10
)
SELECT n FROM counter; Generate a number sequence 1 to 10Notes:
Always include a termination condition to avoid infinite recursion.
Subquery
SubqueriesUse a nested SELECT as a value, list, or table
Syntax:
SELECT * FROM table WHERE col IN (SELECT col FROM other_table);Examples:
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE country = 'US'); IN subquerySELECT * FROM products WHERE price > (SELECT AVG(price) FROM products); Scalar subquery comparisonSELECT u.name, sub.total
FROM users u
JOIN (SELECT user_id, SUM(total) AS total FROM orders GROUP BY user_id) sub
ON u.id = sub.user_id; Derived table subqueryEXISTS / NOT EXISTS
SubqueriesTest whether a subquery returns any rows
Syntax:
SELECT * FROM table WHERE EXISTS (SELECT 1 FROM other WHERE ...);Examples:
SELECT u.name FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
); Users who have at least one orderSELECT p.name FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
); Products that have never been orderedNotes:
EXISTS is often faster than IN for correlated subqueries because it stops at the first match.
CREATE TABLE
Table DDLDefine a new table with columns and constraints
Syntax:
CREATE TABLE table_name (
col1 TYPE CONSTRAINT,
col2 TYPE,
...
);Examples:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
active BOOLEAN DEFAULT true,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
); Typical users table with PG-specific typesCREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value JSONB
); Create only if it does not existNotes:
PostgreSQL enforces strict types (unlike SQLite). Prefer BIGSERIAL over SERIAL for new tables.
ALTER TABLE
Table DDLModify an existing table structure
Syntax:
ALTER TABLE table_name ADD COLUMN col_name TYPE;
ALTER TABLE table_name DROP COLUMN col_name;
ALTER TABLE table_name RENAME COLUMN old TO new;Examples:
ALTER TABLE users ADD COLUMN phone TEXT; Add a new columnALTER TABLE users DROP COLUMN phone; Remove a columnALTER TABLE users RENAME COLUMN phone TO mobile; Rename a columnALTER TABLE users ALTER COLUMN name SET NOT NULL; Add NOT NULL constraintALTER TABLE users ALTER COLUMN name TYPE VARCHAR(255); Change column typeNotes:
PostgreSQL ALTER TABLE is much more powerful than SQLite. You can add, drop, and rename columns, change types, and modify constraints.
DROP TABLE
Table DDLPermanently remove a table and all its data
Syntax:
DROP TABLE [IF EXISTS] table_name [CASCADE];Examples:
DROP TABLE old_logs; Remove a tableDROP TABLE IF EXISTS temp_cache; Remove only if existsDROP TABLE orders CASCADE; Drop table and all dependent objectsNotes:
CASCADE also drops objects that depend on the table (views, foreign keys). Use with caution.
TRUNCATE
Table DDLQuickly remove all rows from a table
Syntax:
TRUNCATE TABLE table_name [CASCADE];Examples:
TRUNCATE TABLE logs; Remove all rows instantlyTRUNCATE TABLE orders, order_items CASCADE; Truncate multiple related tablesTRUNCATE TABLE users RESTART IDENTITY; Reset auto-increment sequencesNotes:
TRUNCATE is faster than DELETE for large tables because it does not scan rows. It does not fire row-level triggers.
CREATE INDEX
IndexesCreate an index to speed up queries on a column
Syntax:
CREATE [UNIQUE] INDEX [CONCURRENTLY] index_name ON table_name (col1, col2);Examples:
CREATE INDEX idx_users_email ON users (email); B-tree index for email lookupsCREATE INDEX CONCURRENTLY idx_orders_date ON orders (created_at); Create index without locking the tableCREATE INDEX idx_users_name_gin ON users USING gin (to_tsvector('english', name)); GIN index for full-text searchNotes:
CONCURRENTLY avoids locking the table during index creation. Always use it in production.
CREATE UNIQUE INDEX
IndexesCreate an index that also enforces uniqueness
Syntax:
CREATE UNIQUE INDEX index_name ON table_name (col1, col2);Examples:
CREATE UNIQUE INDEX idx_users_username ON users (username); Enforce unique usernamesCREATE UNIQUE INDEX idx_user_email_lower ON users (LOWER(email)); Case-insensitive unique emailNotes:
Expression indexes like LOWER(email) ensure uniqueness regardless of case.
Partial Index
IndexesCreate an index on a subset of rows using a WHERE clause
Syntax:
CREATE INDEX index_name ON table_name (col) WHERE condition;Examples:
CREATE INDEX idx_active_users ON users (email) WHERE active = true; Index only active usersCREATE INDEX idx_unshipped ON orders (created_at) WHERE shipped_at IS NULL; Index only unshipped ordersNotes:
Partial indexes are smaller and faster than full indexes. Great for queries that always filter on the same condition.
PRIMARY KEY
ConstraintsUniquely identify each row in a table
Syntax:
col_name TYPE PRIMARY KEY
-- or as table constraint:
CONSTRAINT pk_name PRIMARY KEY (col1, col2)Examples:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL
); Single column primary keyCREATE TABLE order_items (
order_id BIGINT,
product_id BIGINT,
quantity INT,
PRIMARY KEY (order_id, product_id)
); Composite primary keyNotes:
PRIMARY KEY implies UNIQUE and NOT NULL. Use BIGSERIAL or UUID for auto-generated keys.
FOREIGN KEY
ConstraintsEnforce referential integrity between tables
Syntax:
col_name TYPE REFERENCES parent_table(col)
-- or:
CONSTRAINT fk_name FOREIGN KEY (col) REFERENCES parent(col) ON DELETE CASCADEExamples:
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id)
); Inline foreign keyCREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT,
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
); Named FK with cascade deleteNotes:
ON DELETE CASCADE automatically deletes child rows. Other options: SET NULL, SET DEFAULT, RESTRICT, NO ACTION.
UNIQUE / CHECK / DEFAULT
ConstraintsEnforce uniqueness, validation rules, and default values
Syntax:
col TYPE UNIQUE
col TYPE CHECK (condition)
col TYPE DEFAULT valueExamples:
CREATE TABLE products (
sku TEXT UNIQUE NOT NULL,
price NUMERIC CHECK (price >= 0),
status TEXT DEFAULT 'draft'
); Multiple constraints on columnsALTER TABLE users ADD CONSTRAINT chk_age CHECK (age >= 0 AND age <= 150); Add a check constraint to existing tableNotes:
CHECK constraints are evaluated for every INSERT and UPDATE. They cannot reference other tables.
-> / ->> / @>
JSON/JSONBAccess and query JSONB data with operators
Syntax:
col -> 'key' -- returns JSONB
col ->> 'key' -- returns TEXT
col @> '{"key": "val"}' -- containsExamples:
SELECT data -> 'address' -> 'city' FROM users; Navigate nested JSON (returns JSONB)SELECT data ->> 'name' FROM users; Extract value as textSELECT * FROM users WHERE data @> '{"role": "admin"}'; Find rows where JSON contains key-value pairSELECT * FROM events WHERE tags ? 'urgent'; Check if JSONB contains a keyNotes:
-> returns JSONB, ->> returns TEXT. Use @> with a GIN index for fast containment queries.
jsonb_set / json_agg / jsonb_build_object
JSON/JSONBManipulate and aggregate JSON data
Syntax:
jsonb_set(target, path, new_value)
json_agg(expression)
jsonb_build_object(key1, val1, ...)Examples:
UPDATE users SET data = jsonb_set(data, '{address,city}', '"New York"') WHERE id = 1; Update a nested JSON fieldSELECT department, json_agg(jsonb_build_object('name', name, 'salary', salary)) FROM employees GROUP BY department; Aggregate rows into a JSON arraySELECT jsonb_build_object('id', id, 'name', name, 'email', email) FROM users; Build JSON object from columnsSELECT * FROM users, jsonb_each(data) AS kv(key, value); Expand JSONB object into key-value rowsNotes:
JSONB is stored in a binary format. It is faster to query than JSON but slightly slower to insert.
ANY / ALL
ArraysCompare a value against array elements
Syntax:
SELECT * FROM table WHERE val = ANY(array_col);
SELECT * FROM table WHERE val = ALL(array_col);Examples:
SELECT * FROM users WHERE 'admin' = ANY(roles); Find users with admin roleSELECT * FROM products WHERE id = ANY(ARRAY[1, 2, 3]); Match any id in the arraySELECT * FROM scores WHERE 100 <= ALL(attempts); All attempts scored 100 or aboveNotes:
ANY returns true if at least one element matches. ALL returns true only if every element matches.
array_agg / unnest
ArraysAggregate values into an array or expand an array into rows
Syntax:
SELECT array_agg(col) FROM table;
SELECT unnest(array_col) FROM table;Examples:
SELECT department, array_agg(name ORDER BY name) FROM employees GROUP BY department; Collect names into a sorted array per departmentSELECT unnest(tags) AS tag FROM posts; Expand an array column into individual rowsSELECT unnest(ARRAY['a','b','c']) AS letter; Expand a literal array into rowsNotes:
array_agg can include ORDER BY inside the aggregate. unnest is the inverse operation.
BEGIN / COMMIT / ROLLBACK
TransactionsGroup multiple statements into an atomic unit
Syntax:
BEGIN;
-- statements
COMMIT; -- or ROLLBACK;Examples:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; Transfer funds atomicallyBEGIN;
DELETE FROM users WHERE id = 99;
ROLLBACK; Undo changes with ROLLBACKNotes:
PostgreSQL supports full ACID transactions. All statements inside BEGIN...COMMIT succeed or fail together.
SAVEPOINT
TransactionsCreate a checkpoint within a transaction for partial rollback
Syntax:
SAVEPOINT name;
-- statements
ROLLBACK TO name;
RELEASE SAVEPOINT name;Examples:
BEGIN;
INSERT INTO orders (user_id, total) VALUES (1, 50);
SAVEPOINT sp1;
INSERT INTO order_items (order_id, product_id) VALUES (1, 999);
ROLLBACK TO sp1;
INSERT INTO order_items (order_id, product_id) VALUES (1, 100);
COMMIT; Partial rollback within a transactionNotes:
RELEASE SAVEPOINT removes the savepoint without rolling back. Useful for error recovery in complex transactions.
CREATE FUNCTION
FunctionsDefine a reusable function with PL/pgSQL or SQL
Syntax:
CREATE OR REPLACE FUNCTION func_name(args)
RETURNS return_type AS $$
BEGIN
-- body
END;
$$ LANGUAGE plpgsql;Examples:
CREATE OR REPLACE FUNCTION get_user_count()
RETURNS INTEGER AS $$
BEGIN
RETURN (SELECT COUNT(*) FROM users);
END;
$$ LANGUAGE plpgsql; Simple function returning a countCREATE OR REPLACE FUNCTION apply_discount(
p_product_id BIGINT,
p_percent NUMERIC
) RETURNS NUMERIC AS $$
DECLARE
new_price NUMERIC;
BEGIN
UPDATE products
SET price = price * (1 - p_percent / 100)
WHERE id = p_product_id
RETURNING price INTO new_price;
RETURN new_price;
END;
$$ LANGUAGE plpgsql; Function that modifies data and returns resultCREATE OR REPLACE FUNCTION add_numbers(a INT, b INT)
RETURNS INT AS $$
SELECT a + b;
$$ LANGUAGE sql; Simple SQL-language functionNotes:
PL/pgSQL supports variables, loops, conditionals, and exception handling. SQL-language functions are simpler for pure queries.
SERIAL / BIGSERIAL
SequencesAuto-incrementing integer columns
Syntax:
col_name SERIAL -- 4-byte integer
col_name BIGSERIAL -- 8-byte integerExamples:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
); Auto-incrementing 4-byte idCREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
type TEXT NOT NULL
); Auto-incrementing 8-byte id for high-volume tablesNotes:
SERIAL is shorthand for creating a sequence and setting the default. Prefer BIGSERIAL for new tables to avoid overflow. Modern alternative: GENERATED ALWAYS AS IDENTITY.
CREATE SEQUENCE / nextval
SequencesCreate and use custom sequences
Syntax:
CREATE SEQUENCE seq_name START 1 INCREMENT 1;
SELECT nextval('seq_name');Examples:
CREATE SEQUENCE invoice_number_seq START 1000 INCREMENT 1; Custom sequence starting at 1000SELECT nextval('invoice_number_seq'); Get next value from sequenceSELECT currval('invoice_number_seq'); Get current value (after nextval was called)ALTER SEQUENCE invoice_number_seq RESTART WITH 5000; Reset sequence to a new valueNotes:
Sequences are guaranteed to produce unique values even under concurrent access. They may have gaps after rollbacks.
UUID
TypesUniversally unique identifier type
Syntax:
col_name UUID DEFAULT gen_random_uuid()Examples:
CREATE TABLE sessions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id BIGINT REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
); Table with UUID primary keySELECT gen_random_uuid(); Generate a random UUIDNotes:
gen_random_uuid() is built-in since PostgreSQL 13. For older versions, use the uuid-ossp extension.
TIMESTAMPTZ
TypesTimestamp with time zone -- the recommended timestamp type
Syntax:
col_name TIMESTAMPTZ DEFAULT NOW()Examples:
SELECT NOW(); Current timestamp with time zoneSELECT NOW() AT TIME ZONE 'UTC'; Convert to UTCSELECT created_at AT TIME ZONE 'America/New_York' FROM events; Display in a specific time zoneSELECT * FROM events WHERE created_at > NOW() - INTERVAL '24 hours'; Events in the last 24 hoursNotes:
Always use TIMESTAMPTZ over TIMESTAMP. PostgreSQL stores TIMESTAMPTZ in UTC internally and converts on display.
TEXT / VARCHAR / NUMERIC
TypesCommon PostgreSQL data types
Syntax:
TEXT -- unlimited length string
VARCHAR(n) -- string with max length
NUMERIC(p, s) -- exact decimal numberExamples:
CREATE TABLE products (
name TEXT NOT NULL,
sku VARCHAR(50) UNIQUE,
price NUMERIC(10, 2) NOT NULL
); Common column typesSELECT CAST(price AS NUMERIC(10,2)) FROM products; Cast to specific precisionNotes:
In PostgreSQL, TEXT and VARCHAR (without length) perform identically. Use NUMERIC for money and exact decimal values, not FLOAT or REAL.
JSONB
TypesBinary JSON type with indexing and operator support
Syntax:
col_name JSONB DEFAULT '{}'Examples:
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
payload JSONB NOT NULL DEFAULT '{}'
); Table with JSONB columnCREATE INDEX idx_events_payload ON events USING gin (payload); GIN index for fast JSONB queriesNotes:
JSONB is preferred over JSON for most use cases. It supports indexing, containment operators, and is faster for reads.
EXPLAIN
PerformanceShow the query execution plan without running the query
Syntax:
EXPLAIN SELECT ...;Examples:
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com'; Show execution planEXPLAIN (FORMAT JSON) SELECT * FROM users WHERE id = 1; JSON-formatted planNotes:
EXPLAIN shows the estimated plan without executing the query. Use EXPLAIN ANALYZE for actual timing.
EXPLAIN ANALYZE
PerformanceRun the query and show actual execution times and row counts
Syntax:
EXPLAIN ANALYZE SELECT ...;Examples:
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@example.com'; Actual execution time and planEXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 1; Include buffer/cache usage statsNotes:
EXPLAIN ANALYZE actually executes the query. Wrap in a transaction and ROLLBACK for write queries to avoid side effects.
VACUUM / ANALYZE
PerformanceReclaim storage and update query planner statistics
Syntax:
VACUUM [FULL] [ANALYZE] [table_name];Examples:
VACUUM; Reclaim dead tuple storage across all tablesVACUUM ANALYZE users; Vacuum and update statistics for users tableVACUUM FULL large_table; Reclaim maximum space (locks the table)ANALYZE users; Update planner statistics onlyNotes:
Autovacuum handles this automatically in most cases. Run ANALYZE after bulk inserts or deletes to keep the query planner accurate.
\l / \c
psql CLIList databases and connect to a database
Syntax:
\l -- list databases
\c dbname -- connect to databaseExamples:
\l List all databases\c myapp_production Connect to a specific database\conninfo Show current connection infoNotes:
psql meta-commands start with a backslash. They are not SQL and are only available in the psql shell.
\dt / \d
psql CLIList tables and describe table structure
Syntax:
\dt -- list tables
\d tablename -- describe tableExamples:
\dt List all tables in current schema\dt public.* List tables in the public schema\d users Show columns, types, and constraints for users table\di List all indexes\dv List all views\df List all functions\copy / \timing
psql CLIImport/export data and enable query timing
Syntax:
\copy table FROM 'file.csv' CSV HEADER;
\timingExamples:
\copy users TO '/tmp/users.csv' CSV HEADER; Export table to CSV\copy users FROM '/tmp/users.csv' CSV HEADER; Import CSV into table\timing Toggle query execution timing on/off\x Toggle expanded (vertical) display\o /tmp/output.txt Redirect query output to a fileNotes:
\copy runs on the client side (unlike COPY which runs on the server). Use \copy when you do not have server filesystem access.
PostgreSQL Best Practices
Query Optimization
- Use
EXPLAIN ANALYZEto profile slow queries - Create indexes on columns used in WHERE, JOIN, and ORDER BY
- Use
CONCURRENTLYwhen creating indexes in production - Prefer
EXISTSoverINfor correlated subqueries - Use partial indexes for filtered queries
- Run
ANALYZEafter bulk data changes
Schema Design
- Use
TIMESTAMPTZinstead ofTIMESTAMP - Prefer
BIGSERIALoverSERIALfor primary keys - Use
TEXTinstead ofVARCHARunless you need a length limit - Use
NUMERICfor money, neverFLOAT - Add
NOT NULLconstraints wherever possible - Use
ON CONFLICTfor upsert instead of check-then-insert
Security
- Always use parameterized queries to prevent SQL injection
- Use
ROLEandGRANTfor least-privilege access - Enable SSL for all database connections
- Use
pg_hba.confto restrict network access
Maintenance
- Let autovacuum run -- do not disable it
- Monitor table bloat and dead tuples
- Set up regular
pg_dumpbackups - Use
pg_stat_statementsto find slow queries