PL/SQL control structures let you direct execution flow, return data from functions and procedures, handle errors, and iterate over result sets and arrays. They are the foundation of any non-trivial PL/SQL program in PolarDB for PostgreSQL (Compatible with Oracle).
Return from a function
The RETURN and RETURN NEXT commands return data from a function.
RETURN
RETURN expression;RETURN expression terminates the function and passes the expression value to the caller. Use this form in functions that do not return a set.
Scalar return type: The expression is automatically converted to the declared return type.
Composite (row) return type: Write an expression that produces exactly the required columns. Explicit casting may be required.
Output parameters: Write
RETURNwithout an expression. The current values of the output parameter variables are returned.`void` return type: Write
RETURNwithout an expression to exit the function early.
A function must reach a RETURN statement before control exits its top-level block, or a runtime error occurs. Functions with output parameters and functions returning void are exempt: a RETURN is executed automatically when the top-level block ends.
-- Scalar return
RETURN 1 + 2;
RETURN scalar_var;
-- Composite return (cast columns to the correct types)
RETURN composite_type_var;
RETURN (1, 2, 'three'::text);RETURN NEXT and RETURN QUERY
RETURN NEXT expression;
RETURN QUERY query;
RETURN QUERY EXECUTE command-string [ USING expression [, ... ] ];Use these statements in functions declared to return SETOF sometype. Instead of returning immediately, each RETURN NEXT or RETURN QUERY appends rows to the result set. A final RETURN with no arguments signals that the function is done.
RETURN NEXTworks for scalar and composite data types. For composite types, a complete table of results is returned.RETURN QUERYappends the output of a query to the result set.Both statements can be mixed freely in a single function; their results are concatenated.
RETURN QUERY EXECUTEdynamically specifies the query. Insert parameter values withUSING, the same way as in theEXECUTEcommand.
For functions with output parameters, write RETURN NEXT without an expression. Each invocation saves the current output parameter values as one row of the result. To return a set with multiple output parameters, declare the function as RETURN SETOF record. For a single output parameter of type sometype, declare it as RETURN SETOF sometype.
The following example uses RETURN NEXT to return all rows from a table:
CREATE TABLE foo (fooid INT, foosubid INT, fooname TEXT);
INSERT INTO foo VALUES (1, 2, 'three');
INSERT INTO foo VALUES (4, 5, 'six');
CREATE OR REPLACE FUNCTION get_all_foo() RETURN SETOF foo IS
DECLARE
r foo%rowtype;
BEGIN
FOR r IN
SELECT * FROM foo WHERE fooid > 0
LOOP
-- Some processing can be done here.
RETURN NEXT r; -- Return the current row of the SELECT statement.
END LOOP;
RETURN;
END;
SELECT * FROM get_all_foo();The following example uses RETURN QUERY and raises an exception when no rows are found:
CREATE FUNCTION get_available_flightid(date) RETURN SETOF integer IS
BEGIN
RETURN QUERY SELECT flightid
FROM flight
WHERE flightdate >= $1
AND flightdate < ($1 + 1);
-- Check whether rows are returned.
-- If no rows are returned, throw an exception.
IF NOT FOUND THEN
RAISE EXCEPTION 'No flight at %.', $1;
END IF;
RETURN;
END;
-- Return available flights, or throw an exception if no flights are available.
SELECT * FROM get_available_flightid(CURRENT_DATE);The current implementations ofRETURN NEXTandRETURN QUERYaccumulate the entire result set in memory before the function returns. For large result sets, this can cause poor performance: data is written to disk to prevent memory exhaustion, but the function does not return until the entire result set is generated. The point at which data starts being written to disk is controlled by thework_memconfiguration variable. If you have enough memory to store large result sets, consider increasingwork_mem.
Return from a procedure
A procedure does not return a value and can end without a RETURN statement. To exit early, write RETURN without an expression.
If the procedure has output parameters, their final values are returned to the caller.
Call a procedure
Call a procedure from a PL/SQL function, procedure, or DO block using CALL. Each INOUT parameter must correspond to a variable in the CALL statement. The value the procedure assigns to each INOUT parameter is written back to the corresponding variable after the call returns.
CREATE PROCEDURE triple(INOUT x int)
IS
BEGIN
x := x * 3;
END;
DECLARE myvar int := 5;
BEGIN
CALL triple(myvar);
RAISE NOTICE 'myvar = %', myvar; -- prints 15
END;Conditionals
IF and CASE let you execute different code based on conditions.
PL/SQL supports three IF forms:
IF ... THEN ... END IFIF ... THEN ... ELSE ... END IFIF ... THEN ... ELSIF ... THEN ... ELSE ... END IF
PL/SQL supports two CASE forms:
CASE ... WHEN ... THEN ... ELSE ... END CASE(simple CASE)CASE WHEN ... THEN ... ELSE ... END CASE(searched CASE)
IF-THEN
IF boolean-expression THEN
statements
END IF;Executes statements only when the condition is true; otherwise skips them.
IF v_user_id <> 0 THEN
UPDATE users SET email = v_email WHERE user_id = v_user_id;
END IF;IF-THEN-ELSE
IF boolean-expression THEN
statements
ELSE
statements
END IF;Executes the first block when the condition is true, and the ELSE block when the condition is false or NULL.
IF parentid IS NULL OR parentid = ''
THEN
RETURN fullname;
ELSE
RETURN hp_true_filename(parentid) || '/' || fullname;
END IF;IF v_count > 0 THEN
INSERT INTO users_count (count) VALUES (v_count);
RETURN 't';
ELSE
RETURN 'f';
END IF;IF-THEN-ELSIF
IF boolean-expression THEN
statements
[ ELSIF boolean-expression THEN
statements
...
]
[ ELSE
statements ]
END IF;Checks conditions in order and executes the block for the first condition that is true, then skips the rest. If no condition is true, the ELSE block (if present) executes.
IF number = 0 THEN
result := 'zero';
ELSIF number > 0 THEN
result := 'positive';
ELSIF number < 0 THEN
result := 'negative';
ELSE
-- The only other possibility is that the number is null.
result := 'NULL';
END IF;ELSIF can also be spelled ELSEIF.
IF-THEN-ELSIF is cleaner than nested IF-THEN-ELSE when checking multiple conditions. Each nested IF requires a matching END IF, making the code harder to maintain:
-- Equivalent nested IF approach (less readable)
IF demo_row.sex = 'm' THEN
pretty_sex := 'man';
ELSE
IF demo_row.sex = 'f' THEN
pretty_sex := 'woman';
END IF;
END IF;Simple CASE
CASE search-expression
WHEN expression [, expression [ ... ]] THEN
statements
[ WHEN expression [, expression [ ... ]] THEN
statements
... ]
[ ELSE
statements ]
END CASE;Evaluates search-expression once and compares it to each WHEN expression in order. Executes the matching block, then passes control to the statement after END CASE. If no match is found and there is no ELSE clause, a CASE_NOT_FOUND exception is thrown.
CASE x
WHEN 1, 2 THEN
msg := 'one or two';
ELSE
msg := 'other value than one or two';
END CASE;Searched CASE
CASE
WHEN boolean-expression THEN
statements
[ WHEN boolean-expression THEN
statements
... ]
[ ELSE
statements ]
END CASE;Evaluates each WHEN boolean expression in order and executes the block for the first one that is true. If no expression is true and there is no ELSE clause, a CASE_NOT_FOUND exception is thrown. This form is equivalent to IF-THEN-ELSIF, with the key difference that omitting ELSE raises an exception rather than doing nothing.
CASE
WHEN x BETWEEN 0 AND 10 THEN
msg := 'value is between zero and ten';
WHEN x BETWEEN 11 AND 20 THEN
msg := 'value is between eleven and twenty';
END CASE;Simple loops
Use LOOP, EXIT, CONTINUE, WHILE, FOR, and FOREACH to repeat a sequence of statements.
LOOP
[ <<label>> ]
LOOP
statements
END LOOP [ label ];An unconditional loop that repeats indefinitely until terminated by EXIT or RETURN. Attach a label to let EXIT and CONTINUE in nested loops target a specific level.
EXIT
EXIT [ label ] [ WHEN boolean-expression ];Without a label: exits the innermost loop and continues after
END LOOP.With a label: exits the named loop or block and continues after its corresponding
END.With
WHEN: exits only when the boolean expression is true.
EXIT works in all loop types, including unconditional loops.
When used with a BEGIN block, EXIT requires a label. In recent PostgreSQL versions, an unlabeled EXIT does not match a BEGIN block (support for this was dropped after PostgreSQL 8.4).
LOOP
-- Some computations
IF count > 0 THEN
EXIT; -- Exit the loop.
END IF;
END LOOP;
LOOP
-- Some computations
EXIT WHEN count > 0; -- Same result as the previous example
END LOOP;
<<ablock>>
BEGIN
-- Some computations
IF stocks > 100000 THEN
EXIT ablock; -- Causes an exit from the BEGIN block
END IF;
-- If stocks > 100000, the computations here are skipped.
END;CONTINUE
CONTINUE [ label ] [ WHEN boolean-expression ];Without a label: skips the remaining loop body and starts the next iteration of the innermost loop.
With a label: starts the next iteration of the specified loop.
With
WHEN: continues only when the boolean expression is true.
CONTINUE works in all loop types, including unconditional loops.
LOOP
-- Some computations
EXIT WHEN count > 100;
CONTINUE WHEN count < 50;
-- Some computations for count IN [50 .. 100]
END LOOP;WHILE
[ <<label>> ]
WHILE boolean-expression LOOP
statements
END LOOP [ label ];Repeats the loop body as long as the boolean expression is true. The expression is evaluated before each iteration.
WHILE amount_owed > 0 AND gift_certificate_balance > 0 LOOP
-- Some computations
END LOOP;
WHILE NOT done LOOP
-- Some computations
END LOOP;FOR (integer variant)
[ <<label>> ]
FOR name IN [ REVERSE ] expression .. expression [ BY expression ] LOOP
statements
END LOOP [ label ];Iterates over a range of integers. The loop variable name is automatically typed as integer and exists only within the loop; any existing variable with the same name is hidden. Both bound expressions are evaluated once when the loop starts. The default step is 1; use BY to specify a different step. With REVERSE, the step is subtracted rather than added each iteration.
If the lower bound is greater than the upper bound (or less than in REVERSE), the loop body is not executed and no error is thrown.
FOR i IN 1..10 LOOP
-- Iterates over 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
END LOOP;
FOR i IN REVERSE 10..1 LOOP
-- Iterates over 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
END LOOP;
FOR i IN REVERSE 10..1 BY 2 LOOP
-- Iterates over 10, 8, 6, 4, 2
END LOOP;If a label is attached, the loop variable can be referenced with a qualified name using that label.
Loop through query results
[ <<label>> ]
FOR target IN query LOOP
statements
END LOOP [ label ];target is a record variable, a row variable, or a comma-separated list of scalar variables. On each iteration, target is assigned the next row from query. After the loop exits via EXIT, the last assigned row value remains accessible.
The query can be any SQL command that returns rows: SELECT (most common), INSERT/UPDATE/DELETE with a RETURNING clause, or utility commands such as EXPLAIN. PL/SQL variables are substituted as query parameters, and the query plan is cached for potential reuse.
The following example iterates over all materialized views and refreshes each one:
CREATE FUNCTION refresh_mviews() RETURN integer IS
DECLARE
mviews RECORD;
BEGIN
RAISE NOTICE 'Refreshing all materialized views...';
FOR mviews IN
SELECT n.nspname AS mv_schema,
c.relname AS mv_name,
pg_catalog.pg_get_userbyid(c.relowner) AS owner
FROM pg_catalog.pg_class c
LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
WHERE c.relkind = 'm'
ORDER BY 1
LOOP
-- "mviews" holds one record with information about the materialized view
RAISE NOTICE 'Refreshing materialized view %.% (owner: %)...',
quote_ident(mviews.mv_schema),
quote_ident(mviews.mv_name),
quote_ident(mviews.owner);
EXECUTE format('REFRESH MATERIALIZED VIEW %I.%I', mviews.mv_schema, mviews.mv_name);
END LOOP;
RAISE NOTICE 'Done refreshing materialized views.';
RETURN 1;
END;FOR-IN-EXECUTE (dynamic query loop)
[ <<label>> ]
FOR target IN EXECUTE text_expression [ USING expression [, ... ] ] LOOP
statements
END LOOP [ label ];Similar to the query loop above, but the source query is a string expression that is evaluated and replanned each time the loop is entered. This gives you the flexibility of a dynamic command while still allowing parameter injection via USING. As an alternative, declare the query as a cursor.
Loop through arrays
[ <<label>> ]
FOREACH target [ SLICE number ] IN ARRAY expression LOOP
statements
END LOOP [ label ];FOREACH iterates over the elements of an array, in storage order, regardless of the number of dimensions.
Without `SLICE` (or with
SLICE 0): iterates over individual elements. When looping through an array of composite values (records),targetcan be a list of variables; each variable is assigned from the corresponding column of the composite value.With `SLICE n`: iterates over *n*-dimensional slices instead of individual elements. The
SLICEvalue must be an integer constant no greater than the number of array dimensions, andtargetmust be an array variable.
The following example sums the elements of an integer array:
CREATE FUNCTION sum(int[]) RETURN int8 IS
DECLARE
s int8 := 0;
x int;
BEGIN
FOREACH x IN ARRAY $1
LOOP
s := s + x;
END LOOP;
RETURN s;
END;The following example iterates over one-dimensional slices of a two-dimensional array:
CREATE FUNCTION scan_rows(int[]) RETURN void IS
DECLARE
x int[];
BEGIN
FOREACH x SLICE 1 IN ARRAY $1
LOOP
RAISE NOTICE 'row = %', x;
END LOOP;
END;
SELECT scan_rows(ARRAY[[1,2,3],[4,5,6],[7,8,9],[10,11,12]]);
NOTICE: row = {1,2,3}
NOTICE: row = {4,5,6}
NOTICE: row = {7,8,9}
NOTICE: row = {10,11,12}Trap errors
By default, any error in a PL/SQL function aborts both the function and the surrounding transaction. Wrap statements in a BEGIN block with an EXCEPTION clause to catch and recover from errors.
[ <<label>> ]
[ DECLARE
declarations ]
BEGIN
statements
EXCEPTION
WHEN condition [ OR condition ... ] THEN
handler_statements
[ WHEN condition [ OR condition ... ] THEN
handler_statements
... ]
END;Error handling flow:
If no error occurs, all
statementsexecute and control passes to the next statement afterEND.If an error occurs, the remaining
statementsare abandoned and theEXCEPTIONlist is searched for the first matchingcondition.If a match is found, the corresponding
handler_statementsexecute, then control passes to the next statement afterEND.If no match is found, the error propagates as if the
EXCEPTIONclause were not there. An enclosing block with its ownEXCEPTIONclause can catch it; if none exists, the function aborts.
Condition names correspond to PostgreSQL error codes. A category name matches all errors in that category. The special condition OTHERS matches all error types except QUERY_CANCELED and ASSERT_FAILURE (those two can be trapped by name, but doing so is not recommended). Condition names are case-insensitive. You can also specify an error condition by its SQLSTATE code:
WHEN division_by_zero THEN ...
WHEN SQLSTATE '22012' THEN ...If a new error occurs within a handler, the current EXCEPTION clause cannot catch it. It propagates to the nearest enclosing EXCEPTION clause.
When an error is caught, local variables retain their values at the point of the error, but all database changes made within the block are rolled back.
INSERT INTO mytab(firstname, lastname) VALUES('Tom', 'Jones');
BEGIN
UPDATE mytab SET firstname = 'Joe' WHERE lastname = 'Jones';
x := x + 1;
y := x / 0;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'caught division_by_zero';
RETURN x;
END;In this example, the division_by_zero error is caught, and x (incremented) is returned. The UPDATE is rolled back, but the INSERT before the block is not — so the database ends up with Tom Jones but not Joe Jones.
Performance considerations
Entering and exiting a block with an EXCEPTION clause is significantly more expensive than a plain block. Use EXCEPTION only when you need it.
UPDATE and INSERT with error handling
The following example uses exception handling to implement an upsert: try UPDATE first, fall back to INSERT if the row does not exist, and retry if a concurrent insert causes a unique key conflict.
For new code, use INSERT ... ON CONFLICT DO UPDATE instead of this pattern. The example below is intended to illustrate PL/SQL control flow.CREATE TABLE db (a INT PRIMARY KEY, b TEXT);
CREATE FUNCTION merge_db(key INT, data TEXT) RETURN VOID IS
BEGIN
LOOP
-- First, try to update the key.
UPDATE db SET b = data WHERE a = key;
IF found THEN
RETURN;
END IF;
-- The key does not exist. Try to insert it.
-- A concurrent insert of the same key may cause a unique key failure.
BEGIN
INSERT INTO db(a,b) VALUES (key, data);
RETURN;
EXCEPTION WHEN unique_violation THEN
-- Do nothing and loop to try the UPDATE again.
END;
END LOOP;
END;
SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');This code assumes the unique_violation error comes from the INSERT, not from a trigger. If the table has more than one unique index, the loop retries regardless of which index caused the error. Use the diagnostics features below to verify that the caught error is the one you expect.
Get information about an error
Within an exception handler, two special variables are available:
SQLSTATE: the error code of the current exceptionSQLERRM: the error message of the current exception
Both variables are undefined outside the handler.
For more detail, use GET STACKED DIAGNOSTICS:
GET STACKED DIAGNOSTICS variable { = | := } item [ , ... ];Each item is a keyword identifying a status value, assigned to variable of the appropriate data type. Available items:
| Item | Type | Description |
|---|---|---|
RETURNED_SQLSTATE | text | The SQLSTATE error code of the exception |
COLUMN_NAME | text | The name of the column related to the exception |
CONSTRAINT_NAME | text | The name of the constraint related to the exception |
PG_DATATYPE_NAME | text | The name of the data type related to the exception |
MESSAGE_TEXT | text | The primary message text of the exception |
TABLE_NAME | text | The name of the table related to the exception |
SCHEMA_NAME | text | The name of the schema related to the exception |
PG_EXCEPTION_DETAIL | text | The detail message of the exception, if any |
PG_EXCEPTION_HINT | text | The hint message of the exception, if any |
PG_EXCEPTION_CONTEXT | text | The call stack at the time of the exception |
If a diagnostics item has no value, an empty string is returned.
DECLARE
text_var1 text;
text_var2 text;
text_var3 text;
BEGIN
-- Some processing that might cause an exception
...
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT,
text_var2 = PG_EXCEPTION_DETAIL,
text_var3 = PG_EXCEPTION_HINT;
END;Obtain execution location information
GET DIAGNOSTICS retrieves information about the current execution state. GET STACKED DIAGNOSTICS reports the state at the time of the most recent error.
Use the PG_CONTEXT status item to get the current call stack as a multi-line text string. The first line identifies the current function and the GET DIAGNOSTICS statement being executed; subsequent lines list the calling functions higher up in the call stack.
CREATE OR REPLACE FUNCTION outer_func() RETURN integer IS
BEGIN
RETURN inner_func();
END;
CREATE OR REPLACE FUNCTION inner_func() RETURN integer IS
DECLARE
stack text;
BEGIN
GET DIAGNOSTICS stack = PG_CONTEXT;
RAISE NOTICE E'--- Call Stack ---\n%', stack;
RETURN 1;
END;
SELECT outer_func();
NOTICE: --- Call Stack ---
PL/SQL function inner_func() line 5 at GET DIAGNOSTICS
PL/SQL function outer_func() line 3 at RETURN
CONTEXT: PL/SQL function outer_func() line 3 at RETURN
outer_func
------------
1
(1 row)GET STACKED DIAGNOSTICS ... PG_EXCEPTION_CONTEXT returns a similar call stack, but it describes where the error was detected rather than the current execution point.