PL/pgSQL Cursor
Comprehensive introduction to PL/pgSQL cursors — implicit cursors that PostgreSQL creates automatically for SELECT INTO and FOR loops, the FOUND variable for zero-row checks, and the critical correction that plain SELECT INTO does NOT raise on multiple rows in PostgreSQL (that's MySQL behavior).
A cursor in PL/pgSQL is a pointer into the result set of a query. Where a single SELECT INTO hands you exactly one row's worth of data, a cursor lets you walk through any number of rows one at a time, processing each before moving to the next.
PostgreSQL supports two flavors:
| Type | Declared with | Use when |
|---|---|---|
| Implicit cursor | None — automatic | Standard SQL statements (SELECT INTO, INSERT, UPDATE, DELETE) and FOR ... IN SELECT ... LOOP |
| Explicit cursor | cursor_name CURSOR FOR query; | You need fine-grained control over OPEN / FETCH / CLOSE — covered on the Explicit Cursor page |
This page focuses on implicit cursors — the ones PostgreSQL creates and manages for you behind the scenes. You'll write code that uses them every day without thinking about it; understanding what's happening under the hood helps debug edge cases.
PostgreSQL creates implicit cursors automatically whenever you run a SQL statement inside a PL/pgSQL block. You never declare them, never open them, never close them — that's all handled for you. The four operations that trigger one:
The most common case: pull a single row's data into variables.
DO $$
DECLARE
v_name VARCHAR;
BEGIN
SELECT name INTO v_name
FROM employees_20
WHERE id = 1003;
RAISE NOTICE 'Found: %', v_name;
END
$$;
Behind the scenes, PostgreSQL opened a cursor for the SELECT, fetched the one row, assigned its values into v_name, then closed the cursor — all invisibly.
Same automatic cursor management for write operations:
DO $$
BEGIN
INSERT INTO employees_20 (id, name, department_id) VALUES (1099, 'Test User', 99);
UPDATE employees_20 SET department_id = 88 WHERE id = 1099;
DELETE FROM employees_20 WHERE id = 1099;
END
$$;
Each of these statements works through an implicit cursor that PostgreSQL manages on your behalf. You don't see it; you don't have to.
Set up a table the rest of the examples will use:
DROP TABLE IF EXISTS employees_20;
CREATE TABLE employees_20 (
id INT PRIMARY KEY,
name VARCHAR(100),
department_id INT
);
INSERT INTO employees_20 (id, name, department_id) VALUES
(1001, 'Jane Smith', 10),
(1002, 'Michael Johnson', 20),
(1003, 'Emily Williams', 20),
(1004, 'David Brown', 30),
(1005, 'Sarah Davis', 30),
(1006, 'James Miller', 40),
(1007, 'Emma Wilson', 40),
(1008, 'William Taylor', 50),
(1009, 'John Doe', 10),
(1010, 'Olivia Martinez', 50);
id is just an INT column, but the redesigned version makes id a PRIMARY KEY (which it should be), so duplicates would error. Removed the duplicates.The behavior of SELECT INTO when the row count isn't exactly one is a common source of confusion — and PostgreSQL differs from Oracle and MySQL here.
| Match count | Plain SELECT INTO | SELECT INTO STRICT |
|---|---|---|
| 0 rows | Variables NULL; FOUND is false | Raises NO_DATA_FOUND |
| 1 row | Variables assigned; FOUND is true | Variables assigned |
| 2+ rows | Picks first row arbitrarily; no error | Raises TOO_MANY_ROWS |
SELECT INTO in PostgreSQL silently picks the first row when multiple match. To get the multi-row error, use SELECT INTO STRICT. (See the SELECT INTO page in zip 1 for the full coverage.)The FOUND special variable tells you whether the most recent statement matched anything. The cleanest way to handle "row may not exist":
DO $$
DECLARE
emp_record employees_20%ROWTYPE;
BEGIN
SELECT * INTO emp_record
FROM employees_20
WHERE id = 9999; -- no such employee
IF NOT FOUND THEN
RAISE NOTICE 'No employee with id 9999';
ELSE
RAISE NOTICE 'Found: %', emp_record.name;
END IF;
END
$$;
If a query could return multiple rows but you only want one, add ORDER BY ... LIMIT 1 — that gives you a deterministic pick. The original tutorial used LIMIT 1 alone, but without an ORDER BY, "first" is undefined.
DO $$
DECLARE
emp_record employees_20%ROWTYPE;
BEGIN
SELECT * INTO emp_record
FROM employees_20
WHERE department_id = 50
ORDER BY id
LIMIT 1;
RAISE NOTICE 'First employee in dept 50: %', emp_record.name;
END
$$;
For processing many rows, the most idiomatic PL/pgSQL pattern is FOR record IN SELECT ... LOOP. PostgreSQL implicitly opens a cursor, fetches each row in turn, and closes the cursor when the loop ends. No explicit DECLARE / OPEN / FETCH / CLOSE needed.
DO $$
DECLARE
emp_rec record;
BEGIN
FOR emp_rec IN
SELECT id, name FROM employees_20 WHERE department_id = 30
LOOP
RAISE NOTICE 'Employee % - %', emp_rec.id, emp_rec.name;
END LOOP;
END
$$;
Two rows in department 30, two NOTICE lines, no manual cursor work. The record type adapts to whatever columns the SELECT returned — you can also use a %ROWTYPE variable when the shape is fixed.
Combine the iteration with an accumulator variable to compute summaries during the walk:
DO $$
DECLARE
emp_rec record;
total_emps INTEGER := 0;
BEGIN
FOR emp_rec IN
SELECT name FROM employees_20 ORDER BY id
LOOP
total_emps := total_emps + 1;
END LOOP;
RAISE NOTICE 'Walked through % employees', total_emps;
END
$$;
SELECT COUNT(*) — it's faster. The walk-and-count pattern above demonstrates the loop mechanics, but for actual aggregations you should let the database optimizer plan it: SELECT COUNT(*) INTO total_emps FROM employees_20. The FOR loop earns its keep when each row needs processing, not just counting.| Use implicit cursors when… | Use explicit cursors when… |
|---|---|
| Single SELECT INTO, INSERT, UPDATE, DELETE | You need to OPEN, FETCH a few rows, then maybe stop early |
Iterating with FOR rec IN SELECT ... LOOP | You need to FETCH from the same cursor at multiple points in the function |
| You want clean, idiomatic PL/pgSQL with no boilerplate | You need to pass a cursor to another function (use refcursor) |
| Performance is fine — the engine optimizes well | You need scrollable cursors or fine-grained fetch sizes |
For the vast majority of use cases, implicit cursors via FOR ... IN ... LOOP are the right tool. Reach for explicit cursors only when implicit ones can't express what you need.
- Reach for
FOR rec IN SELECT ... LOOPas the default pattern for iterating rows. It's the cleanest, most readable form and lets PostgreSQL handle cursor lifecycle. - Add
ORDER BY ... LIMIT 1when you want a deterministic single row from a possibly-multi-row query.LIMIT 1alone gives you "some row" — not always the same one. - Use
SELECT INTO STRICTwhen the query should return exactly one row and anything else is a bug. Loud failure beats silent wrong-data. - Check
FOUNDafter a plainSELECT INTOwhen zero rows is a valid case you need to handle. - Don't loop over rows just to compute aggregates — let the SQL engine do
SUM,AVG,COUNTfor you. Loops are for per-row processing that pure SQL can't express. - Beware Oracle-isms. PL/pgSQL doesn't have
NO_DATA_FOUNDraised automatically (only withSTRICT); doesn't have%ROWCOUNTas a cursor attribute (useFOUNDorGET DIAGNOSTICSinstead).
- A cursor is a pointer into a query's result set, used for row-by-row processing.
- PostgreSQL creates implicit cursors automatically for SELECT INTO, INSERT, UPDATE, DELETE, and
FOR rec IN SELECT ... LOOP. You don't manage them. - The FOR loop over a SELECT is the idiomatic pattern for walking rows — clean, lifecycle-managed, no boilerplate.
- Plain
SELECT INTO: 0 rows → variables NULL andFOUNDfalse; 1 row → assigned; 2+ rows → picks first arbitrarily, no error. UseSTRICTto make multi-row a hard error. - The
FOUNDspecial variable tells you whether the latest statement matched anything. - Use explicit cursors only when implicit ones can't express what you need — covered on the next pages.