Functions, procedures and triggers
Functions and procedures
The idea in 30 seconds
A function packages a query or a piece of logic so SQL can call it like a built-in. Every function declares how predictable it is: VOLATILE, STABLE or IMMUTABLE. PostgreSQL trusts that label to decide what it may cache and index, so a wrong label produces wrong answers without any error. Procedures are for the one thing functions cannot do: commit in the middle.
Helps to know first: SQL data modification, Constraints and data integrity
On this page
A function is a named piece of SQL
A function takes arguments, runs some SQL or procedural code, and returns a value or a set of rows. The simplest kind is written in SQL itself:
CREATE OR REPLACE FUNCTION order_total(p_order_id integer) RETURNS numeric
LANGUAGE sql STABLE
AS $$
SELECT sum(quantity * unit_price) FROM order_items WHERE order_id = p_order_id
$$;
SELECT id, order_total(id) FROM orders WHERE customer_id = 1;
For loops, conditions and error handling there is PL/pgSQL, PostgreSQL’s procedural language. You will see it in triggers on the next page.
Volatility: a promise PostgreSQL believes
Every function is labelled with how predictable it is:
| Label | Promise | Example |
|---|---|---|
VOLATILE (the default) | Anything goes: may change data, may return something different every call | random(), a function that inserts rows |
STABLE | Changes nothing; same result for the same arguments within one statement | Anything that reads tables, or depends on settings like the time zone |
IMMUTABLE | Changes nothing; same result for the same arguments forever | Arithmetic, lower(text) |
The label is not decoration. PostgreSQL uses it to decide what it may evaluate once instead of per row, and what it may store in an index. Only IMMUTABLE functions can be indexed, because an index stores results computed once and reuses them forever:
CREATE OR REPLACE FUNCTION order_month(ts timestamptz) RETURNS timestamptz
LANGUAGE sql STABLE AS $$ SELECT date_trunc('month', ts) $$;
CREATE INDEX ON orders (order_month(ordered_at));
ERROR: functions in index expression must be marked IMMUTABLE
The tempting fix is to change STABLE to IMMUTABLE. PostgreSQL does not check the claim.
PredictThe index that lies
order_month is relabelled IMMUTABLE and indexed while the session’s time zone is India. Later, a session set to UTC counts March orders with WHERE order_month(ordered_at) = '2025-03-01 00:00+00'. Will the index give the same answer as reading the table?
No. The index found 0 orders; reading the table found 3.
how | count
--------------------+-------
using the index | 0
scanning the table | 3date_trunc('month', …) on a timestamptz depends on the session’s time zone: the start of March in India is a different instant from the start of March in UTC. The index stored the Indian answers. Asked the UTC question, it matched none of them. No error, no warning; just a wrong count that depends on which plan the database happens to choose.
The PostgreSQL documentation calls this out as a common error: a function whose result depends on a setting such as the time zone must be STABLE, not IMMUTABLE. To index months safely, index a value that does not depend on the session, such as date_trunc('month', ordered_at AT TIME ZONE 'UTC'), and query with the same expression.
Procedures: when you need to commit along the way
A function runs inside the transaction of the statement that called it, so it cannot end that transaction:
CREATE OR REPLACE FUNCTION f_commit() RETURNS void LANGUAGE plpgsql AS $$ BEGIN COMMIT; END $$;
SELECT f_commit();
ERROR: invalid transaction termination
CONTEXT: PL/pgSQL function f_commit() line 1 at COMMIT
A procedure, run with CALL, may COMMIT and ROLLBACK as it goes. That is what long batch jobs need, such as archiving old rows in chunks of 10,000 and committing after each chunk, so each chunk’s work is saved and its locks are released before the next one starts.
CREATE OR REPLACE PROCEDURE p_commit() LANGUAGE plpgsql AS $$ BEGIN COMMIT; END $$;
CALL p_commit(); -- CALL
Functions that run with someone else’s privileges
By default a function runs with the privileges of whoever calls it. SECURITY DEFINER makes it run with its owner’s privileges instead, which is how you can let a user do one specific privileged thing. It is also a classic security hole: if the function’s search_path includes a schema the caller can write to, the caller can create a table or function with a matching name and hijack it. The documentation’s rule: set search_path on every SECURITY DEFINER function, excluding schemas untrusted users can write to.
Check yourself
Why does PostgreSQL refuse to index a
STABLEfunction?Show answer
An index stores each result once and reuses it forever. A
STABLEfunction only promises the same result within one statement, so a stored result could become wrong later.What went wrong when
order_monthwas markedIMMUTABLE?Show answer
Its result depends on the session time zone. The index stored values computed in one time zone; a query in another time zone computed different values, so the index matched nothing while a table scan found three rows.
When do you need a procedure instead of a function?
Show answer
When the code must commit or roll back partway through, as in a batch job that processes rows in chunks. Only procedures called with
CALLcan end and start transactions.
Finished reading?
Saved in this browser only.
Sources
Where each claim on this page comes from.
- 1.PostgreSQL documentation: 36.7 Function Volatility CategoriesPrimary· §36.7 Function Volatility Categories
The meaning of VOLATILE, STABLE and IMMUTABLE; a common error is labelling IMMUTABLE a function whose results depend on a setting such as TimeZone, which should be STABLE; PostgreSQL does not enforce the label.
- 2.PostgreSQL documentation: 41.8 Transaction ManagementPrimary· §41.8 Transaction Management
COMMIT and ROLLBACK are allowed in procedures invoked by CALL and in DO blocks.
- 3.PostgreSQL documentation: CREATE FUNCTIONPrimary· Writing SECURITY DEFINER Functions Safely
A SECURITY DEFINER function runs with its owner's privileges, so its search_path should exclude schemas untrusted users can write to.
Go further
What to read next, and why it is worth the time.
- 1.PostgreSQL documentation: 36.7 Function Volatility CategoriesPrimary
The single most important page about writing functions. It is short; read all of it.
Applies to PostgreSQL 18. Draft: not yet fact-checked end to end.