PostgreSQL WHILE PostgreSQL · Conditional · WHILE

PL/pgSQL WHILE Loop

Master the PL/pgSQL WHILE loop — pre-test loop semantics, real-world example walking pending tasks, infinite loop pitfalls and how to avoid them, and when WHILE beats FOR or set-based SQL.

A WHILE loop runs its body as long as a condition is true. The condition is checked before each iteration — if it's false (or NULL) on the first check, the body never executes at all. This is the "pre-test loop" pattern.

Loop typeWhen to use
WHILE condition LOOPTermination depends on a condition; you don't know in advance how many iterations
FOR i IN low..high LOOPYou're iterating a known integer range
FOR rec IN SELECT ... LOOPYou're walking the rows of a query
LOOP ... EXIT WHEN ...Exit condition is in the middle of the body, not at the top
[ <<label>> ] WHILE condition LOOP statements; END LOOP;

Three things to remember:

  • The condition is checked before each iteration. If false on entry, the body is skipped entirely.
  • Something inside the body must change so the condition eventually becomes false — otherwise you have an infinite loop.
  • NULL is treated as not-true. A condition that evaluates to NULL terminates the loop, just like FALSE.
Example 1 — Counting with WHILE

Display the value of a counter from 0 up to (but not including) 5:

PL/pgSQL — basic WHILE
DO $$
DECLARE
    counter INTEGER := 0;
BEGIN
    WHILE counter < 5 LOOP
        RAISE NOTICE 'Counter %', counter;
        counter := counter + 1;
    END LOOP;
END
$$;
OUTPUT
psql — basic WHILE
postgres=#DO $$ DECLARE counter INTEGER := 0; BEGIN WHILE counter < 5 LOOP RAISE NOTICE 'Counter %', counter; counter := counter + 1; END LOOP; END $$;
NOTICE: Counter 0
NOTICE: Counter 1
NOTICE: Counter 2
NOTICE: Counter 3
NOTICE: Counter 4
DO

Five iterations, then counter reaches 5, the condition becomes false, and the loop exits. The "pre-test" nature: when counter = 5 the condition is checked first, finds false, and the body doesn't run again.

💡 For a known integer range, FOR i IN 0..4 LOOP is simpler. WHILE earns its keep when the termination condition isn't a simple integer count — say, "while there are still pending rows to process" or "while the response is incomplete."

Consider a tasks table where each task has a status. The WHILE pattern shines when you need to keep processing as long as work remains, without knowing in advance how much work that is.

PL/pgSQL — Setup
DROP TABLE IF EXISTS tasks;

CREATE TABLE tasks (
    task_id   SERIAL PRIMARY KEY,
    task_name VARCHAR(100),
    status    VARCHAR(20)
);

INSERT INTO tasks (task_name, status) VALUES
    ('Review Report',         'Pending'),
    ('Prepare Presentation',  'Completed'),
    ('Update Documentation',  'Pending'),
    ('Conduct Meeting',       'Pending'),
    ('Analyze Data',          'Pending'),
    ('Develop Prototype',     'Completed'),
    ('Test Software',         'Pending'),
    ('Write Code',            'Pending'),
    ('Review Code',           'Completed'),
    ('Plan Project',          'Pending');
Example 2 — Walking Pending Tasks One by One

The pattern: read the next pending task ID; if there is one, process it; loop until none remain.

PL/pgSQL — process pending tasks
DO $$
DECLARE
    task_id_to_process INT;
BEGIN
    -- Get the first pending task
    SELECT MIN(task_id) INTO task_id_to_process
    FROM   tasks WHERE status = 'Pending';

    -- Loop until no more pending tasks
    WHILE task_id_to_process IS NOT NULL LOOP
        UPDATE tasks
        SET    status = 'Completed'
        WHERE  task_id = task_id_to_process;

        RAISE NOTICE 'Task ID % has been completed.', task_id_to_process;

        -- Get next pending task — drives the loop forward
        SELECT MIN(task_id) INTO task_id_to_process
        FROM   tasks WHERE status = 'Pending';
    END LOOP;
END
$$;
OUTPUT
psql — process tasks
postgres=#DO $$ DECLARE task_id_to_process INT; BEGIN SELECT MIN(task_id) INTO task_id_to_process FROM tasks WHERE status = 'Pending'; WHILE task_id_to_process IS NOT NULL LOOP ... END $$;
NOTICE: Task ID 1 has been completed.
NOTICE: Task ID 3 has been completed.
NOTICE: Task ID 4 has been completed.
NOTICE: Task ID 5 has been completed.
NOTICE: Task ID 7 has been completed.
NOTICE: Task ID 8 has been completed.
NOTICE: Task ID 10 has been completed.
DO

