PostgreSQL Introduction PostgreSQL · Anonymous Block · Introduction

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 resultApp 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 latencySingle 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 NOTICE and 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.
💡 The pragmatic stance: Use PL/pgSQL for clear wins — multi-statement transactional work where round-trip latency matters, security-boundary functions, triggers, ad-hoc maintenance scripts. Don't reach for it as a substitute for application-layer code just because it's possible. Application code is easier to test, version, and migrate.

Every piece of PL/pgSQL code is organized into blocks. A block has up to three sections — only the body is mandatory:

[ <<label>> ] DECLARE -- optional: variables, constants, cursors declarations BEGIN -- mandatory: the executable code statements; EXCEPTION -- optional: error handlers WHEN condition THEN handler; END [ label ];
SectionPurposeRequired?
DECLAREVariables, constants, cursors, exceptions, record typesOptional
BEGIN ... ENDThe executable statements — the body of the blockRequired
EXCEPTIONCatches errors raised inside the body and handles themOptional

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:

DO $$ DECLARE -- variable declarations BEGIN -- PL/pgSQL code END $$;

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.

VariantNotes
$$ ... $$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.
💡 Dollar-quoting is one of PostgreSQL's nicer features. Without it, every ' 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.
Example 1 — Hello World

The simplest possible PL/pgSQL block:

PL/pgSQL — Hello World
DO $$
BEGIN
    RAISE NOTICE 'Hello, world!';
END
$$;
OUTPUT
psql — hello world
postgres=#DO $$ BEGIN RAISE NOTICE 'Hello, world!'; END $$;
NOTICE: Hello, world!
DO

Two parts of the output to notice:

  • NOTICE: Hello, world! — the message from RAISE NOTICE. PostgreSQL's NOTICE level appears in client tools (like psql) but doesn't interrupt execution.
  • DO — the command tag, confirming the anonymous block executed.
Example 2 — A Block with a Variable

Declare a variable, store a value in it, print the result. RAISE NOTICE '...%...', value uses % as a positional placeholder — like printf:

PL/pgSQL — Variable + RAISE NOTICE
DO $$
DECLARE
    message_text VARCHAR(255) := 'VRIDDH TECHNOLOGIES';
BEGIN
    -- The PostgreSQL equivalent of Oracle's DBMS_OUTPUT.PUT_LINE
    RAISE NOTICE '%', message_text;
END
$$;
OUTPUT
psql — message variable
postgres=#DO $$ DECLARE message_text VARCHAR(255) := 'VRIDDH TECHNOLOGIES'; ... END $$;
NOTICE: VRIDDH TECHNOLOGIES
DO
📌 Coming from Oracle PL/SQL? Oracle uses 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 NAMEDATALEN at compile time; longer names get truncated)
  • Not case-sensitive when unquoted: counter, Counter, and COUNTER all 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
⚠️ The original tutorial said identifiers "should not exceed 30 characters." That's wrong for PostgreSQL — that's the Oracle limit. PostgreSQL's default 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:

PL/pgSQL — Comments
-- 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 via CREATE 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; use CREATE FUNCTION or CREATE PROCEDURE to 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 %', value for output and debugging — it's PostgreSQL's equivalent of DBMS_OUTPUT.PUT_LINE.
  • Identifiers are up to 63 characters, not case-sensitive when unquoted, and follow standard rules for the leading character.