psql and the basics of SQL
Drive the server from psql, learn the meta-commands worth memorising, and create tables with the types PostgreSQL actually gives you.
Getting comfortable in psql
psql is the official client and it is worth learning properly: the backslash meta-commands introspect a live database faster than any GUI, and they are available on every server you will ever ssh into.
psql -h 10.0.0.20 -p 5432 -U app -d appdb
psql "postgresql://app:[email protected]:5432/appdb?sslmode=require"
psql -c "SELECT now();" appdb # one statement, good in scripts
psql -f schema.sql appdb # run a file
PGHOST=10.0.0.20 PGUSER=app psql appdb # env vars instead of flags| Meta-command | What it does |
|---|---|
\l | List databases |
\c appdb | Connect to another database |
\dt / \dt+ | List tables (with size) |
\d orders | Describe a table: columns, indexes, foreign keys |
\di | List indexes |
\df | List functions |
\x | Toggle expanded output — invaluable for wide rows |
\timing | Show how long each statement took |
\e / \i file.sql | Edit the current query / run a file |
\du | List roles and their attributes |
Identifiers are folded to lower case unless you quote them, so CREATE TABLE Orders creates orders while "Orders" creates a table whose name you must quote forever after. Prefer plain lower-case snake_case names.
Creating tables with real types
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES customers (id) ON DELETE RESTRICT,
total numeric(12,2) NOT NULL CHECK (total >= 0),
status text NOT NULL DEFAULT 'new'
CHECK (status IN ('new', 'paid', 'shipped', 'cancelled')),
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_orders_customer_created ON orders (customer_id, created_at DESC);
INSERT INTO orders (customer_id, total) VALUES (1, 19.90)
RETURNING id, created_at;GENERATED ALWAYS AS IDENTITYis the modern replacement for theserialpseudo-type and avoids orphaned sequence ownership.- Use
numericfor money, neverrealordouble precision— binary floating point cannot represent 0.10 exactly. - Prefer
timestamptz(stored as UTC) overtimestampso the session time zone cannot change what the value means. texthas no performance penalty againstvarchar(n); add aCHECKconstraint when a length limit is a real business rule.
💡
PostgreSQL runs each statement in its own implicit transaction, so a single
INSERT ... RETURNING is already atomic — no explicit BEGIN needed for one statement.Querying and shaping results
SELECT o.id,
c.name,
o.total,
o.created_at AT TIME ZONE 'UTC' AS created_utc
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = ANY (ARRAY['paid', 'shipped'])
AND o.created_at >= now() - interval '30 days'
ORDER BY o.created_at DESC
LIMIT 50;
-- aggregate, then filter the groups
SELECT status, count(*) AS n, sum(total) AS revenue
FROM orders
GROUP BY status
HAVING count(*) > 5
ORDER BY revenue DESC;LIMITandOFFSETwork, but keyset pagination usingWHERE (created_at, id) < (:last_created, :last_id)stays fast at any depth.ILIKEis a case-insensitiveLIKE; a b-tree index does not serve it, so usecitextor a functional index onlower(col)when you need speed.EXPLAIN (ANALYZE, BUFFERS)gives estimated versus actual rows plus cache behaviour — the first place to look when a query is slow.
FAQ
Where do I set the default schema search path?
With
SET search_path TO app, public; per session or ALTER ROLE app IN DATABASE appdb SET search_path TO app, public; permanently. Without it, unqualified names resolve through public.How do I see what a user can do?
\du lists role attributes and \dp table_name shows the access control list for a table, including inherited grants.Related
JSONB and arrays Transactions and MVCC basics
Last refreshed 2026-09-18.