postgresql-ops.md is about configuring PostgreSQL: pg_hba.conf, auth methods,
backup design. This file is about operating a running database from the shell —
the commands you run when you're investigating something or doing maintenance.
psql has a parallel command set prefixed with \. They are not SQL; they are
psql client commands that often run a SQL query under the hood.
| Command | What it does | SQL equivalent |
|---|---|---|
\l |
List all databases | SELECT datname FROM pg_database; |
\du |
List all roles (users) | SELECT rolname FROM pg_roles; |
\c <db> |
Connect to a different database in the same session | (reconnect) |
\dt |
List tables in current schema | SELECT ... FROM pg_class WHERE relkind='r'; |
\d <table> |
Describe a table (columns, indexes, constraints) | (multiple system catalog queries) |
\dn |
List schemas | SELECT nspname FROM pg_namespace; |
\df |
List functions | SELECT ... FROM pg_proc; |
\timing on |
Show execution time after every query | (client-side toggle) |
\x |
Toggle "expanded" output (one column per line) | (client-side toggle) |
\q |
Quit | (client-side) |
The \d family takes a pattern: \dt nextcloud_* lists all tables starting with
nextcloud_. Useful when a database has hundreds of tables across tenants.
pg_stat_activity is the single most useful operational view. It shows every
connection currently open, what it's doing, and how long it's been doing it.
SELECT pid, usename, datname, state, query_start, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start;Key columns:
| Column | Meaning |
|---|---|
pid |
Backend process ID — needed to terminate the connection |
state |
active (running query), idle in transaction (open tx), idle (just sitting there) |
query_start |
When the current query started — long durations indicate stuck or expensive queries |
wait_event |
What the backend is waiting for (lock, I/O, etc.) — NULL means actively running |
idle in transaction is the dangerous state: a client opened a transaction
and walked away, holding locks. Those connections must be terminated to unblock
DDL operations like DROP DATABASE.
Two functions, different semantics:
| Function | Effect |
|---|---|
pg_cancel_backend(pid) |
Cancel the current query on this backend. The connection survives. |
pg_terminate_backend(pid) |
Force-close the entire connection. The client gets a connection error. |
To forcibly close all non-superuser connections to a database (required before
DROP DATABASE):
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = 'olddb'
AND pid != pg_backend_pid();The pid != pg_backend_pid() clause excludes your own session — otherwise you
disconnect yourself and the next statement (the DROP DATABASE) never runs.
sudo -u postgres dropdb olddbWhy dropdb instead of psql -c "DROP DATABASE olddb;":
dropdbis a thin wrapper but it handles edge cases (existence check with--if-exists, force option--forcein PG13+ that auto-terminates connections).- Running as the
postgresOS user viasudo -u postgresuses peer authentication (auth methodpeerinpg_hba.conf) — the OS user maps directly to the DB superuser, no password prompt.
sudo -u postgres dropdb --if-exists --force olddb--force (PG13+) is the killer feature: it terminates active connections automatically
instead of failing with "database is being accessed by other users".
pg_dumpall produces a single SQL file containing roles, tablespaces, and all databases:
psql -U postgres -f /backups/pg-all-20260415.sqlYou will see ERROR: role "X" already exists lines. Normal. The dump contains
unconditional CREATE ROLE statements; on a fresh restore those would create the
roles, on a partial restore they collide with existing ones. The data still
restores correctly because role creation is independent of data restoration.
To verify the dump file is non-empty before trusting it:
zcat /backups/pg-all-20260415.sql.gz | head -50The header includes a SET block and \connect — if you see binary garbage or
nothing, the dump is corrupt and the file is unusable.
\l -- did all databases come back?
\du -- are all roles present?
SELECT count(*) FROM pg_stat_user_tables; -- non-zero per restored DBSELECT schemaname, relname, n_live_tup
FROM pg_stat_user_tables
WHERE relname IN ('users', 'documents') -- known tables you expected
ORDER BY n_live_tup DESC;n_live_tup is an estimate (updated by autovacuum), but it's good enough to
distinguish "table is empty" from "table has data". Don't trust it for exact counts —
use SELECT count(*) for that.
PostgreSQL uses URI-style connection strings (RFC 3986):
postgresql://<user>:<password>@<host>:<port>/<database>?<param>=<value>
| Component | Notes |
|---|---|
user |
URL-encode special characters (@ → %40, : → %3A) |
password |
Same. Avoid passwords with reserved chars; use a ~/.pgpass file when possible |
host |
Hostname, IP, or socket path. host=/var/run/postgresql for Unix-socket connections |
port |
Default 5432 — omit if standard |
database |
Database name. Required for most clients |
| Parameters | sslmode=require, application_name=myapp, connect_timeout=10 |
Example for OpenWebUI pointing at a Tailnet PostgreSQL:
postgresql://owui:secret@100.x.y.z:5432/owui?sslmode=require&application_name=openwebui
sslmode=require is the minimum acceptable for cross-host connections. Use
verify-full if you want certificate-based identity verification too.