Functions Basics
9 examples to get you started with PostgreSQL functions - 6 basic and 3 intermediate.
Prerequisites
CREATE on schema app.
# Local dev with Docker (PostgreSQL 18)
docker run -d --name pg18 -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres:18
psql postgres://postgres:dev@localhost:5432/postgresBasic Examples
1. SQL Function
Single SELECT expression body.
CREATE OR REPLACE FUNCTION app.account_order_count(p_account_id bigint)
RETURNS bigint
LANGUAGE sql
STABLE
AS $$
SELECT count(*) FROM app.orders WHERE account_id = p_account_id;
$$;- Inlinable by planner when marked STABLE/IMMUTABLE
- No procedural variables
- Fast for simple projections
2. PL/pgSQL Function
Variables and IF/LOOP.
CREATE OR REPLACE FUNCTION app.is_vip(p_account_id bigint)
RETURNS boolean
LANGUAGE plpgsql
STABLE
AS $$
DECLARE v_total numeric;
BEGIN
SELECT coalesce(sum(total),0) INTO v_total FROM app.orders WHERE account_id = p_account_id;
RETURN v_total > 10000;
END;
$$;- PL/pgSQL for branches and loops
- VOLATILE default if not specified on SQL
- Keep bodies small
3. Call in Query
Use in SELECT and WHERE.
SELECT id, app.account_order_count(id) AS orders FROM app.accounts;- Correlated calls per row can be slow
- Consider JOIN rewrite
- Mark volatility correctly
4. RETURNS TABLE
Set-returning function shape.
CREATE OR REPLACE FUNCTION app.big_orders(p_min numeric)
RETURNS TABLE (order_id bigint, total numeric)
LANGUAGE sql
STABLE
AS $$
SELECT id, total FROM app.orders WHERE total >= p_min;
$$;- Works like a parameterized view
- Planner may inline SQL functions
- Grant EXECUTE to roles
5. IMMUTABLE Example
Pure functions enable index expressions.
CREATE OR REPLACE FUNCTION app.norm_email(e text)
RETURNS text
LANGUAGE sql
IMMUTABLE
AS $$ SELECT lower(trim(e)); $$;- IMMUTABLE must not depend on DB contents or time
- Wrong label causes stale plans
- Use for simple string math
6. DROP and Replace
CREATE OR REPLACE updates body.
CREATE OR REPLACE FUNCTION app.norm_email(e text) RETURNS text
LANGUAGE sql IMMUTABLE AS $$ SELECT lower(trim(e)); $$;- Cannot change return type with OR REPLACE
- Use DROP FUNCTION if signature changes
- Track in migrations
Intermediate Examples
7. Procedure vs Function
Procedures allow transaction control (CALL).
CREATE OR REPLACE PROCEDURE app.archive_old_orders()
LANGUAGE plpgsql
AS $$
BEGIN
DELETE FROM app.orders WHERE created_at < now() - interval '2 years';
END;
$$;
CALL app.archive_old_orders();- Procedures use CALL not SELECT
- Can COMMIT inside procedure blocks
- Functions cannot commit
8. SECURITY INVOKER
Default runs as caller.
ALTER FUNCTION app.account_order_count(bigint) SECURITY INVOKER;- Invoker respects RLS of caller
- DEFINER elevates privileges
- Audit definer functions carefully
9. Set search_path in Definer
Harden privileged functions.
CREATE OR REPLACE FUNCTION app.admin_stat()
RETURNS int LANGUAGE sql SECURITY DEFINER
SET search_path = pg_catalog, pg_temp
AS $$ SELECT 1; $$;- Prevents object hijacking
- See security definer article
- Minimal privilege still required
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.