Connecting
Default port is 5432. Build a URL for your host with the PostgreSQL connection string generator.
| Command | What it does |
|---|---|
| Connect over the local socket as your OS user, to the same-named database | |
| Connect with host, port, user and database | |
| Force a password prompt before connecting | |
| Connect with a connection URL | |
| Require TLS, prompt for the password | |
| Key-value form, verify the server certificate | |
| Pass the password for one command | |
| Defaults for psql, pg_dump and every libpq tool | |
| Save a password so psql stops asking | |
| Required, psql ignores a readable .pgpass | |
| Run one statement and exit | |
| Bare values, no headers or padding, for scripts | |
| Check the server accepts connections | |
| Installed client version |
Example: connect over verified TLS with the password stored in instead of your shell history.
echo 'db.example.com:5432:shop:app:s3cret' >> ~/.pgpass
chmod 600 ~/.pgpass
psql "host=db.example.com dbname=shop user=app sslmode=verify-full sslrootcert=ca.pem"
psql Meta Commands
Backslash commands run in psql only, not in other clients. Start psql with to print the SQL behind each one.
| Command | What it does |
|---|---|
| List databases | |
| Switch to another database | |
| Switch database and user | |
| Current database, user, host, port and TLS details | |
| List tables in the search path | |
| List tables in one schema | |
| Tables with size and description | |
| Describe a table: columns, indexes, foreign keys | |
| Same plus storage and comments | |
| List schemas | |
| List users and roles with their attributes | |
| List indexes, views, sequences, functions | |
| List installed extensions | |
| Grants on a table | |
| Print a function's source | |
| Expanded rows only when they are too wide | |
| Print long results without the pager | |
| Show NULL as visible text | |
| Print how long each statement took | |
| Run one query with expanded output | |
| Rerun the query every 2 seconds | |
| Edit the last query in | |
| SQL syntax help for a statement | |
| Help for every meta command | |
| Quit |
Outside psql, replaces and replaces .
Example: a psql session that finds the orders table, checks its size and watches new rows arrive.
\c shop
\dt+ orders
\d orders
\timing on
SELECT count(*) FROM orders WHERE created_at > now() - interval '1 hour' \watch 5
Databases and Schemas
| Command | What it does |
|---|---|
| Create a database owned by a role | |
| Copy a database; nobody may be connected to shop | |
| Same as CREATE DATABASE, from the shell | |
| Rename; fails while others are connected | |
| Change the owner | |
| Size of one database | |
| DestructiveDelete a database and every table in it | |
| DestructiveDisconnect everyone, then delete | |
| Which database you are in | |
| Create a schema owned by a role | |
| Resolve unqualified names in this order, this session | |
| Same, for every new session of a role | |
| Current schema lookup order | |
| Move a table to another schema | |
| DestructiveDelete a schema and everything in it |
Example: make a scratch copy of the shop database for a risky migration, then drop it.
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname = 'shop' AND pid <> pg_backend_pid(); -- template must be idle
CREATE DATABASE shop_migration_test TEMPLATE shop OWNER app;
DROP DATABASE shop_migration_test WITH (FORCE); -- when you are done
Users, Roles and Grants
A user is a role with LOGIN. Since PostgreSQL 15, ordinary users cannot create tables in the public schema until you grant CREATE.
| Command | What it does |
|---|---|
| Create a role that can log in | |
| Create a group role to hold grants | |
| Change a password; psql sends it hashed | |
| Change a password in SQL | |
| Let a role create databases | |
| Expire the password on a date | |
| Allow connecting to a database | |
| Allow seeing objects in a schema | |
| Allow creating tables in a schema | |
| Read every existing table | |
| Grant on one table | |
| Needed for inserts into serial columns | |
| Also cover tables app creates later | |
| Add a user to a group role | |
| Read every table in every schema (14+) | |
| Take a privilege away | |
| Fix "must be owner of table" for ALTER and DROP | |
| Hand all objects to another role | |
| DestructiveDrop its remaining objects and grants | |
| Delete a role once it owns nothing | |
| Act as another role to test grants | |
| Back to your login role | |
| List users outside psql |
Example: a read-only reporting user for the shop database that also sees tables added later.
CREATE ROLE readonly NOLOGIN;
GRANT CONNECT ON DATABASE shop TO readonly;
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
ALTER DEFAULT PRIVILEGES FOR ROLE app IN SCHEMA public GRANT SELECT ON TABLES TO readonly;
CREATE USER metabase WITH PASSWORD 'change-me' IN ROLE readonly;
Tables
| Command | What it does |
|---|---|
| Create only if missing | |
| Empty copy with defaults, indexes and constraints | |
| New table from a query | |
| Add a column with a default | |
| Add only if missing | |
| DestructiveRemove a column and its data | |
| Rename a column | |
| Rename a table | |
| Change a type; fails if a view uses the column | |
| Change a type with an explicit cast | |
| Set a default for new rows | |
| Forbid NULLs; scans the table | |
| Add a check constraint | |
| Add a foreign key | |
| Remove a constraint | |
| Description shown by | |
| DestructiveDelete every row instantly | |
| DestructiveAlso empties tables that reference it | |
| DestructiveDelete the table |
Example: the orders table used on this page, with an identity key, a foreign key and a check.
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total numeric(10,2) NOT NULL CHECK (total >= 0),
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);
Queries
Format long queries with the PostgreSQL formatter.
| Command | What it does |
|---|---|
| Insert and get the new id back | |
| Insert several rows | |
| Skip rows that would break a unique key | |
| Upsert; EXCLUDED is the row you tried to insert | |
| Update with a join | |
| Delete with a join | |
| Delete and list what went | |
| Latest order per user | |
| Case-insensitive LIKE | |
| Match any value in an array | |
| Page 3 of 20 rows | |
| Conditional count in one pass | |
| Rank rows within each group | |
| ... | Run several statements as one transaction |
| Undo everything since BEGIN | |
| ... | Undo part of a transaction |
Example: upsert a user, then report what each user has spent with a CTE.
INSERT INTO users (email, name, plan) VALUES ('[email protected]', 'Ada King', 'team')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name, plan = EXCLUDED.plan
RETURNING id, plan;
WITH paid AS (
SELECT user_id, sum(total) AS spent FROM orders WHERE status = 'paid' GROUP BY user_id
)
SELECT u.email, coalesce(p.spent, 0) AS spent
FROM users u LEFT JOIN paid p ON p.user_id = u.id
ORDER BY spent DESC;
Import and Export
reads and writes files on your machine. Plain uses files on the database server and needs superuser or the pg_write_server_files role.
| Command | What it does |
|---|---|
| Export a table to CSV | |
| Export a query to CSV | |
| Import CSV into chosen columns | |
| CSV from the shell, no needed | |
| Server COPY that works for any role | |
| Send query output to a file until the next | |
| Run a SQL file inside the session | |
| Run a file in one transaction, stop at the first error | |
| Plain SQL dump of one database | |
| Compressed custom format for pg_restore | |
| Directory format, 4 tables in parallel | |
| Schema only, no rows | |
| Data only, one table | |
| Everything except one table | |
| Dump that restores under any role | |
| Roles and tablespaces, which pg_dump skips | |
| List what a dump contains | |
| Restore into an existing empty database | |
| Create the database named in the dump, then restore | |
| DestructiveDrop each object before recreating it | |
| Parallel restore of a directory dump | |
| Turn a custom dump into a plain SQL file |
In 1bench: the PostgreSQL GUI exports a whole database, one schema or one table to a .sql file with structure, data and sequence values, read inside one read-only transaction. It does not need pg_dump installed.
Example: back up the shop database every night and restore it into a fresh database to prove the backup works.
pg_dump -Fc -d shop -f "shop-$(date +%F).dump"
createdb -O app shop_restore_check
pg_restore -d shop_restore_check --no-owner "shop-$(date +%F).dump"
psql -d shop_restore_check -c 'SELECT count(*) FROM orders'
Indexes and EXPLAIN
| Command | What it does |
|---|---|
| B-tree index; blocks writes while it builds | |
| Build without blocking writes | |
| Unique, case-insensitive email | |
| Partial index, only matching rows | |
| Covering index for index-only scans | |
| Index JSONB for and | |
| Drop without blocking reads and writes | |
| Rebuild a bloated index online | |
| Indexes on a table with their sizes | |
| Index definitions as SQL | |
| Indexes never used since stats reset | |
| Show the plan without running the query | |
| Run the query and show real rows and times | |
| Add cache hits, reads and output columns | |
| Plan as JSON for visualizer tools | |
| Refresh planner statistics after a bulk load |
Since PostgreSQL 18, prints buffer counts without asking.
Example: check that the latest-orders query uses an index, and time a delete without keeping it.
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 2 ORDER BY created_at DESC LIMIT 20;
-- Index Scan using orders_user_created_idx on orders ... (actual time=0.009..0.010 rows=20.00 loops=1)
BEGIN;
EXPLAIN ANALYZE DELETE FROM orders WHERE status = 'refunded'; -- really deletes
ROLLBACK; -- so put it back
Sessions and Locks
| Command | What it does |
|---|---|
| Who is active and what they run | |
| Longest-running queries first | |
| Sessions holding an open transaction | |
| Waiting sessions and the PIDs blocking them | |
| Locks held or wanted on one table | |
| Cancel the running query, keep the session | |
| DestructiveKill the session, rolling back its transaction | |
| DestructiveKill all connections to a database | |
| Cancel this session's statements after 30 seconds | |
| Give up waiting for a lock after 5 seconds | |
| End forgotten open transactions for a role | |
| Your own PID, to exclude from kill queries |
Ctrl+C in psql cancels the running query, the same as .
In 1bench: the PostgreSQL GUI server overview shows connections against max_connections split into active and idle, the cache hit rate, uptime, and the largest databases with their size and connection count.
Example: find the session blocking an update and cancel its query, killing the session only if that fails.
SELECT pid, pg_blocking_pids(pid) AS blocked_by, left(query, 60) AS query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0; -- 26077 | {26074} | UPDATE users ...
SELECT pg_cancel_backend(26074);
SELECT pg_terminate_backend(26074); -- if it is idle in transaction
Server Admin
| Command | What it does |
|---|---|
| Full server version string | |
| Just the version number | |
| Path to postgresql.conf | |
| Path to pg_hba.conf, the login rules | |
| Where the data files live | |
| Connection limit | |
| Write a setting to postgresql.auto.conf | |
| Apply config changes that need no restart | |
| Changes waiting for a restart | |
| Table size including indexes and TOAST | |
| 10 largest tables | |
| Dead rows and last autovacuum per table | |
| Reclaim dead rows and refresh statistics | |
| DestructiveRewrites the table, blocks all access | |
| Install an extension in this database | |
| Extensions the server can install | |
| Top queries by total time | |
| true on a read replica | |
| Is the server running, and its PID | |
| Reload config from the shell | |
| Restart, needed for settings like shared_buffers |
pg_stat_statements also needs in postgresql.conf and a restart.
Example: raise work_mem for the whole server without a restart, then confirm it from a new session.
psql -U postgres -c "ALTER SYSTEM SET work_mem = '64MB'" -c "SELECT pg_reload_conf()"
psql -U postgres -c 'SHOW work_mem' # 64MB
Dates, JSONB and Strings
The common ones. A full date functions sheet and a JSONB sheet are coming. Escape quotes in values with the PostgreSQL escape tool.
| Command | What it does |
|---|---|
| Current time, today, this time yesterday | |
| First instant of the month, for grouping | |
| Year as a number; also month, day, dow | |
| Format a timestamp as text | |
| Convert timestamptz to UTC wall time | |
| One row per day, for gap-free reports | |
| JSONB field as text | |
| Nested JSONB field as text | |
| Rows whose JSONB contains this object | |
| Replace one key, value must be JSON | |
| One row per array element | |
| Join values from many rows into one string | |
| Part of a string by delimiter | |
| Escape a single quote by doubling it |
Example: daily signup counts for the last week from the events table, with plan pulled from JSONB.
SELECT d::date AS day, e.payload->>'plan' AS plan, count(e.id) AS signups
FROM generate_series(current_date - 6, current_date, interval '1 day') d
LEFT JOIN events e ON e.created_at::date = d::date AND e.type = 'signup'
GROUP BY 1, 2
ORDER BY 1;
Neon, Supabase, RDS and YugabyteDB
All four speak the Postgres protocol, so psql, pg_dump and every command above work. These rows differ.
| Command | What it does |
|---|---|
| Neon; add to the endpoint ID for the pooled host | |
| Print a Neon branch's connection string | |
| Branch a Neon database, data included | |
| Supabase session pooler, works over IPv4 | |
| Supabase direct connection, IPv6 unless you buy the IPv4 add-on | |
| AWS CA bundle for verify-full on RDS and Aurora | |
| Let an RDS or Aurora user log in with IAM | |
| IAM token to use as the password, valid 15 minutes (PAM authentication failed?) | |
| Aurora reader endpoint; drop for the writer | |
| YugabyteDB shell, a psql fork with the same meta commands |
RDS and Aurora have no superuser. The master user gets rds_superuser, and server settings live in parameter groups, so is refused.
In 1bench: the Amazon RDS GUI signs a fresh IAM token for every new connection and ships the RDS CA bundle, so verify-full works without downloading global-bundle.pem or exporting a new PGPASSWORD every 15 minutes.
Example: connect to RDS PostgreSQL with IAM auth and a verified TLS connection.
curl -sO https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
export PGPASSWORD="$(aws rds generate-db-auth-token \
--hostname shop.abc123.us-east-1.rds.amazonaws.com --port 5432 \
--region us-east-1 --username app)"
psql "host=shop.abc123.us-east-1.rds.amazonaws.com dbname=shop user=app sslmode=verify-full sslrootcert=global-bundle.pem"
Gotchas
- Unquoted names fold to lowercase. A table created as must be quoted in every query, and looks for .
- does not grant table access. It covers CONNECT, CREATE and TEMP only, so users still get "permission denied for table" (error 42501) until you grant on the schema and tables.
- only covers tables created later by the role it names (or by you, without ). Run it as, or for, the role that runs your migrations.
- fails with "cannot alter type of a column used by a view or rule". Drop and recreate the view in the same transaction.
- executes the statement. On an UPDATE or DELETE, wrap it in and .
- On macOS, and can fail with "a worker process died unexpectedly" when the host is a name like localhost. It showed up in testing with the EDB 18.6 binaries. Setting fixed it, and so did connecting to 127.0.0.1.