Introduction
PL/pgSQL seamlessly integrates with SQL queries, letting you execute SELECT, INSERT, UPDATE, and DELETE statements and capture their results into variables. This tight integration is what makes PL/pgSQL powerful -- you combine procedural control flow with the full power of SQL without leaving the database engine.
Key Concepts
- SELECT INTO: Captures the result of a query into one or more PL/pgSQL variables.
- STRICT: An optional modifier that raises an error if SELECT INTO returns zero rows or more than one row.
- FOUND: A boolean variable automatically set after most SQL statements -- TRUE if a row was returned/affected, FALSE otherwise.
- PERFORM: Executes a query and discards the result. Used when you call a function for its side effects.
- GET DIAGNOSTICS: Retrieves metadata about the last executed command, such as the number of affected rows.
Real World Context
A user registration function needs to: (1) check if the email already exists, (2) insert the new user, and (3) return the generated ID. SELECT INTO with FOUND handles the existence check, INSERT ... RETURNING INTO captures the new ID, and GET DIAGNOSTICS can verify exactly how many rows were affected -- all in one clean function.
Deep Dive
SELECT INTO
sqlDECLARE user_name TEXT; user_email TEXT; user_count INTEGER; BEGIN SELECT COUNT(*) INTO user_count FROM users; SELECT name, email INTO user_name, user_email FROM users WHERE id = 1; -- STRICT: raises error if not exactly 1 row SELECT name INTO STRICT user_name FROM users WHERE id = 1; END;
Checking for Found Rows
sqlCREATE FUNCTION get_user_name(user_id INTEGER) RETURNS TEXT AS $$ DECLARE result TEXT; BEGIN SELECT name INTO result FROM users WHERE id = user_id; IF NOT FOUND THEN RETURN 'User not found'; END IF; RETURN result; END; $$ LANGUAGE plpgsql;
PERFORM (No Results Needed)
Use PERFORM when you want to execute a query but do not need its result:
sqlPERFORM pg_notify('channel', 'message'); PERFORM 1 FROM users WHERE email = input_email; IF FOUND THEN RAISE EXCEPTION 'Email already exists'; END IF;
INSERT with RETURNING INTO
sqlCREATE FUNCTION create_order( p_user_id INTEGER, p_items JSONB ) RETURNS INTEGER AS $$ DECLARE new_order_id INTEGER; BEGIN INSERT INTO orders (user_id, status, created_at) VALUES (p_user_id, 'pending', NOW()) RETURNING id INTO new_order_id; INSERT INTO order_items (order_id, product_id, quantity) SELECT new_order_id, (item->>'product_id')::INTEGER, (item->>'quantity')::INTEGER FROM jsonb_array_elements(p_items) AS item; RETURN new_order_id; END; $$ LANGUAGE plpgsql;
GET DIAGNOSTICS
sqlDECLARE row_count INTEGER; BEGIN UPDATE products SET price = price * 1.1 WHERE category = 'electronics'; GET DIAGNOSTICS row_count = ROW_COUNT; RAISE NOTICE 'Updated % rows', row_count; END;
Common Pitfalls
- Forgetting to check FOUND -- SELECT INTO silently sets variables to NULL when no row matches. Always check FOUND after SELECT INTO to avoid operating on NULL values.
- Using SELECT without INTO inside PL/pgSQL -- A bare SELECT (without INTO) causes an error. Use PERFORM if you do not need the result.
Best Practices
- Use STRICT for single-row lookups -- It makes the intent clear and catches bugs where a query unexpectedly returns zero or multiple rows.
- Use RETURNING INTO instead of a separate SELECT -- Combining INSERT/UPDATE with RETURNING INTO is atomic and avoids an extra query.
Summary
- SELECT INTO captures query results into variables; add STRICT to enforce exactly one row.
- The FOUND variable indicates whether the last SQL statement returned or affected any rows.
- PERFORM discards results and is used for side-effect-only calls; GET DIAGNOSTICS retrieves row counts.
Code Examples
sql
CREATE FUNCTION get_user_name(user_id INTEGER) RETURNS TEXT AS $$
DECLARE
result TEXT;
BEGIN
SELECT name INTO result FROM users WHERE id = user_id;
IF NOT FOUND THEN
RETURN 'User not found';
END IF;
RETURN result;
END;
$$ LANGUAGE plpgsql;