PL/pgSQL Explained
PL/pgSQL is PostgreSQL's built-in procedural language, and its entire value proposition is that it runs inside the same process as the query executor instead of shipping rows back and forth to a client.
Search across all documentation pages
PL/pgSQL is PostgreSQL's built-in procedural language, and its entire value proposition is that it runs inside the same process as the query executor instead of shipping rows back and forth to a client.
That proximity is also its limit: PL/pgSQL is not a general-purpose programming language, it is glue code for expressing control flow that plain SQL cannot, and reaching for it too early trades away the planner's ability to optimize your logic as a single query.
This page builds the mental model behind blocks, volatility, STRICT, and the function-versus-procedure split so that Functions Basics, PL/pgSQL Essentials, Volatility & STRICT, and Stored Procedures and CALL read as one coherent system.
DECLARE/BEGIN/EXCEPTION/END) around SQL statements, running inside the database process rather than round-tripping to a client.A PL/pgSQL function body is one block: an optional DECLARE section for local variables, a BEGIN/END section holding statements, and an optional EXCEPTION section for error handling.
CREATE OR REPLACE FUNCTION app.bump_version(p_id bigint)
RETURNS void LANGUAGE plpgsql AS $$
BEGIN
UPDATE app.accounts SET name = name || ' v2' WHERE id = p_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'missing account %', p_id;
END IF;
END;
$$;Every SQL statement inside that block runs through PostgreSQL's Server Programming Interface (SPI), the same internal API that lets PL/pgSQL hand a query string to the ordinary parser, planner, and executor and get rows back.
The special variable FOUND is set after every SPI-executed statement to whether it affected or returned at least one row, which is exactly what the example above uses to detect a missing account.
Control flow reads like any procedural language: IF/ELSIF/ELSE, LOOP/WHILE/FOR, and an implicit cursor opens automatically whenever a FOR loop iterates over a query's results.
The simplest mental model is that PL/pgSQL is a thin scripting layer wrapped around ordinary SQL statements, not a replacement for SQL's own set-based logic.
A function (CREATE FUNCTION) always runs inside the transaction that called it and returns a value with RETURN, while a procedure (CREATE PROCEDURE, invoked with CALL) can issue its own COMMIT or ROLLBACK and returns nothing unless it uses OUT parameters.
That transaction-control split exists because a function can be called from inside a larger SQL expression, such as a SELECT list or a WHERE clause, where starting or ending a transaction mid-statement would be meaningless.
Volatility tells the planner what it's allowed to assume about repeated calls: IMMUTABLE promises the same input always produces the same output with no side effects, STABLE promises the same result within a single statement's snapshot, and VOLATILE (the default) promises nothing and forces a fresh call every time.
IMMUTABLE -> can be constant-folded, indexed in an expression index
STABLE -> safe to call once per statement, reused across rows
VOLATILE -> re-evaluated for every row, blocks many optimizationsMarking a function STRICT tells PostgreSQL that if any argument is NULL, the function should return NULL immediately without ever executing the body, which both saves work and removes an entire class of NULL-handling bugs from the function itself.
Trigger functions are a special case that returns the pseudo-type trigger instead of a normal SQL type, and PostgreSQL calls them automatically with implicit NEW/OLD row variables rather than explicit arguments, which is the shared machinery behind The Trigger Model.
Exception handling works through the EXCEPTION WHEN clause, and because PostgreSQL implements it with a savepoint under the hood, catching an error inside a large loop has a real per-iteration cost that a well-designed batch job should account for.
SECURITY DEFINER changes whose privileges a function runs with: by default a function runs as SECURITY INVOKER (the calling user's privileges), while SECURITY DEFINER makes it run as the function's owner, which is how you let a low-privilege user perform a narrowly scoped privileged action without granting them broad table access.
That power comes with a well-known trap: a SECURITY DEFINER function that doesn't pin its own search_path can be tricked into resolving an unqualified object name to a malicious object the caller controls, which is why every SECURITY DEFINER function should set search_path explicitly in its definition.
Volatility also affects parallel query eligibility, since a VOLATILE function disables parallelism for the query calling it unless it is additionally marked PARALLEL SAFE, so a function that looks harmless can silently force a serial plan on an otherwise parallelizable query.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| SQL function | Inlines into the calling query, fully optimizable by the planner | No procedural control flow at all | Simple wrappers, computed columns, small reusable expressions |
| PL/pgSQL function | Procedural control, exception handling, callable from SQL expressions | Cannot manage its own transactions | Multi-step logic invoked from a query or another function |
Procedure (CALL) | Can COMMIT/ROLLBACK mid-execution | Cannot be called from inside a SQL expression | Long batch jobs that need periodic commits |
| Trigger function | Runs automatically on DML, sees NEW/OLD | Adds invisible cost to every write on the table | Enforcing invariants or capturing changes at write time |
Testing database functions deserves the same discipline as testing application code, and because PL/pgSQL functions run inside a transaction by default, wrapping a test in a transaction that always rolls back is a cheap way to exercise a function's real side effects without leaving test data behind.
CALL can issue its own COMMIT or ROLLBACK, a function always runs inside the caller's existing transaction.STRICT means that if any argument is NULL the function returns NULL immediately without running its body, it says nothing about whether the caller is allowed to omit the argument.search_path can still let a caller escalate what the function does.VOLATILE when it's actually deterministic disables constant folding, expression indexing, and parallel query eligibility that IMMUTABLE or STABLE would have unlocked.A single block: an optional DECLARE section for local variables, a BEGIN/END section of statements, and an optional EXCEPTION section for error handling.
Every embedded SQL statement is handed to the Server Programming Interface (SPI), which routes it through the same parser, planner, and executor as any query run from a client.
It reflects whether the most recent SPI-executed statement affected or returned at least one row, which is the standard way to check whether an UPDATE or SELECT INTO actually matched anything.
No, only a procedure invoked with CALL can issue COMMIT or ROLLBACK, a function always executes inside the transaction that called it.
CALL rather than embed it in a queryIf any argument is NULL, PostgreSQL returns NULL immediately without ever executing the function body, which both saves work and eliminates a class of NULL-handling bugs.
A STABLE function is guaranteed to return the same result for the same arguments within one statement's snapshot, letting the planner call it once and reuse the result, while a VOLATILE function must be called fresh for every row.
Yes, IMMUTABLE functions can back an expression index, and VOLATILE functions disable parallel query for the statement calling them unless also marked PARALLEL SAFE.
If the function doesn't pin its own search_path, a caller can manipulate their session's search_path to trick the function into resolving an unqualified name to an object the caller controls.
It returns the special pseudo-type trigger, is never called directly with arguments, and instead receives implicit NEW/OLD row variables when PostgreSQL invokes it automatically on a matching DML event.
Each EXCEPTION WHEN block is implemented with an underlying savepoint, so wrapping error handling around every iteration of a large loop adds real per-iteration overhead worth measuring in batch jobs.
Prefer plain SQL whenever the logic is genuinely set-based, and reach for PL/pgSQL only when you need real branching, loops, or exception handling that a single query can't express.
Wrap the test call in a transaction that always rolls back at the end, which exercises the function's real side effects without leaving test data behind in the database.
FOUND in practiceIMMUTABLE/STABLE/VOLATILE and planner effectssearch_path trapsCALLStack versions: This page was written for PostgreSQL 18.4 (stable line 18, maintenance line 17), where the block structure, volatility,
STRICT, andSECURITY DEFINERbehavior described here have been stable across recent major releases.
Reviewed by Chris St. John·Last updated Jul 15, 2026