By WebNest Studio Engineering Team
PostgreSQL Tutorial
Writing Functions in PostgreSQL with PL/pgSQL
PostgreSQL lets you write server-side functions that execute inside the database itself, close to the data. The default and most widely used procedural language for this is PL/pgSQL, which extends plain SQL with variables, conditionals, loops, and exception handling — similar in spirit to a small imperative programming language embedded in SQL.
Functions are useful for encapsulating business logic that should always run consistently regardless of which application or client touches the database, for building reusable calculations, and for powering triggers.
Why it matters
A function stores logic in the database so that every application and report uses the same calculation. PL/pgSQL adds variables, conditions and loops to SQL. You will meet such functions in mature systems, and they are what triggers are built from.
Anatomy of a PL/pgSQL Function
A function is created with CREATE FUNCTION name(parameters) RETURNS return_type AS $$ ... $$ LANGUAGE plpgsql;. The $$ pair is "dollar quoting," which lets you write a multi-line function body without escaping internal quotes. Inside, a DECLARE section (optional) defines local variables, and a BEGIN ... END; block holds the executable logic, ending with a RETURN statement for the declared return type.
Parameters, Logic, and Control Flow
Function parameters are referenced directly by name inside the body. PL/pgSQL supports IF/ELSIF/ELSE, LOOP/WHILE/FOR, and can run ordinary SQL statements (SELECT INTO a variable, INSERT, UPDATE) as part of its logic — meaning a function can both compute a value and have side effects on the database.
Calling a Function
Once created, a function is called like any built-in function: SELECT function_name(arg1, arg2);. Functions can also be used inside larger queries, as default values for columns, or attached to triggers so they run automatically on INSERT/UPDATE/DELETE.
Example
A PL/pgSQL function that applies a capped discount to a price
CREATE FUNCTION apply_discount(price NUMERIC, discount NUMERIC)
RETURNS NUMERIC AS $$
DECLARE
capped NUMERIC;
BEGIN
IF discount > 0.5 THEN
capped := 0.5;
ELSE
capped := discount;
END IF;
RETURN price - (price * capped);
END;
$$ LANGUAGE plpgsql;
SELECT apply_discount(100.00, 0.75);
apply_discount
-----------------
50.00
Common Mistakes
- Forgetting the LANGUAGE plpgsql clause at the end of CREATE FUNCTION, which the parser needs to know how to interpret the function body.
- Using = instead of := for variable assignment inside a PL/pgSQL block — plain SQL comparison and assignment look different in PL/pgSQL.
- Not handling the case where a SELECT INTO a variable returns no rows, leaving the variable NULL and causing confusing downstream errors rather than an explicit check.
- Writing complex, performance-critical logic entirely in PL/pgSQL loops row-by-row when an equivalent set-based SQL query would run far faster.
Exercise
This exercise uses PL/pgSQL, so it needs a PostgreSQL server. Write a function apply_discount that takes a price and a percentage and returns the discounted price rounded to two decimals. The discount must be capped at 50 per cent, whatever percentage is passed. Then call it twice.
Expected result: apply_discount(200, 10) returns 180.00, and apply_discount(200, 80) returns 100.00 because of the cap.
-- TODO: CREATE FUNCTION apply_discount(price numeric, percent numeric) RETURNS numeric
-- TODO: SELECT apply_discount(200, 10);
-- TODO: SELECT apply_discount(200, 80);
Show hints
- Variables are declared in a
DECLAREsection, andLEAST(percent, 50)applies the cap. - The body is written between
BEGINandEND;, and the result is returned withRETURN.
Show solution
CREATE FUNCTION apply_discount(price NUMERIC, percent NUMERIC)
RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
effective_percent NUMERIC := LEAST(percent, 50);
BEGIN
RETURN ROUND(price * (1 - effective_percent / 100), 2);
END;
$$;
SELECT apply_discount(200, 10);
SELECT apply_discount(200, 80);
Quick quiz
1. What is PL/pgSQL?
- A. A graphical tool
- B. A backup format
- C. PostgreSQL's procedural language, which adds variables and control flow to SQL
- D. A type of index
Show answer
C. It is the language most PostgreSQL functions and triggers are written in.
2. What are the $$ marks around a function body?
- A. Dollar quoting, a way to write a string that may itself contain quotation marks
- B. Currency formatting
- C. A comment
- D. A variable prefix
Show answer
A. The body is a string, and dollar quoting avoids having to escape every single quote inside it.
3. How is a function that returns a value used?
- A. Only with CALL
- B. Only from psql
- C. Only inside triggers
- D. Inside a query, for example SELECT apply_discount(200, 10)
Show answer
D. CALL is for procedures.
Interview questions
What is the difference between a function and a procedure in PostgreSQL?
Show answer
A function returns a value or a set of rows and is called inside a query; it runs within the caller's transaction and cannot commit or roll back. A procedure, added in PostgreSQL 11, is run with CALL, does not return a value in the same way, and can commit or roll back inside its body, which makes it suitable for long batch jobs that work in several transactions.
What do IMMUTABLE, STABLE and VOLATILE mean on a function?
Show answer
They tell the planner how a function behaves. IMMUTABLE: the same arguments always give the same result and the function reads no data, so the call can be evaluated once and can be used in an index. STABLE: the result does not change within a single statement, though it may read tables. VOLATILE, the default: the result may change on every call or the function has side effects, so it is re-evaluated each time.
Key Points to Remember
- PL/pgSQL is PostgreSQL's default procedural language for writing server-side functions.
- Dollar quoting ($$ ... $$) lets a function body span multiple lines without escaping quotes.
- Functions support DECLARE for local variables and BEGIN/END blocks with conditionals, loops, and embedded SQL.
- Functions are called like built-ins and can also back triggers, defaults, or computed columns.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs your SQL on SQLite in your browser. Standard SQL works; a few features that exist only in PostgreSQL or MySQL do not. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering