PostgreSQL Blocks PostgreSQL · Anonymous Block · Blocks

PL/pgSQL Blocks

Master PL/pgSQL block structure — anonymous DO blocks, named blocks via CREATE FUNCTION, nested blocks with labels, declaration sections, and best practices. Worked examples include a fixed orders/revenue calculation, the add_numbers function, and outer/inner nested blocks.

In PL/pgSQL, a block is the fundamental unit of code. Everything you write — anonymous scripts, functions, procedures, triggers — is structured around the BEGIN ... END block. Blocks group related statements, scope variables to where they're needed, and mark the boundaries for exception handling.

Two big categories you'll encounter:

TypeCreated withPurpose
Anonymous blockDO $$ ... $$;One-shot procedural code — ad-hoc scripts, maintenance tasks, migrations
Named blockCREATE FUNCTION / CREATE PROCEDUREReusable code with a stored name — call it from anywhere
[ <<label>> ] DECLARE -- optional declarations BEGIN -- mandatory statements; EXCEPTION -- optional handlers END [ label ];
ElementNotes
<<label>>Optional name for the block — useful for qualifying variables in nested blocks (outer.x) and as a target for EXIT
DECLAREVariables, constants, cursors, exceptions. Each declaration ends with ;
BEGIN ... ENDThe executable body. Each statement ends with ;
EXCEPTIONError handlers. Catches errors raised inside the body
ENDFollowed by a semicolon. Can repeat the label for clarity: END outer;

The basic syntax:

variable_name data_type [= initial_value];

Examples:

PL/pgSQL — Variable declarations
-- With initial value
counter int = 0;

-- Without initial value (defaults to NULL)
max int;

-- Both := and = work for assignment
total numeric := 100.50;
quantity int = 5;
💡 PL/pgSQL accepts both = and := as assignment. The SQL standard prefers := for assignment to keep it distinct from = (comparison) — and that's the form Oracle PL/SQL requires. PL/pgSQL accepts both for convenience, but := is more conventional in published examples and avoids ambiguity.

An anonymous block is PL/pgSQL code that runs once, immediately, with no name and no stored definition. You wrap it in DO $$ ... $$;. Useful for maintenance tasks, data migrations, ad-hoc analysis — anything you wouldn't want to clutter the database catalog with a permanent function for.

DO $$ DECLARE -- Variable declarations BEGIN -- PL/pgSQL code END $$;
Example 1 — A Simple Anonymous Block
PL/pgSQL — Anonymous block
DO $$
BEGIN
    RAISE NOTICE 'This is an anonymous block';
END
$$;
OUTPUT
psql — anonymous block
postgres=#DO $$ BEGIN RAISE NOTICE 'This is an anonymous block'; END $$;
NOTICE: This is an anonymous block
DO
Example 2 — Anonymous Block Querying a Table

A more substantial example — calculate revenue from the past month using SELECT INTO to capture a query result, then format it with RAISE NOTICE. First, the sample table:

PL/pgSQL — Setup
DROP TABLE IF EXISTS orders;

CREATE TABLE orders (
    order_id     SERIAL PRIMARY KEY,
    order_date   DATE,
    order_amount NUMERIC
);

INSERT INTO orders (order_id, order_date, order_amount) VALUES
    (1, '2024-03-01',  500),
    (2, '2024-03-05',  700),
    (3, '2024-03-10',  900),
    (4, '2024-03-15',  400),
    (5, '2024-03-20',  600),
    (6, '2024-03-25',  800),
    (7, '2024-03-30', 1000),
    (8, '2024-04-05', 1200);
⚠️ The original tutorial had a typo in this INSERT: (2,,'2024-03-05', 700) with a double comma — a hard syntax error that would prevent the example from running. The fixed version above uses single commas as intended.

Now the anonymous block that uses the data:

PL/pgSQL — Last-month revenue
DO $$
DECLARE
    total_revenue NUMERIC := 0;
BEGIN
    SELECT SUM(order_amount)
    INTO   total_revenue
    FROM   orders
    WHERE  order_date >= CURRENT_DATE - INTERVAL '1 month';

    RAISE NOTICE 'Total revenue generated in the last month: $%', total_revenue;
END
$$;
OUTPUT
psql — revenue
postgres=#DO $$ DECLARE total_revenue NUMERIC := 0; BEGIN ... END $$;
NOTICE: Total revenue generated in the last month: $1200
DO

The exact total depends on when you run the example — CURRENT_DATE - INTERVAL '1 month' evaluates relative to today. With test data dated March 2024 and an April 2024 row, the result varies by current date.

📌 The DO statement is not part of the block — it's the wrapper that executes a block. When you see DO $$ ... $$;, the actual block is everything between the dollar-quotes. The DO command was added in PostgreSQL 9.0 specifically to make anonymous PL/pgSQL execution easy.

Named blocks — PL/pgSQL functions and procedures — are stored in the database and can be called by name. The syntax for a function:

