PL/pgSQL Introduction
Comprehensive introduction to PostgreSQL's PL/pgSQL — what it is, why use it (round-trip avoidance), trade-offs, the BEGIN/END block structure, the DO statement for anonymous blocks, dollar-quoting variants, RAISE NOTICE for output, identifier rules including the 63-char limit, and how it compares to other procedural languages.
PL/pgSQL ("Procedural Language / PostgreSQL") is the procedural language built into PostgreSQL. It extends plain SQL with control structures — variables, IF/CASE branches, LOOP iterations, exception handling — so you can package multi-statement logic that lives and runs inside the database server.
PL/pgSQL is the language you reach for when you want to:
- Group a sequence of SQL statements into one named, reusable function or procedure
- Add control flow that pure SQL doesn't have
- Write triggers — code that fires automatically on INSERT/UPDATE/DELETE
- Run ad-hoc procedural blocks via
DO(no function definition required)
From PostgreSQL 9.0 onwards, PL/pgSQL is installed by default — you don't need to CREATE EXTENSION for it. Functions, procedures, and triggers written in PL/pgSQL are first-class citizens, callable from anywhere a built-in SQL function would be.
Plain SQL is great at querying — but it has no loops, no real conditionals, no error handling. For anything more than a single statement, you'd otherwise have to round-trip between your application and the database for every step. That's slow and fragile:
| Without PL/pgSQL (multiple round trips) | With PL/pgSQL (one server-side block) |
|---|---|
| App sends query 1 → server processes → returns result | App sends one CALL or SELECT fn() |
| App processes result → sends query 2 → ... | All logic runs server-side in a single context |
| N queries = N round trips of network + parsing latency | Single round trip, single parse, intermediate values stay on the server |
The benefits compound:
- Reduced network traffic — only the final result crosses the wire
- No client-side state for intermediate values — they live and die inside the function
- Parse caching — the planner remembers the function's query plans
- Centralized business logic — one canonical implementation, reused across applications
PL/pgSQL isn't free of cost. Practical drawbacks worth weighing:
- Specialized skill set. Application developers know JavaScript or Python; fewer know PL/pgSQL well. Code review, debugging, and maintenance become harder when the procedural logic lives in the database.
- Version control is awkward. Function bodies live in the database, not in your Git repo by default — you need migration scripts to track changes properly.
- Debugging is limited. No native step debugger; you debug with
RAISE NOTICEand patience. Third-party debuggers exist but require extra setup. - Vendor lock-in. PL/pgSQL is PostgreSQL-only. Moving to MySQL or SQL Server means a rewrite — they each have their own incompatible procedural dialects.
- Fragmented logic. Some logic lives in your app, some in the database. New team members have to look in two places to understand what runs where.
Every piece of PL/pgSQL code is organized into blocks. A block has up to three sections — only the body is mandatory:
| Section | Purpose | Required? |
|---|---|---|
DECLARE | Variables, constants, cursors, exceptions, record types | Optional |
BEGIN ... END | The executable statements — the body of the block | Required |
EXCEPTION | Catches errors raised inside the body and handles them | Optional |
The body must contain at least one statement — a bare NULL; works as a placeholder if nothing else is needed.
The DO statement runs an anonymous block — PL/pgSQL code executed once, with no function definition required:
The $$ markers are dollar-quoting — PostgreSQL's mechanism for embedding code that contains single quotes (almost certain in any non-trivial block) without escaping every one of them. Anything between the opening and closing dollar-quote is a single string literal, regardless of what's inside.
| Variant | Notes |
|---|---|
$$ ... $$ | The standard form. Use unless your code itself contains the literal $$. |
$tag$ ... $tag$ | Tagged form — pick any identifier as the tag. Useful for nesting: $outer$ ... $body$ ... $body$ ... $outer$. |
'...' | Standard SQL string. Works but every embedded ' must be doubled to ''. Cumbersome — avoid. |
' in the function body would have to be escaped as '', and any nested PL/pgSQL would need yet another layer of escaping. With $$, you can paste real code with single quotes, embedded JSON, or anything else without a single backslash.The simplest possible PL/pgSQL block:
DO $$
BEGIN
RAISE NOTICE 'Hello, world!';
END
$$;
Two parts of the output to notice:
NOTICE: Hello, world!— the message fromRAISE NOTICE. PostgreSQL'sNOTICElevel appears in client tools (likepsql) but doesn't interrupt execution.DO— the command tag, confirming the anonymous block executed.
Declare a variable, store a value in it, print the result. RAISE NOTICE '...%...', value uses % as a positional placeholder — like printf:
DO $$
DECLARE
message_text VARCHAR(255) := 'VRIDDH TECHNOLOGIES';
BEGIN
-- The PostgreSQL equivalent of Oracle's DBMS_OUTPUT.PUT_LINE
RAISE NOTICE '%', message_text;
END
$$;
DBMS_OUTPUT.PUT_LINE for this — PostgreSQL has no direct equivalent. The convention is to use RAISE NOTICE 'format %', value for debugging output. Make sure your client (e.g. psql) has client_min_messages set to NOTICE or lower, otherwise the messages won't appear.Identifiers — variable, function, column, and table names — follow standard PostgreSQL rules:
- Start with a letter or underscore
- Followed by letters, digits, underscores, or dollar signs
- Maximum length is 63 characters (set by
NAMEDATALENat compile time; longer names get truncated) - Not case-sensitive when unquoted:
counter,Counter, andCOUNTERall refer to the same identifier - Quoted in double quotes (
"My Name") preserves case and allows otherwise-illegal characters - Cannot use a reserved keyword as an unquoted identifier
NAMEDATALEN allows 63 characters, more than enough for descriptive names like calculate_quarterly_revenue_summary.PL/pgSQL supports both single-line and block comments — same syntax as standard SQL:
-- Single-line comment to end of line
/*
Block comment
Can span multiple lines
*/
The pages that follow walk through PL/pgSQL fundamentals:
- Blocks — anonymous blocks via
DO, named blocks viaCREATE FUNCTION, nested subblocks - Constants — named immutable values
- Data Types — composite types:
%TYPE,%ROWTYPE,RECORD - Variables — declaration, assignment, scoping rules
- Return — exiting functions and producing values
- Label — naming blocks and loops for control flow
- Nested Block — blocks inside blocks for scope and modularity
- Select Into — assigning query results to variables
- PL/pgSQL is PostgreSQL's procedural language — it adds control structures, variables, and exception handling to plain SQL.
- Every PL/pgSQL block has up to three sections: DECLARE (optional), BEGIN/END (required), and EXCEPTION (optional).
- Use
DO $$ ... $$;to run an anonymous block once; useCREATE FUNCTIONorCREATE PROCEDUREto create a reusable named version. - Dollar-quoting (
$$ ... $$or$tag$ ... $tag$) lets you embed code with single quotes without escaping. Strongly preferred over single-quoted bodies. - Use
RAISE NOTICE 'format %', valuefor output and debugging — it's PostgreSQL's equivalent ofDBMS_OUTPUT.PUT_LINE. - Identifiers are up to 63 characters, not case-sensitive when unquoted, and follow standard rules for the leading character.