PL/pgSQL Functions in version 7.0
PL/pgSQL (Procedural Language/PostgreSQL) is the default procedural language supported by AnalyticDB for PostgreSQL. The PL/pgSQL implementation in AnalyticDB for PostgreSQL is based on PostgreSQL 12. You can use PL/pgSQL to create user-defined functions (UDFs) for complex data processing and business logic.
Overview
PL/pgSQL is a block-structured imperative language designed for database programming. You can use PL/pgSQL to:
Create functions and stored procedures.
Implement complex business rules and data validation logic.
Encapsulate data operations to simplify application code.
Extend SQL with variables, conditional branching, loops, and exception handling.
Use cases
Scenario | Description |
Data validation | Implement complex business rule checks. |
Data transformation | Define custom transformation logic for ETL processes. |
Business encapsulation | Wrap multi-step operations into functions to simplify application-layer calls. |
Cursor processing | Process large datasets row by row. |
Limits
The PL/pgSQL implementation in AnalyticDB for PostgreSQL differs from standard PostgreSQL in the following ways:
Limit | Description |
No trigger support | You cannot create triggers by using PL/pgSQL. |
Forward-only cursors | Cursors are not scrollable and can only traverse forward. |
No updatable cursors | The |
No parallel retrieve cursors | The |
No SELECT INTO in EXECUTE | You cannot use |
Language structure
Block structure
PL/pgSQL code is organized into blocks. Each block contains a declaration section, a statement section, and an optional exception-handling section. The syntax is as follows:
[ <<label>> ]
[ DECLARE
-- Declaration section
declarations ]
BEGIN
-- Statement section
statements
[ EXCEPTION
-- Exception-handling section
WHEN condition THEN
handler_statements ]
END [ label ];Example: Basic block structure
CREATE FUNCTION simple_example() RETURNS integer AS $$
DECLARE
counter integer := 0;
BEGIN
counter := counter + 1;
RETURN counter;
END;
$$ LANGUAGE plpgsql;Example: Nested blocks
Blocks can be nested. Inner blocks can access variables from outer blocks.
CREATE FUNCTION nested_blocks() RETURNS void AS $$
<<outer_block>>
DECLARE
outer_var integer := 1;
BEGIN
RAISE NOTICE 'outer_var = %', outer_var;
<<inner_block>>
DECLARE
inner_var integer := 2;
BEGIN
RAISE NOTICE 'inner_var = %', inner_var;
RAISE NOTICE 'outer_var from inner = %', outer_var;
END inner_block;
END outer_block;
$$ LANGUAGE plpgsql;Labels (<<label>>) serve the following purposes:
Specify target blocks or loops in
EXITandCONTINUEstatements.Disambiguate variables with the same name by using the
label.variablesyntax.
Comments
PL/pgSQL supports two comment styles:
-- Single-line comment
x := 1; -- End-of-line comment
/*
* Multi-line comment
* Can span multiple lines
*/
x := 1;Declarations and data types
Variable declarations
Declare variables in the DECLARE section by using the following syntax:
name [ CONSTANT ] type [ NOT NULL ] [ { DEFAULT | := } expression ];Examples
DECLARE
user_id integer; -- Basic declaration
quantity integer := 10; -- With default value
tax_rate numeric(5,2) DEFAULT 0.08; -- Using DEFAULT keyword
PI CONSTANT numeric := 3.14159265; -- Constant
counter integer NOT NULL := 0; -- NOT NULL constraint (default value required)
greeting text := 'Hello, World!'; -- String variableData types
PL/pgSQL supports all PostgreSQL base data types and provides the following specialized types:
%TYPE: Reference a column type. Use
%TYPEto reference the data type of a table column. The function automatically adapts when the table structure changes.DECLARE emp_name employees.name%TYPE; emp_salary employees.salary%TYPE;%ROWTYPE: Reference a row type. Use
%ROWTYPEto declare a record variable that matches a table structure.DECLARE emp_row employees%ROWTYPE; BEGIN SELECT * INTO emp_row FROM employees WHERE id = 1; RAISE NOTICE 'Employee: %, Salary: %', emp_row.name, emp_row.salary; END;RECORD: Dynamic record type. A
RECORDvariable has its structure determined at runtime, which is suitable for dynamic query scenarios.DECLARE rec RECORD; BEGIN FOR rec IN SELECT id, name FROM employees LOOP RAISE NOTICE 'ID: %, Name: %', rec.id, rec.name; END LOOP; END;Array types: One-dimensional and multi-dimensional arrays are supported.
DECLARE int_array integer[]; str_array text[] := ARRAY['a', 'b', 'c']; numbers integer[] := '{1, 2, 3, 4, 5}'; BEGIN RAISE NOTICE 'First element: %', str_array[1]; -- Index starts at 1 int_array := int_array || 10; -- Append element RAISE NOTICE 'Array length: %', array_length(str_array, 1); END;Composite types: Custom structured types are supported.
-- Create a custom type CREATE TYPE person_type AS ( name text, age integer, email text ); -- Use in a function DECLARE person person_type; BEGIN person.name := 'John'; person.age := 30; person.email := 'john@example.com'; END;
Function parameters
Function parameters can be referenced in the following ways:
-- Method 1: Named parameters (recommended)
CREATE FUNCTION add_nums(a integer, b integer) RETURNS integer AS $$
BEGIN
RETURN a + b;
END;
$$ LANGUAGE plpgsql;
-- Method 2: Positional parameters
CREATE FUNCTION add_nums(integer, integer) RETURNS integer AS $$
BEGIN
RETURN $1 + $2;
END;
$$ LANGUAGE plpgsql;
-- Method 3: ALIAS FOR
CREATE FUNCTION add_nums(integer, integer) RETURNS integer AS $$
DECLARE
first_num ALIAS FOR $1;
second_num ALIAS FOR $2;
BEGIN
RETURN first_num + second_num;
END;
$$ LANGUAGE plpgsql;Expressions and assignments
Assignments
PL/pgSQL supports the following assignment methods:
:= assignment operator: Assign a value directly to a variable.
DECLARE x integer; y integer; total integer; BEGIN x := 10; y := 20; total := x + y; END;SELECT INTO assignment: Assign query results to variables by using
SELECT INTO.DECLARE emp_name text; emp_salary numeric; emp_count integer; BEGIN -- Single-value assignment SELECT name INTO emp_name FROM employees WHERE id = 1; -- Multi-value assignment SELECT name, salary INTO emp_name, emp_salary FROM employees WHERE id = 1; -- Aggregate result assignment SELECT count(*) INTO emp_count FROM employees; -- Strict mode: requires exactly one row, otherwise raises an exception SELECT name INTO STRICT emp_name FROM employees WHERE id = 1; END;FOUND variable:
FOUNDis a Boolean variable that indicates whether the last SQL statement affected any rows.DECLARE emp_name text; BEGIN SELECT name INTO emp_name FROM employees WHERE id = 999; IF NOT FOUND THEN RAISE NOTICE 'Employee not found'; ELSE RAISE NOTICE 'Found: %', emp_name; END IF; END;
Expression evaluation
Expressions in PL/pgSQL use standard SQL expression syntax and support mathematical operations, string operations, conditional evaluation, and SQL function calls.
DECLARE
result numeric;
condition boolean;
BEGIN
-- Mathematical expression
result := 10 + 20 * 3;
-- String expression
result := length('Hello') + length('World');
-- Conditional expression
condition := (result > 100) AND (result < 200);
-- SQL function call
result := abs(-50) + round(3.7);
END;Expression caching (plan caching)
PL/pgSQL automatically caches execution plans for SQL statements to improve repeated execution efficiency. Be aware of the following considerations:
-- A cached plan may not be optimal when parameter values vary widely
CREATE FUNCTION get_employee(emp_id integer) RETURNS text AS $$
DECLARE
result text;
BEGIN
-- This query plan is cached (may not be optimal if emp_id varies widely)
SELECT name INTO result FROM employees WHERE id = emp_id;
RETURN result;
END;
$$ LANGUAGE plpgsql;The volatility classification of a function (IMMUTABLE/STABLE/VOLATILE) affects how the database caches query plans that reference the function.
IMMUTABLE: A pure function that always returns the same output for the same input. The optimizer can evaluate the result at planning time (constant folding). Does not depend on database content, configuration, or time. Use cases: mathematical calculations, string formatting, fixed-rule conversions.
CREATE FUNCTION celsius_to_fahrenheit(c numeric) RETURNS numeric AS $$ BEGIN RETURN c * 9.0 / 5.0 + 32; END; $$ LANGUAGE plpgsql IMMUTABLE;STABLE: Returns the same result within a single table scan. Can read the database, but the result does not change within the same SQL statement execution. The optimizer can cache the result within a single scan to avoid repeated calls. Can be used in index conditions (WHERE clause of Index Scan). Use cases: reading configuration tables, queries based on the current transaction snapshot, time zone conversions.
CREATE FUNCTION get_config_value(key text) RETURNS text AS $$ DECLARE val text; BEGIN SELECT value INTO val FROM config_table WHERE config_key = key; RETURN val; END; $$ LANGUAGE plpgsql STABLE;VOLATILE: May return different results on each call. Can modify database state (INSERT/UPDATE/DELETE). The optimizer does not cache or optimize away any calls. Cannot be used in index conditions. This is the default. Use cases: data modifications, operations that depend on random numbers or current time, operations with side effects.
CREATE FUNCTION log_access(user_id integer) RETURNS void AS $$ BEGIN INSERT INTO access_log VALUES (user_id, now()); END; $$ LANGUAGE plpgsql VOLATILE;
Basic statements
PERFORM
PERFORM executes a query when you do not need the result. The syntax is the same as SELECT, but the PERFORM keyword replaces SELECT.
BEGIN
-- Call a function
PERFORM my_procedure(10);
-- Execute an operation with side effects
PERFORM pg_sleep(1);
-- Check whether data exists
PERFORM 1 FROM employees WHERE id = 100;
IF FOUND THEN
RAISE NOTICE 'Employee exists';
END IF;
END;In PL/pgSQL, a SELECT statement without an INTO clause causes a syntax error. Use PERFORM instead when you do not need the query result.
EXECUTE
EXECUTE runs dynamically constructed SQL statements. Use EXECUTE when table names, column names, or query conditions must be determined at runtime.
Pass parameters with USING: Use the
USINGclause to pass parameter values safely.DECLARE min_salary numeric := 50000; dept_id integer := 10; emp_count integer; BEGIN EXECUTE 'SELECT count(*) FROM employees WHERE salary > $1 AND department_id = $2' INTO emp_count USING min_salary, dept_id; RAISE NOTICE 'Count: %', emp_count; END;
In AnalyticDB for PostgreSQL, the EXECUTE statement does not support SELECT INTO syntax. Use EXECUTE ... INTO ... syntax instead.
Prevent SQL injection: When you dynamically construct SQL, you must escape user input.
-- Bad: Direct concatenation of user input. This is a SQL injection risk. EXECUTE 'SELECT * FROM ' || user_input; -- Good: Use quote_ident for identifiers and quote_literal for values EXECUTE 'SELECT * FROM ' || quote_ident(table_name) || ' WHERE name = ' || quote_literal(user_name); -- Recommended: Use the format function (%I = identifier, %L = literal) EXECUTE format('SELECT * FROM %I WHERE name = %L', table_name, user_name); -- Recommended: Use USING to pass parameter values EXECUTE 'SELECT * FROM employees WHERE name = $1' USING user_name;
RETURN
PL/pgSQL functions support the following return methods:
Return a single value: Use
RETURNto return a scalar value.CREATE FUNCTION get_sum(a integer, b integer) RETURNS integer AS $$ BEGIN RETURN a + b; END; $$ LANGUAGE plpgsql;RETURN NEXT and RETURN QUERY: Return a result set.
-- RETURN NEXT: Return rows one at a time CREATE FUNCTION get_numbers(n integer) RETURNS SETOF integer AS $$ DECLARE i integer; BEGIN FOR i IN 1..n LOOP RETURN NEXT i; END LOOP; RETURN; END; $$ LANGUAGE plpgsql; -- RETURN QUERY: Return a query result set CREATE FUNCTION get_employees(dept_id integer) RETURNS SETOF employees AS $$ BEGIN RETURN QUERY SELECT * FROM employees WHERE department_id = dept_id; RETURN; END; $$ LANGUAGE plpgsql; -- RETURN QUERY EXECUTE: Return a dynamic query result set CREATE FUNCTION get_by_table(tbl text) RETURNS SETOF RECORD AS $$ BEGIN RETURN QUERY EXECUTE 'SELECT * FROM ' || quote_ident(tbl); END; $$ LANGUAGE plpgsql;
RETURN NEXT and RETURN QUERY buffer result sets in memory (limited by work_mem, with overflow written to disk). Monitor memory usage when you process large result sets.
RAISE
RAISE outputs debugging information or raises exceptions.
Message levels:
RAISEsupports the following message levels.Level
Description
DEBUG
Debug information, not displayed by default.
LOG
Log information.
INFO
General information.
NOTICE
Notice information, displayed by default.
WARNING
Warning information.
EXCEPTION
Raises an exception, interrupts execution, and rolls back the current transaction.
Format messages: Use
%as a placeholder. Placeholders are replaced by the subsequent arguments in order.DECLARE user_name text := 'John'; user_age integer := 30; BEGIN RAISE NOTICE 'User % is % years old', user_name, user_age; -- Output: NOTICE: User John is 30 years old RAISE NOTICE 'Completion: %%100'; -- Use %% to output a percent sign END;Raise custom exceptions: Use the
USINGclause to specify error details.BEGIN RAISE EXCEPTION 'Invalid input' USING ERRCODE = 'invalid_parameter_value', DETAIL = 'The value must be positive', HINT = 'Provide a positive integer'; END;
Control structures
Conditional statements
PL/pgSQL supports the following conditional statements:
IF statements: Support
IF...THEN...ELSIF...ELSE...END IFstructure.DECLARE grade integer := 85; result text; BEGIN IF grade >= 90 THEN result := 'A'; ELSIF grade >= 80 THEN result := 'B'; ELSIF grade >= 70 THEN result := 'C'; ELSIF grade >= 60 THEN result := 'D'; ELSE result := 'F'; END IF; RAISE NOTICE 'Grade: %', result; END;CASE expressions: Support simple CASE and searched CASE forms.
-- Simple CASE DECLARE status integer := 1; status_text text; BEGIN status_text := CASE status WHEN 0 THEN 'Inactive' WHEN 1 THEN 'Active' WHEN 2 THEN 'Pending' ELSE 'Unknown' END; END; -- Searched CASE DECLARE age integer := 25; category text; BEGIN category := CASE WHEN age < 13 THEN 'Child' WHEN age < 20 THEN 'Teenager' WHEN age < 60 THEN 'Adult' ELSE 'Senior' END; END;
Loop statements
PL/pgSQL supports the following loop statements:
LOOP: An infinite loop that requires
EXITto terminate.DECLARE counter integer := 0; BEGIN LOOP counter := counter + 1; RAISE NOTICE 'Counter: %', counter; EXIT WHEN counter >= 5; END LOOP; END;WHILE loop: Loops while the condition is true.
DECLARE counter integer := 1; BEGIN WHILE counter <= 5 LOOP RAISE NOTICE 'Counter: %', counter; counter := counter + 1; END LOOP; END;FOR loop (integer range): Iterates over an integer range. Supports forward, reverse, and custom step values.
BEGIN -- Forward iteration FOR i IN 1..5 LOOP RAISE NOTICE 'i = %', i; END LOOP; -- Reverse iteration FOR i IN REVERSE 5..1 LOOP RAISE NOTICE 'i = %', i; END LOOP; -- Custom step FOR i IN 1..10 BY 2 LOOP RAISE NOTICE 'i = %', i; -- Output: 1, 3, 5, 7, 9 END LOOP; END;FOR loop (query results): Iterates over each row returned by a query.
DECLARE rec RECORD; BEGIN FOR rec IN SELECT id, name FROM employees ORDER BY id LOOP RAISE NOTICE 'ID: %, Name: %', rec.id, rec.name; END LOOP; -- Iterate over dynamic query results FOR rec IN EXECUTE 'SELECT * FROM employees WHERE salary > $1' USING 50000 LOOP RAISE NOTICE 'High earner: %', rec.name; END LOOP; END;FOREACH (iterate over arrays): Iterates over each element of an array.
DECLARE arr integer[] := ARRAY[1, 2, 3, 4, 5]; elem integer; BEGIN FOREACH elem IN ARRAY arr LOOP RAISE NOTICE 'Element: %', elem; END LOOP; END;EXIT and CONTINUE:
EXITterminates the current loop or a loop specified by a label.CONTINUEskips the current iteration and proceeds to the next.BEGIN <<outer_loop>> FOR i IN 1..10 LOOP FOR j IN 1..10 LOOP IF j = 5 THEN CONTINUE; -- Skip current inner iteration END IF; IF i * j > 30 THEN EXIT outer_loop; -- Exit outer loop END IF; RAISE NOTICE '% * % = %', i, j, i * j; END LOOP; END LOOP outer_loop; END;
Cursors
Cursors process query results row by row. They are suitable for scenarios that require step-by-step processing of large datasets.
Declare cursors
DECLARE
-- Bound cursor: query is bound at compile time
emp_cursor CURSOR FOR
SELECT id, name, salary FROM employees ORDER BY id;
-- Unbound cursor
ref_cursor refcursor;
-- Parameterized cursor
emp_cursor2 CURSOR (min_sal numeric) FOR
SELECT * FROM employees WHERE salary >= min_sal;Cursor operations
Cursors support the following operations:
Open a cursor: Use different methods depending on the cursor type.
DECLARE emp_cursor CURSOR FOR SELECT * FROM employees; ref_cursor refcursor; BEGIN -- Open a bound cursor OPEN emp_cursor; -- Open an unbound cursor OPEN ref_cursor FOR SELECT * FROM departments; -- Open a parameterized cursor OPEN emp_cursor2(50000); -- Open a dynamic cursor OPEN ref_cursor FOR EXECUTE 'SELECT * FROM ' || quote_ident(table_name); END;FETCH: Retrieve data: Read data from a cursor one row at a time.
DECLARE emp_cursor CURSOR FOR SELECT id, name FROM employees; emp_id integer; emp_name text; BEGIN OPEN emp_cursor; LOOP FETCH emp_cursor INTO emp_id, emp_name; EXIT WHEN NOT FOUND; RAISE NOTICE 'ID: %, Name: %', emp_id, emp_name; END LOOP; CLOSE emp_cursor; END;FOR IN cursor: Automatically manages the cursor lifecycle. No manual open or close operations are required.
DECLARE emp_cursor CURSOR FOR SELECT * FROM employees; rec RECORD; BEGIN FOR rec IN emp_cursor LOOP RAISE NOTICE 'Employee: %', rec.name; END LOOP; -- The cursor is opened and closed automatically END;
Cursors and performance
Cursors fetch data row by row on demand without loading all results into memory at once.
Cursors that remain open during a transaction consume database resources.
In long-running transactions, unclosed cursors may prevent VACUUM from cleaning up expired data.
Error handling
EXCEPTION blocks
Use the EXCEPTION clause within a BEGIN...END block to catch and handle exceptions.
CREATE FUNCTION safe_divide(a numeric, b numeric) RETURNS numeric AS $$
DECLARE
result numeric;
BEGIN
BEGIN
result := a / b;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Division by zero, returning NULL';
result := NULL;
WHEN numeric_value_out_of_range THEN
RAISE NOTICE 'Result out of range';
result := NULL;
END;
RETURN result;
END;
$$ LANGUAGE plpgsql;Common error conditions
Condition name | SQLSTATE | Description |
division_by_zero | 22012 | Division by zero. |
null_value_not_allowed | 22004 | Null value is not allowed. |
numeric_value_out_of_range | 22003 | Numeric value is out of range. |
string_data_right_truncation | 22001 | String truncation. |
unique_violation | 23505 | Unique constraint violation. |
foreign_key_violation | 23503 | Foreign key constraint violation. |
check_violation | 23514 | CHECK constraint violation. |
not_null_violation | 23502 | NOT NULL constraint violation. |
no_data_found | P0002 | SELECT INTO returned no data. |
too_many_rows | P0003 | SELECT INTO returned more than one row. |
OTHERS | - | Matches all exceptions not explicitly caught. |
Retrieve error information
Use the SQLERRM and SQLSTATE variables to obtain error details.
BEGIN
INSERT INTO unique_table VALUES (duplicate_key);
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Error: % (SQLSTATE: %)', SQLERRM, SQLSTATE;
IF SQLSTATE = '23505' THEN
RAISE NOTICE 'Duplicate key error';
END IF;
END;Multiple exception handlers
BEGIN
-- Business operations
EXCEPTION
WHEN unique_violation THEN
RAISE NOTICE 'Record already exists';
WHEN foreign_key_violation THEN
RAISE NOTICE 'Referenced record not found';
WHEN check_violation OR not_null_violation THEN
RAISE NOTICE 'Data validation failed';
WHEN OTHERS THEN
RAISE NOTICE 'Unexpected error: %', SQLERRM;
RAISE; -- Re-raise the exception
END;Subtransaction mechanism of EXCEPTION blocks
Each time a BEGIN block that contains an EXCEPTION clause is entered, PL/pgSQL creates a subtransaction.
Subtransaction creation is independent of the function volatility classification (IMMUTABLE/STABLE/VOLATILE). Even if a function is declared as IMMUTABLE, EXCEPTION blocks still create subtransactions. In high-frequency call scenarios, subtransactions consume significant vmem resources and degrade execution performance.
Performance optimization
Plan caching
PL/pgSQL automatically caches execution plans for SQL statements. A plan is generated and cached on the first execution, and subsequent calls reuse the cached plan.
CREATE FUNCTION get_employee(emp_id integer) RETURNS text AS $$
DECLARE
emp_name text;
BEGIN
-- This query plan is cached and reused for subsequent calls
SELECT name INTO emp_name FROM employees WHERE id = emp_id;
RETURN emp_name;
END;
$$ LANGUAGE plpgsql;When parameter value distributions are skewed, the cached generic plan may not be optimal. In such cases, use EXECUTE to force plan regeneration on each call.
CREATE FUNCTION search_employees(status text) RETURNS SETOF employees AS $$
BEGIN
-- status='active' accounts for 99% of data. The cached plan may not be optimal for status='inactive'.
-- Use EXECUTE to re-plan on each call
RETURN QUERY EXECUTE
'SELECT * FROM employees WHERE status = $1'
USING status;
END;
$$ LANGUAGE plpgsql;Method | Plan cached | Use cases |
Static SQL | Yes | Scenarios where parameter value distributions are uniform. |
EXECUTE + USING | No | Scenarios where parameter value distributions are skewed. |
EXECUTE with concatenation | No | Scenarios with dynamic table or column names. |
Minimize EXCEPTION block invocations
Functions that contain EXCEPTION blocks create a subtransaction on each call, which consumes vmem resources. Avoid calling such functions at high frequency in large-data scenarios.
You can check the current optimizer by running the following command:
SHOW optimizer;Use EXPLAIN to verify the actual predicate execution order:
EXPLAIN SELECT * FROM large_table
WHERE cheap_condition
AND expensive_plpgsql_function(column);The ORCA optimizer in AnalyticDB for PostgreSQL may not automatically reorder predicates in the WHERE clause. When you write SQL, place low-cost filter conditions before expensive ones.
-- Bad: Call a function with EXCEPTION blocks row by row on millions of rows
SELECT * FROM million_rows WHERE plpgsql_func(col);
-- Good: Filter with low-cost conditions first to reduce function calls
SELECT * FROM large_table
WHERE cheap_condition
AND expensive_plpgsql_function(column);Memory management
RETURN NEXT and RETURN QUERY buffer result sets in memory. When the buffer exceeds the work_mem limit, it is written to disk. For large result sets, use RETURN QUERY.
-- Recommended: Use RETURN QUERY for large result sets
CREATE FUNCTION generate_rows() RETURNS SETOF integer AS $$
BEGIN
RETURN QUERY SELECT generate_series(1, 1000000);
END;
$$ LANGUAGE plpgsql;For scenarios that require row-by-row processing of large datasets, use cursors with batch processing to release resources periodically:
CREATE FUNCTION process_large_result() RETURNS void AS $$
DECLARE
cur CURSOR FOR SELECT * FROM large_table;
rec RECORD;
batch_size integer := 1000;
counter integer := 0;
BEGIN
FOR rec IN cur LOOP
-- Process a single row
PERFORM process_row(rec.id);
counter := counter + 1;
IF counter % batch_size = 0 THEN
-- Release resources periodically
RAISE NOTICE 'Processed % rows', counter;
END IF;
END LOOP;
END;
$$ LANGUAGE plpgsql;Batch operations
The performance of PL/pgSQL loops for row-by-row processing is significantly lower than SQL set-based operations. Use native SQL set-based operations instead of loops whenever possible.
-- Bad: Update rows one at a time (slow)
CREATE FUNCTION update_salaries_slow() RETURNS void AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT id, salary FROM employees LOOP
UPDATE employees SET salary = rec.salary * 1.1 WHERE id = rec.id;
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- Good: Set-based operation (fast)
CREATE FUNCTION update_salaries_fast() RETURNS void AS $$
BEGIN
UPDATE employees SET salary = salary * 1.1;
END;
$$ LANGUAGE plpgsql;
-- Or use SQL directly without PL/pgSQL
UPDATE employees SET salary = salary * 1.1;Use PERFORM to execute batch DML operations:
BEGIN
-- Batch delete
PERFORM 1 FROM delete_old_records();
-- Batch insert
PERFORM insert_batch_data(source_table, target_table);
END;Alternatives to avoid row-by-row processing:
-- Bad: Row-by-row insertion
FOR i IN 1..1000 LOOP
INSERT INTO target VALUES (i, 'value' || i);
END LOOP;
-- Good: Batch insertion
INSERT INTO target
SELECT i, 'value' || i FROM generate_series(1, 1000) i;
-- Or use COPY
COPY target FROM '/path/to/data.csv';Debugging and diagnostics
Debug with RAISE NOTICE
CREATE FUNCTION debug_example(input integer) RETURNS integer AS $$
DECLARE
step1_result integer;
step2_result integer;
BEGIN
RAISE NOTICE 'Function called with input: %', input;
step1_result := input * 2;
RAISE NOTICE 'After step 1: %', step1_result;
step2_result := step1_result + 10;
RAISE NOTICE 'After step 2: %', step2_result;
RETURN step2_result;
END;
$$ LANGUAGE plpgsql;Handle variable name conflicts
When a PL/pgSQL variable has the same name as a SQL column, use the plpgsql.variable_conflict parameter to control priority.
SET plpgsql.variable_conflict = 'error'; -- Raise an error (default)
SET plpgsql.variable_conflict = 'use_variable'; -- Prioritize the variable
SET plpgsql.variable_conflict = 'use_column'; -- Prioritize the columnConfigure log levels
SET log_min_messages = 'debug1'; -- Set server log level
SET client_min_messages = 'notice'; -- Set client message levelFAQ
Common causes of slow function execution
Cause
Solution
EXCEPTION block subtransaction accumulation
Reduce the number of calls to functions that contain EXCEPTION blocks in high-frequency scenarios.
Oversized RETURN NEXT result sets
Use RETURN QUERY instead, or process data in batches with cursors.
Unclosed cursors
Holding a large number of cursors open for a long time.
Row-by-row data processing
Use SQL set-based operations (UPDATE, INSERT...SELECT).
Variables holding large data
Avoid storing large JSON or TEXT data in variables.
Can IMMUTABLE/STABLE prevent subtransactions in EXCEPTION blocks?
No. The volatility classification of a function affects only optimizer behavior and does not prevent subtransaction creation in EXCEPTION blocks. The following function still creates a subtransaction on each call:
CREATE FUNCTION my_func(text) RETURNS boolean AS $$ BEGIN PERFORM $1::json; RETURN true; EXCEPTION WHEN OTHERS THEN RETURN false; END; $$ LANGUAGE plpgsql IMMUTABLE; -- IMMUTABLE does not prevent subtransactionsHow do I choose between PL/pgSQL, PL/Python, and C functions?
Scenario
Recommendation
Reason
Simple data processing
PL/pgSQL
Simple syntax with good SQL integration.
Complex string or JSON processing
PL/Python
Rich Python library ecosystem.
High-frequency calls (millions per query)
C functions
No interpreter overhead.
Error handling with high-frequency calls
C functions
No subtransaction overhead.
Network or file I/O required
PL/Python
Supports system library calls.
Math or scientific computing
PL/Python + NumPy
Specialized computing library support.
Extreme performance requirements
C functions
Maximum performance.