CREATE OR REPLACE FUNCTION function_name(parameter_list) RETURNS return_type AS $$ DECLARE -- variable declarations BEGIN -- the body RETURN some_expression; END; $$ LANGUAGE plpgsql;
Example 3 — A Named Function

A function that takes two integers and returns their sum:

PL/pgSQL — Named function
CREATE OR REPLACE FUNCTION add_numbers(num1 INTEGER, num2 INTEGER)
RETURNS INTEGER AS $$
DECLARE
    result INTEGER;
BEGIN
    result := num1 + num2;
    RETURN result;
END;
$$ LANGUAGE plpgsql;

-- Call it
SELECT add_numbers(10, 20);
OUTPUT
psql — add_numbers
postgres=#CREATE OR REPLACE FUNCTION add_numbers(...) RETURNS INTEGER AS $$ ... $$ LANGUAGE plpgsql;
CREATE FUNCTION
postgres=#SELECT add_numbers(10, 20);
add_numbers
-------------
30
(1 row)
Example 4 — A Function Querying a Table

Reuse the orders table from earlier. Wrap the count-of-orders logic in a named function:

PL/pgSQL — calculate_total_orders
CREATE OR REPLACE FUNCTION calculate_total_orders()
RETURNS INTEGER AS $$
DECLARE
    total_orders INTEGER;
BEGIN
    SELECT COUNT(*)
    INTO   total_orders
    FROM   orders;

    RETURN total_orders;
END;
$$ LANGUAGE plpgsql;

SELECT calculate_total_orders();
OUTPUT
psql — calculate_total_orders
postgres=#SELECT calculate_total_orders();
calculate_total_orders
------------------------
8
(1 row)

The function encapsulates the logic — any other code that needs the count just calls calculate_total_orders() instead of repeating the SELECT. If the counting rule changes (e.g. exclude cancelled orders), update the function once and every caller benefits.

Use anonymous (DO) for…Use named (CREATE FUNCTION/PROCEDURE) for…
One-time data migrationsReusable business logic
Ad-hoc maintenance tasksCode called from triggers, other functions, or applications
Testing PL/pgSQL snippets interactivelyAnything that benefits from caching parsed plans
Migration scripts run onceLogic that needs to be discoverable in information_schema
Code that doesn't need to be discoverable laterCode that's part of the application's data model

A block can contain another block — a subblock. Nested blocks have their own DECLARE sections, so variables declared inside are scoped to the inner block only. Useful for organizing logic and limiting variable lifetime.

Example 5 — Outer and Inner Block

Two blocks, both labelled, with the inner one referencing a variable from the outer:

PL/pgSQL — Nested blocks with labels
DO $$
<>
DECLARE
    x int = 0;
BEGIN
    x := x + 1;

    <>
    DECLARE
        y int = 2;
    BEGIN
        y := y + x;
        RAISE NOTICE 'x=% y=%', x, y;
    END inner;
END outer;
$$;
OUTPUT
psql — nested blocks
postgres=#DO $$ <<outer>> DECLARE x int = 0; BEGIN ... END outer; $$;
NOTICE: x=1 y=3
DO

Step by step:

  1. Outer block declares x = 0, then increments to 1
  2. Inner block declares y = 2, then adds x (which is 1) → y = 3
  3. RAISE NOTICE prints x=1 y=3

Subblocks get a more thorough treatment in the dedicated Nested Block page — including how variable shadowing works when an inner block reuses an outer block's name.

  1. Use DO for one-time tasks; create named functions for reusable logic. Don't pollute the catalog with single-use functions.
  2. Always specify LANGUAGE plpgsql on CREATE FUNCTION. Without it, PostgreSQL defaults to SQL, and your PL/pgSQL syntax will fail to parse.
  3. Use CREATE OR REPLACE FUNCTION for development — makes your script idempotent. Skip the explicit DROP/CREATE dance.
  4. Label outer blocks when nesting — they let inner blocks unambiguously refer to outer variables.
  5. Match END label to BEGIN labelEND outer; vs END inner;. Optional but catches mistakes when blocks get long.
  6. Initialize variables explicitly when the default NULL would cause subtle bugs (e.g. NULL + 5 = NULL).
  7. Keep blocks focused. If a function is doing five things, consider splitting it.
  • A PL/pgSQL block has up to three sections: DECLARE (optional), BEGIN/END (required), EXCEPTION (optional).
  • Anonymous blocks run via DO $$ ... $$; — one-shot, not stored. Useful for migrations, ad-hoc tasks.
  • Named blocks live as functions or procedures created with CREATE FUNCTION / CREATE PROCEDURE. Reusable, callable from anywhere.
  • Variable declarations: name type = initial_value; — both = and := work for assignment.
  • Subblocks are blocks nested inside other blocks. They have their own scope; inner blocks can reach outer variables but outer blocks can't see inner ones.
  • Always end CREATE FUNCTION with LANGUAGE plpgsql — without it, PostgreSQL assumes plain SQL.