PostgreSQL Cursor Params PostgreSQL · Cursor · Parameters

PL/pgSQL Cursor With Parameters

Master PL/pgSQL parameterized cursors — declaring CURSOR (param type, ...) FOR query, opening with different argument values, and the cleaner FOR rec IN cursor_name(args) LOOP syntax. Key distinction: PostgreSQL uses FOR not IS (the latter is Oracle).

An explicit cursor with parameters lets you declare the cursor once and open it multiple times with different argument values, getting different result sets each time. The query body stays the same; the placeholders take whatever values you pass at OPEN time.

Compare two approaches:

Without parametersWith parameters
Hard-code values in the queryDeclare placeholders once
Need different cursor for each query variantOne cursor, multiple opens with different args
Or rebuild query as a string and use EXECUTEParsed and planned once; arguments substituted
Repetitive code if you query similar things oftenConcise — the query body is reused

Declare a parameterized cursor:

DECLARE cursor_name CURSOR (parameter_list) FOR cursor_query;

Open it, supplying values for the parameters:

OPEN cursor_name(value1, value2, ...);

Three things to know:

  • Each parameter is declared as name type — exactly like function parameters.
  • Parameters are referenced inside the cursor query just like ordinary expressions: WHERE col = parameter_name.
  • Parameters are scoped to the cursor query — you can't reference them outside.
📌 Note: PL/pgSQL does NOT use IS when declaring a cursor. If you've worked in Oracle PL/SQL where CURSOR cursor_name IS query; is standard, expect to drop the IS in PostgreSQL — the syntax uses FOR.
PL/pgSQL — Setup
DROP TABLE IF EXISTS products;

CREATE TABLE products (
    product_id   INT PRIMARY KEY,
    product_name VARCHAR(100),
    list_price   NUMERIC
);

INSERT INTO products (product_id, product_name, list_price) VALUES
    (1, 'iPhone 14',             999),
    (2, 'Samsung Galaxy S23',    899),
    (3, 'Sony PlayStation 5',    499),
    (4, 'MacBook Pro',          1499),
    (5, 'Dell XPS 15',          1299),
    (6, 'Bose QuietComfort 45',  329),
    (7, 'Nintendo Switch',       299),
    (8, 'GoPro Hero 12',         499),
    (9, 'Canon EOS R5',         3899);
Example 1 — Cursor with Two Price-Range Parameters

Declare a cursor that takes low and high price bounds. Open it twice with different argument values to fetch two different price ranges:

PL/pgSQL — parameterized cursor
DO $$
DECLARE
    -- Parameterized cursor: takes a price range
    products_in_range CURSOR (low NUMERIC, high NUMERIC) FOR
        SELECT product_name, list_price
        FROM   products
        WHERE  list_price BETWEEN low AND high
        ORDER  BY list_price;

    rec record;
BEGIN
    -- First open: mass-market range
    RAISE NOTICE 'Mass products (100 - 500):';
    OPEN products_in_range(100, 500);
    LOOP
        FETCH products_in_range INTO rec;
        EXIT WHEN NOT FOUND;
        RAISE NOTICE '  %: %', rec.product_name, rec.list_price;
    END LOOP;
    CLOSE products_in_range;

    -- Second open: same cursor, different arguments
    RAISE NOTICE 'Luxury products (800 - 1000):';
    OPEN products_in_range(800, 1000);
    LOOP
        FETCH products_in_range INTO rec;
        EXIT WHEN NOT FOUND;
        RAISE NOTICE '  %: %', rec.product_name, rec.list_price;
    END LOOP;
    CLOSE products_in_range;
END
$$;
OUTPUT
psql — two ranges
postgres=#DO $$ DECLARE products_in_range CURSOR (low NUMERIC, high NUMERIC) FOR ...; ... END $$;
NOTICE: Mass products (100 - 500):
NOTICE: Nintendo Switch: 299
NOTICE: Bose QuietComfort 45: 329
NOTICE: Sony PlayStation 5: 499
NOTICE: GoPro Hero 12: 499
NOTICE: Luxury products (800 - 1000):
NOTICE: Samsung Galaxy S23: 899
NOTICE: iPhone 14: 999
DO

Same cursor declaration; two opens with different arguments; two completely different result sets. The query body never had to be repeated.

⚠️ The original tutorial declared a "parameterized" cursor but then put the literal values 100 and 500 directly in the query — making the parameters unused decoration. The example above shows the actual parameterized pattern: parameters declared on the cursor, values supplied at OPEN.
Example 2 — Parameterized Cursor with FOR Loop

The verbose OPEN / FETCH / EXIT WHEN NOT FOUND / CLOSE dance can also be written as a FOR loop — passing the parameter values right at the loop. PostgreSQL handles the cursor lifecycle for you, just like with implicit cursors:

PL/pgSQL — FOR loop with parameterized cursor
DO $$
DECLARE
    products_in_range CURSOR (low NUMERIC, high NUMERIC) FOR
        SELECT product_name, list_price
        FROM   products
        WHERE  list_price BETWEEN low AND high
        ORDER  BY list_price;

    rec record;
BEGIN
    -- FOR loop opens, fetches, and closes the cursor automatically
    FOR rec IN products_in_range(100, 500) LOOP
        RAISE NOTICE '  %: %', rec.product_name, rec.list_price;
    END LOOP;
END
$$;
OUTPUT
psql — FOR with cursor
postgres=#DO $$ DECLARE products_in_range CURSOR (low NUMERIC, high NUMERIC) FOR ...; ... FOR rec IN products_in_range(100, 500) LOOP ... END $$;
NOTICE: Nintendo Switch: 299
NOTICE: Bose QuietComfort 45: 329
NOTICE: Sony PlayStation 5: 499
NOTICE: GoPro Hero 12: 499
DO

Half the lines, same result. FOR rec IN cursor_name(args) LOOP is the idiomatic shortcut.

Example 3 — When Parameters Help vs When They Don't

Parameterized cursors really shine when the same cursor is opened many times in different contexts. For a one-shot query, plain FOR rec IN SELECT ... LOOP with values inlined is simpler and equally fast — the planner caches plans either way.

SituationBest fit
Single query, single usePlain FOR rec IN SELECT ... WHERE col = value LOOP
Same query body, multiple opens with different args (in one block)Parameterized cursor
Different queries entirelySeparate cursor declarations or separate FOR loops
Query that's built dynamically from user inputUse EXECUTE with USING (parameter binding) — covered later
  1. Use FOR rec IN cursor_name(args) LOOP as the default — much cleaner than the explicit OPEN/FETCH/CLOSE form.
  2. Reach for parameterized cursors when you'd otherwise repeat the same query body with different argument values. One declaration, many opens.
  3. Don't reach for them for one-shot queries — plain inlined values are simpler.
  4. Add an ORDER BY to the cursor query if you care about the row order — without one, PostgreSQL doesn't promise a specific order.
  5. Don't use IS after the cursor name — that's Oracle syntax. PostgreSQL uses FOR.
  6. Parameters take any expression type — numbers, strings, dates, even arrays. Match types with the WHERE clause they'll appear in.
  • A parameterized cursor declares placeholders that get values at OPEN time — the same query body is reusable with different inputs.
  • Syntax: cursor_name CURSOR (param1 type, param2 type) FOR query; — note the FOR, not IS (which is Oracle).
  • Open with values: OPEN cursor_name(value1, value2);.
  • FOR rec IN cursor_name(args) LOOP ... END LOOP is the cleanest way to iterate — PostgreSQL handles the cursor lifecycle.
  • Best when the same query body is opened multiple times with different arguments. For single-use queries, inline values are simpler.