PostgreSQL SELECT INTO PostgreSQL · Anonymous Block · SELECT INTO

PL/pgSQL SELECT INTO

Master PL/pgSQL SELECT INTO — the workhorse for reading single rows. Critical PostgreSQL distinction: plain SELECT INTO silently takes the first row on multi-row results (NO error, unlike MySQL). Use SELECT INTO STRICT for exactly-one-row enforcement. Worked examples cover single values, multiple targets, %ROWTYPE assignments, NO_DATA_FOUND/TOO_MANY_ROWS handling, and the FOUND variable.

The SELECT INTO statement runs a query and stores the result in one or more PL/pgSQL variables. It's the workhorse for reading single-row data from a table inside a procedural block.

Two forms exist:

  • SELECT ... INTO target FROM ... — quietly assigns the first row to target. If the query returns zero rows, target becomes NULL. If it returns multiple rows, only the first one is used and no error is raised.
  • SELECT ... INTO STRICT target FROM ... — same, but raises an exception when the query doesn't return exactly one row. Zero rows raises NO_DATA_FOUND; multiple rows raises TOO_MANY_ROWS.
⚠️ A frequent misconception: the original tutorial said "If the query returns multiple rows, SELECT INTO raises an error." That's wrong in PostgreSQL — that's MySQL behavior (where SELECT INTO errors on multi-row results without explicit limits). In PostgreSQL, plain SELECT INTO silently takes the first row; only SELECT INTO STRICT raises on multiple rows. Use STRICT explicitly when you need that guarantee.
SELECT column1, column2, ... INTO [STRICT] target1, target2, ... FROM table_name WHERE condition;
ElementMeaning
STRICTOptional. Forces exactly-one-row semantics. Raises NO_DATA_FOUND on zero rows, TOO_MANY_ROWS on multiple.
target1, target2, ...Variables that receive each column. Must match the query column list in count and compatible type.

The target can also be a single %ROWTYPE variable or RECORD variable, in which case the entire row is assigned at once.

PostgreSQL — Setup
DROP TABLE IF EXISTS employees;

CREATE TABLE employees (
    employee_id   SERIAL PRIMARY KEY,
    first_name    VARCHAR(50),
    last_name     VARCHAR(50),
    salary        NUMERIC(10, 2),
    department    VARCHAR(50)
);

INSERT INTO employees (first_name, last_name, salary, department) VALUES
    ('Alice',   'Johnson', 75000, 'Engineering'),
    ('Bob',     'Smith',   62000, 'Engineering'),
    ('Charlie', 'Davis',   88000, 'Sales'),
    ('Dana',    'Wilson',  55000, 'Sales');
Example 1 — Reading a Single Value

The simplest use: pull one column from one row into one variable:

PL/pgSQL — single value
DO $$
DECLARE
    emp_salary NUMERIC(10, 2);
BEGIN
    SELECT salary
    INTO   emp_salary
    FROM   employees
    WHERE  first_name = 'Alice';

    RAISE NOTICE 'Alice''s salary: %', emp_salary;
END $$;
OUTPUT
psql — single value
postgres=#DO $$ ... single value SELECT INTO ... $$;
NOTICE: Alice's salary: 75000.00
DO
Example 2 — Reading Multiple Values

Pull several columns into separate variables in one query — much cheaper than running two SELECTs:

PL/pgSQL — multiple values
DO $$
DECLARE
    emp_first  employees.first_name%TYPE;
    emp_last   employees.last_name%TYPE;
    emp_salary employees.salary%TYPE;
BEGIN
    SELECT first_name, last_name, salary
    INTO   emp_first, emp_last, emp_salary
    FROM   employees
    WHERE  employee_id = 3;

    RAISE NOTICE 'Employee: % %', emp_first, emp_last;
    RAISE NOTICE 'Salary:   %', emp_salary;
END $$;
OUTPUT
psql — multiple values
postgres=#DO $$ ... multiple-target SELECT INTO ... $$;
NOTICE: Employee: Charlie Davis
NOTICE: Salary: 88000.00
DO

Note the use of %TYPE for each variable — they automatically match the column types so a future ALTER TABLE doesn't break this code.

Example 3 — Plain SELECT INTO With Multiple Rows

Demonstrating the silent first-row behavior. The query matches multiple employees in the Engineering department, but plain SELECT INTO picks one without warning:

PL/pgSQL — silent first-row
DO $$
DECLARE
    emp_name TEXT;
BEGIN
    -- Two employees in Engineering — but no STRICT, no error
    SELECT first_name
    INTO   emp_name
    FROM   employees
    WHERE  department = 'Engineering';

    RAISE NOTICE 'Got: %', emp_name;
    RAISE NOTICE '(no error raised even though 2 rows matched)';
END $$;
OUTPUT
psql — silent first row
postgres=#DO $$ ... plain SELECT INTO ... $$;
NOTICE: Got: Alice
NOTICE: (no error raised even though 2 rows matched)
DO

The result is whichever row Postgres returned first — typically determined by storage order, not anything stable. Without an ORDER BY, the "first" row can change between queries.

Example 4 — SELECT INTO STRICT

Adding STRICT enforces exactly-one-row semantics. Multiple rows produce TOO_MANY_ROWS; zero rows produce NO_DATA_FOUND:

PL/pgSQL — STRICT errors
DO $$
DECLARE
    emp_name TEXT;
BEGIN
    SELECT first_name
    INTO   STRICT emp_name
    FROM   employees
    WHERE  department = 'Engineering';

    RAISE NOTICE 'Got: %', emp_name;
END $$;
OUTPUT
psql — too many rows
postgres=#DO $$ ... STRICT with 2 matches ... $$;
ERROR: query returned more than one row
CONTEXT: PL/pgSQL function inline_code_block line 5 at SQL statement
Example 5 — Catching STRICT Errors

The right pattern with STRICT: pair it with an EXCEPTION block to handle the "no row" or "too many rows" cases explicitly:

PL/pgSQL — STRICT + EXCEPTION
DO $$
DECLARE
    emp_name TEXT;
    target_id INTEGER := 999;   -- nonexistent
BEGIN
    BEGIN
        SELECT first_name
        INTO   STRICT emp_name
        FROM   employees
        WHERE  employee_id = target_id;

        RAISE NOTICE 'Found: %', emp_name;
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE NOTICE 'No employee with id %', target_id;
        WHEN TOO_MANY_ROWS THEN
            RAISE NOTICE 'Multiple employees with id % (data integrity issue)', target_id;
    END;

    RAISE NOTICE 'Block continued normally.';
END $$;
OUTPUT
psql — handled missing row
postgres=#DO $$ ... STRICT + EXCEPTION block ... $$;
NOTICE: No employee with id 999
NOTICE: Block continued normally.
DO
Example 6 — INTO a %ROWTYPE Variable

Pull an entire row in one shot with SELECT * INTO row_var:

PL/pgSQL — INTO ROWTYPE
DO $$
DECLARE
    emp employees%ROWTYPE;
BEGIN
    SELECT *
    INTO   STRICT emp
    FROM   employees
    WHERE  employee_id = 1;

    RAISE NOTICE 'Name:       % %',  emp.first_name, emp.last_name;
    RAISE NOTICE 'Salary:     %',    emp.salary;
    RAISE NOTICE 'Department: %',    emp.department;
END $$;
OUTPUT
psql — INTO ROWTYPE
postgres=#DO $$ ... SELECT * INTO STRICT emp ... $$;
NOTICE: Name: Alice Johnson
NOTICE: Salary: 75000.00
NOTICE: Department: Engineering
DO

After any data-modifying statement (including SELECT INTO without STRICT), the special boolean variable FOUND indicates whether a row was actually returned. It's the lightweight alternative to STRICT + EXCEPTION when you just want to know "did this match anything":

PL/pgSQL — FOUND check
DO $$
DECLARE
    emp_name TEXT;
BEGIN
    SELECT first_name
    INTO   emp_name
    FROM   employees
    WHERE  employee_id = 999;

    IF NOT FOUND THEN
        RAISE NOTICE 'No row matched.';
    ELSE
        RAISE NOTICE 'Matched: %', emp_name;
    END IF;
END $$;
OUTPUT
psql — FOUND check
postgres=#DO $$ ... IF NOT FOUND ... $$;
NOTICE: No row matched.
DO
Use plain SELECT INTO when…Use SELECT INTO STRICT when…
Zero rows is acceptable (you'll handle NULL)Zero rows is a real error you want to surface
You're using LIMIT 1 or ORDER BY ... LIMIT 1The query is meant to match exactly one row by primary key or unique constraint
You'll check FOUND afterwardsYou want the database to enforce one-row-only and error otherwise
💡 Default to STRICT for primary-key lookups. If you're querying by a unique key, "no rows" or "multiple rows" both indicate something is wrong with the data or the query — STRICT surfaces the problem immediately rather than silently masking it. Use plain SELECT INTO mainly for "find me any matching row, NULL is OK" situations.
  1. Default to STRICT for unique-key lookups — surfaces data anomalies instead of hiding them.
  2. Use %TYPE for target variables — automatic schema-tracking saves edits later.
  3. Use %ROWTYPE when reading whole rows — clearer than juggling individual scalars.
  4. Wrap STRICT in BEGIN ... EXCEPTION when zero / many rows are expected possibilities — handle them gracefully rather than aborting.
  5. Use IF NOT FOUND for the lightweight "did the query find anything" pattern when STRICT + EXCEPTION feels heavyweight.
  6. Don't rely on plain SELECT INTO with multi-row results. The "first row" is unspecified without ORDER BY; either add LIMIT 1 with explicit ordering, or use STRICT.
  7. Be aware of the MySQL/Oracle difference. Coming from those, the "multiple rows raise an error" mental model doesn't apply to PostgreSQL plain SELECT INTO.
  • SELECT INTO reads query results into PL/pgSQL variables. The target can be scalars, a %ROWTYPE, or a RECORD.
  • Plain SELECT INTO is forgiving: zero rows produces NULL; multiple rows silently picks the first one (no error).
  • SELECT INTO STRICT enforces exactly-one-row: NO_DATA_FOUND on zero rows, TOO_MANY_ROWS on multiple.
  • Use STRICT for primary-key / unique-key lookups; use plain SELECT INTO when "no match" is acceptable and you'll check FOUND.
  • Pair STRICT with BEGIN ... EXCEPTION handlers when you expect to handle missing rows gracefully.
  • The FOUND variable is the lightweight check after a plain SELECT INTO.