Once all 'Pending' tasks have been updated, SELECT MIN(task_id) ... WHERE status = 'Pending' returns no rows, task_id_to_process becomes NULL, and the loop exits. NULL is treated as not-true in the WHILE condition.

⚠️ But wait — this whole loop is unnecessary for this particular task. The same effect can be achieved with one set-based statement: UPDATE tasks SET status = 'Completed' WHERE status = 'Pending';. The loop above is instructive for showing WHILE mechanics, but if you spot a chance to replace a loop with a single SQL statement, take it — set-based SQL is dramatically faster.
Example 3 — When WHILE is Genuinely the Right Tool

Sometimes you genuinely need WHILE — when each iteration depends on results from the previous one. Example: doubling a value until it exceeds a threshold:

PL/pgSQL — exponential growth
DO $$
DECLARE
    value      INTEGER := 1;
    iterations INTEGER := 0;
    target     INTEGER := 1000;
BEGIN
    WHILE value < target LOOP
        value := value * 2;
        iterations := iterations + 1;
        RAISE NOTICE 'After % doublings: %', iterations, value;
    END LOOP;

    RAISE NOTICE 'Reached % in % iterations', value, iterations;
END
$$;
OUTPUT
psql — doubling
postgres=#DO $$ DECLARE value INTEGER := 1; iterations INTEGER := 0; target INTEGER := 1000; BEGIN WHILE value < target LOOP ... END $$;
NOTICE: After 1 doublings: 2
NOTICE: After 2 doublings: 4
NOTICE: After 3 doublings: 8
NOTICE: After 4 doublings: 16
NOTICE: After 5 doublings: 32
NOTICE: After 6 doublings: 64
NOTICE: After 7 doublings: 128
NOTICE: After 8 doublings: 256
NOTICE: After 9 doublings: 512
NOTICE: After 10 doublings: 1024
NOTICE: Reached 1024 in 10 iterations
DO

You don't know up front how many iterations are needed — that's exactly when WHILE is the right tool. Each iteration's value depends on the previous one's, and the termination condition is data-driven.

The most common WHILE bug: the condition can never become false. Three patterns to watch for:

PL/pgSQL — bug examples (DON'T RUN)
-- Bug 1: forgot to update the loop variable
DECLARE counter INT := 0;
BEGIN
    WHILE counter < 5 LOOP
        RAISE NOTICE '%', counter;
        -- counter is never incremented — infinite loop
    END LOOP;
END;

-- Bug 2: wrong direction
DECLARE counter INT := 5;
BEGIN
    WHILE counter < 10 LOOP
        counter := counter - 1;        -- moves AWAY from 10
    END LOOP;
END;

-- Bug 3: condition can become NULL but the body assumes it eventually goes false
DECLARE x INTEGER := 5;
BEGIN
    WHILE x > 0 LOOP
        x := some_function_returning_null();   -- NULL > 0 is NULL → loop exits
        -- but is that what you intended?
    END LOOP;
END;
💡 Always have a clear "what makes the condition false" plan. Before you write the loop body, identify exactly what statement inside the body will eventually make the condition false. If you can't point at it, the loop is suspicious.
Use caseBest fit
Iterate a known integer range (1..N)FOR i IN 1..N LOOP
Walk all rows from a queryFOR rec IN SELECT ... LOOP
Loop until a condition becomes falseWHILE condition LOOP
Exit condition is in the middle of the bodyLOOP ... EXIT WHEN ... END LOOP
Update many rows uniformlyDon't loop — single SQL statement
  1. Identify what makes the condition false before writing the body. If you can't point at the line, the loop is at risk of running forever.
  2. Initialize the loop variable before the loop. Don't depend on default values to control loop entry — set it explicitly.
  3. Don't use WHILE when set-based SQL works. One UPDATE ... WHERE usually beats a row-by-row loop.
  4. Use FOR for known integer ranges and FOR rec IN SELECT for query iteration. WHILE is for everything else.
  5. Be careful with NULL. A WHILE condition that evaluates to NULL exits the loop — sometimes that's what you want, sometimes it's a hidden bug.
  6. Add a safety counter for unbounded loops in production code: EXIT WHEN iterations > MAX_ITERATIONS. Defends against a logic bug that would otherwise lock up the database.
  • WHILE condition LOOP runs its body as long as the condition is true. It's the pre-test loop — condition is checked before each iteration, including the first.
  • If the condition is false (or NULL) on entry, the body never executes.
  • Inside the body, something must change so the condition eventually becomes false — otherwise you have an infinite loop.
  • NULL counts as not-true: a NULL condition exits the loop just like FALSE.
  • WHILE shines when each iteration depends on the previous one's result. For known ranges or query iteration, prefer FOR.
  • For uniform row updates, set-based SQL beats any loop.