PL/pgSQL Variables
Master PL/pgSQL variables — declaration syntax, common data types (INTEGER, NUMERIC, TEXT, BOOLEAN, DATE, TIMESTAMP, JSONB, arrays), NOT NULL constraints, := vs = assignment, and variable scope including shadowing. Includes the proper TIMESTAMP vs TIME usage for clock_timestamp() and PostgreSQL's 1-indexed arrays.
A variable is a named storage location declared in the DECLARE section of a block. It holds a single value of a specific type, optionally with an initial assignment, and can be read or written throughout the block's BEGIN ... END body.
| Element | Purpose |
|---|---|
variable_name | The identifier. Standard Postgres rules: starts with letter/underscore, up to 63 characters, case-insensitive unless quoted. |
CONSTANT | Optional. If present, the value can never be reassigned after declaration. |
data_type | Any PostgreSQL type — see the data-types page for %TYPE and %ROWTYPE alternatives. |
NOT NULL | Optional. If present, the variable cannot hold NULL — including by default. Requires an initial value. |
:= initial_value | Optional initial assignment. DEFAULT value works the same way. Without it, the variable starts as NULL. |
| Type | Holds | Example value |
|---|---|---|
INTEGER / INT | Whole numbers, ±2.1 billion | 42 |
BIGINT | Whole numbers, very large | 9000000000 |
NUMERIC(p, s) | Exact decimals — for money, scientific values | 1234.56 |
REAL / DOUBLE PRECISION | Floating point — for measurements, math | 3.14159 |
TEXT / VARCHAR(n) | Strings; TEXT is unbounded | 'Hello' |
BOOLEAN | TRUE, FALSE, or NULL | TRUE |
DATE | Calendar date, no time | '2026-04-22' |
TIMESTAMP | Date + time | '2026-04-22 14:30:00' |
TIMESTAMPTZ | Date + time + timezone | '2026-04-22 14:30:00+05:30' |
UUID | 128-bit identifier | 'abc...' |
JSONB | Binary JSON for structured data | '{"k": "v"}'::JSONB |
type[] | Array of any type | ARRAY[1,2,3] |
Various types declared and assigned, with both := and DEFAULT shown:
DO $$
DECLARE
counter INTEGER := 0;
pi NUMERIC(10,5) DEFAULT 3.14159;
greeting TEXT := 'Hello, world';
is_ready BOOLEAN := FALSE;
today DATE := CURRENT_DATE;
created_at TIMESTAMP := clock_timestamp();
BEGIN
counter := counter + 1;
is_ready := TRUE;
RAISE NOTICE 'counter: %', counter;
RAISE NOTICE 'pi: %', pi;
RAISE NOTICE 'greeting: %', greeting;
RAISE NOTICE 'is_ready: %', is_ready;
RAISE NOTICE 'today: %', today;
RAISE NOTICE 'created_at: %', created_at;
END $$;
created_at TIME = clock_timestamp() — same bug as the constants page. TIME stores only hours/minutes/seconds with no date, so the date portion of clock_timestamp() would be discarded. Use TIMESTAMP for timestamps.Marking a variable NOT NULL is a runtime guarantee — assigning NULL to it later raises an error. Useful for working values that should never be undefined:
DO $$
DECLARE
rate NUMERIC NOT NULL := 0.05;
BEGIN
RAISE NOTICE 'rate = %', rate;
rate := NULL; -- This raises an error
RAISE NOTICE 'after assignment: %', rate;
END $$;
rate NUMERIC NOT NULL; without an initializer is a compile error — the constraint says "this can never be NULL," but variables without an initial value start as NULL, contradicting the constraint immediately.Arrays are first-class types in PostgreSQL. Declare them with type[] and create them with ARRAY[...] literals or ARRAY(SELECT ...) subqueries:
DO $$
DECLARE
fibonacci INTEGER[] := ARRAY[0, 1, 1, 2, 3, 5, 8, 13];
cities TEXT[] := ARRAY['Chennai', 'Mumbai', 'Bengaluru'];
total INTEGER := 0;
i INTEGER;
BEGIN
-- Sum the array
FOR i IN 1 .. array_length(fibonacci, 1) LOOP
total := total + fibonacci[i];
END LOOP;
RAISE NOTICE 'Fibonacci: %', fibonacci;
RAISE NOTICE 'Sum: %', total;
RAISE NOTICE 'Array len: %', array_length(fibonacci, 1);
RAISE NOTICE 'First city: %', cities[1];
RAISE NOTICE 'All cities: %', cities;
END $$;
fibonacci[1] gives 0 (the first element), not the second. Coming from C-family languages (where arrays start at 0), this catches everyone at least once.Variables declared in an outer block are visible to nested blocks. If a nested block redeclares a variable with the same name, the inner declaration shadows the outer one — the outer variable becomes inaccessible inside the nested block, but resumes visibility when the nested block ends.
DO $$
DECLARE
x INTEGER := 10;
BEGIN
RAISE NOTICE 'Outer x = %', x;
DECLARE
x INTEGER := 100; -- Shadows the outer x
BEGIN
RAISE NOTICE 'Inner x = %', x;
END;
RAISE NOTICE 'Outer x again = %', x;
END $$;
The outer x wasn't modified — it was shadowed for the duration of the inner block, then visible again afterwards.
plpgsql.extra_warnings setting to flag shadowing automatically.PL/pgSQL has two assignment operators that mean the same thing — := and =. By convention, := is preferred because = is also the comparison operator inside expressions, and the visual difference helps reading.
DO $$
DECLARE
a INTEGER;
b INTEGER;
fetched_salary NUMERIC;
BEGIN
a := 10; -- Preferred
b = 20; -- Also valid; not recommended
-- Assign from a query with SELECT INTO
SELECT salary
INTO fetched_salary
FROM employees
WHERE first_name = 'Alice';
RAISE NOTICE 'a = %, b = %, salary = %', a, b, fetched_salary;
END $$;
- Declare every variable in
DECLARE— Postgres requires it; you can't introduce new variables mid-block. - Initialize at declaration when sensible — saves a separate assignment line and rules out NULL by accident.
- Use
:=for assignment,=for comparison — both work for assignment but the visual distinction helps. - Use
NOT NULLfor variables that should always have a value — but remember it requires an initializer. - Avoid shadowing. Different name = different concept; same name across nested blocks usually means a bug waiting.
- Use
%TYPEwhen the variable is tied to a column (see the data-types page). - Match types carefully —
TIMESTAMPfor timestamps (notTIME),NUMERIC(p,s)for decimals (not bareNUMERICfor money).
- Variables are declared in the
DECLAREsection withname [CONSTANT] type [NOT NULL] [:= initial];. - Without an initializer, variables start as NULL.
NOT NULLenforces non-null at runtime — and requires an initial value.- Use
:=for assignment by convention;=also works but is the comparison operator in expressions. - Variables in outer blocks are visible to nested blocks; nested redeclarations shadow them. Avoid this for code clarity.
- PostgreSQL arrays are 1-indexed;
array_length(arr, 1)returns the count.