The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Create a SQL function | CREATE FUNCTION app.net_total(numeric, numeric) RETURNS numeric LANGUAGE sql IMMUTABLE RETURN $1 - $2; | View examples |
| Call with named notation | SELECT app.calculate_total(subtotal => 100, discount => 5); | View examples |
| Return rows | RETURNS TABLE (order_id bigint, total numeric) | View examples |
| Create PL/pgSQL function | CREATE FUNCTION app.require_positive(value integer) RETURNS integer LANGUAGE plpgsql AS $$ ... $$; | View examples |
| Raise an error | RAISE EXCEPTION 'quantity must be positive'
USING ERRCODE = '22023'; | View examples |
| Handle an expected error | EXCEPTION WHEN unique_violation THEN ... | View examples |
| Create a procedure | CREATE PROCEDURE app.refresh_reports() LANGUAGE plpgsql AS $$ ... $$; | View examples |
| Call a procedure | CALL app.refresh_reports(); | View examples |
| Declare stable behavior | LANGUAGE sql STABLE | View examples |
| Return null on null input | RETURNS NULL ON NULL INPUT | View examples |
| Run as function owner | SECURITY DEFINER SET search_path = pg_catalog, app | View examples |
| Create trigger function | CREATE FUNCTION app.set_updated_at() RETURNS trigger LANGUAGE plpgsql AS $$ ... $$; | View examples |
| Attach row trigger | CREATE TRIGGER set_updated_at BEFORE
UPDATE
ON app.items FOR EACH ROW EXECUTE FUNCTION app.set_updated_at(); | View examples |
| Drop an exact signature | DROP FUNCTION app.net_total(numeric, numeric) RESTRICT; | View examples |
Server-side routines centralize data-adjacent behavior but also introduce privileges, transaction, deployment, and observability concerns. Prefer plain SQL constraints and expressions where sufficient, declare volatility and security truthfully, schema-qualify privileged code, and test routines under the exact caller roles and concurrency they will face.
Step by step
Detailed examples
Use SQL-language functions for expression-shaped logic
SQL functions can return a scalar, one row, or a set and are often easier for the planner to understand than procedural code. Parameter types form the function identity, so overloaded calls can become ambiguous. Explicit schemas and casts make resolution predictable.
CREATE FUNCTION app.net_total(subtotal numeric, discount numeric)
RETURNS numeric
LANGUAGE sql
IMMUTABLE
RETURNS NULL ON NULL INPUT
RETURN subtotal - discount;
SELECT app.net_total(100, 5); net_total
---------
95Use procedural blocks for stateful control, not routine queries
PL/pgSQL adds variables, conditions, loops, diagnostics, and exception blocks. SELECT INTO expects a row assignment; STRICT changes zero or multiple rows into errors. An EXCEPTION block creates extra transaction machinery and rolls database changes in that block back before the handler runs, so keep it narrow.
CREATE FUNCTION app.require_positive(value integer)
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
IF value <= 0 THEN
RAISE EXCEPTION 'quantity must be positive' USING ERRCODE = '22023';
END IF;
RETURN value;
END;
$$; Choose procedures only when CALL semantics are needed
Procedures are invoked with CALL and do not appear in expressions. They can perform transaction control only in allowed top-level CALL contexts and not when surrounding attributes or call chains prohibit it. A procedure is not automatically more performant or secure than a function.
CREATE PROCEDURE app.refresh_reporting_data()
LANGUAGE plpgsql
AS $$
BEGIN
REFRESH MATERIALIZED VIEW app.daily_sales;
INSERT INTO app.refresh_log(refreshed_at) VALUES (clock_timestamp());
END;
$$;
CALL app.refresh_reporting_data(); Tell the planner the truth about volatility and parallel safety
VOLATILE is the default and permits changes on every call. STABLE promises consistent results within a statement; IMMUTABLE promises the same result forever for the same arguments. A false IMMUTABLE declaration can fold stale values into plans or indexes. COST, ROWS, and PARALLEL labels also affect planning and must reflect behavior.
CREATE FUNCTION app.current_tax_rate(region_code text)
RETURNS numeric
LANGUAGE sql
STABLE
STRICT
AS $$
SELECT rate FROM app.tax_rates WHERE region = region_code
$$; Harden every security-definer routine
SECURITY DEFINER runs with owner privileges and is a privilege boundary. Remove PUBLIC execute unless intended, set a trusted search_path that places temporary or user-writable schemas last or excludes them, schema-qualify objects, and avoid dynamic SQL from untrusted strings. The owner must not be an unnecessarily powerful superuser.
CREATE FUNCTION app.account_count()
RETURNS bigint
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = pg_catalog, app
AS $$ SELECT count(*) FROM app.accounts $$;
REVOKE ALL ON FUNCTION app.account_count() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION app.account_count() TO reporting_role; Keep trigger timing, level, and return behavior explicit
BEFORE row triggers may modify and return NEW or return null to skip the row; AFTER row trigger return values are ignored. Statement triggers run once and transition tables can expose changed sets. Multiple triggers introduce hidden ordering and recursion risk, so prefer constraints and explicit writes when they express the rule.
CREATE FUNCTION app.set_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := clock_timestamp();
RETURN NEW;
END;
$$;
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON app.items
FOR EACH ROW
EXECUTE FUNCTION app.set_updated_at(); Version signatures, privileges, and dependencies together
CREATE OR REPLACE cannot change a function's name, argument types, or return type arbitrarily. Adding overloads can change resolution. Capture owner, configuration, comments, grants, and dependent objects; test nulls, errors, permissions, plans, concurrent calls, and rollback behavior before deployment.
SELECT n.nspname, p.proname, pg_get_function_identity_arguments(p.oid) AS arguments,
p.provolatile, p.prosecdef, pg_get_userbyid(p.proowner) AS owner
FROM pg_proc AS p
JOIN pg_namespace AS n ON n.oid = p.pronamespace
WHERE n.nspname = 'app'
ORDER BY p.proname, arguments; Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
- PostgreSQL Global Development GroupPostgreSQL: CREATE FUNCTIONpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: CREATE PROCEDUREpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: PL/pgSQLpostgresql.org
- PostgreSQL Global Development GroupPostgreSQL: Trigger Functionspostgresql.org
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



