Advanced SQL
Advanced SQL
Section titled “Advanced SQL”PL/pgSQL
Section titled “PL/pgSQL”DB9 supports PL/pgSQL functions and procedures with variable declarations and standard control flow.
Multi-line DECLARE entries parse correctly — a declaration whose type or value spans several
lines produces the same value as PostgreSQL, in both a DO block and a CREATE FUNCTION body:
DECLARE v int := 1 + 2; -- 3DECLARE s text := 'abc' || 'def'; -- 'abcdef'DECLARE v int := 42; -- 42Dynamic SQL and exception handling
Section titled “Dynamic SQL and exception handling”EXECUTE works in a CREATE FUNCTION body as well as a DO block. The command string may be a
literal, a variable, or an expression, and INTO and USING behave as in PostgreSQL:
CREATE FUNCTION count_rows(t TEXT) RETURNS INT AS $$DECLARE n INT;BEGIN EXECUTE 'SELECT count(*) FROM ' || quote_ident(t) INTO n; RETURN n;END;$$ LANGUAGE plpgsql;
CREATE FUNCTION double_it(p INT) RETURNS INT AS $$DECLARE n INT;BEGIN EXECUTE 'SELECT $1 * 2' INTO n USING p; RETURN n;END;$$ LANGUAGE plpgsql;Exception handling (BEGIN ... EXCEPTION) still requires a DO block:
DO $$BEGIN EXECUTE 'CREATE TABLE audit_snapshot(x int)';EXCEPTION WHEN others THEN RAISE NOTICE 'setup skipped';END;$$;Exception handlers follow PostgreSQL rollback semantics: statements that ran before the exception are rolled back, and only the handler’s effects persist.
CASE statements
Section titled “CASE statements”CASE works in both function bodies and DO blocks, in simple and searched form:
CREATE FUNCTION size_label(n INT) RETURNS TEXT AS $$BEGIN CASE WHEN n > 100 THEN RETURN 'large'; WHEN n > 10 THEN RETURN 'medium'; ELSE RETURN 'small'; END CASE;END;$$ LANGUAGE plpgsql;CREATE FUNCTION increment(val INT) RETURNS INT AS $$BEGIN RETURN val + 1;END;$$ LANGUAGE plpgsql;
CREATE FUNCTION safe_divide(a NUMERIC, b NUMERIC) RETURNS NUMERIC AS $$BEGIN RETURN a / NULLIF(b, 0);END;$$ LANGUAGE plpgsql;SELECT … INTO
Section titled “SELECT … INTO”SELECT ... INTO variable assigns a query result to a declared variable.
CREATE FUNCTION user_count() RETURNS INT AS $$DECLARE n INT;BEGIN SELECT count(*) INTO n FROM users; RETURN n;END;$$ LANGUAGE plpgsql;RETURNING … INTO
Section titled “RETURNING … INTO”DML statements inside PL/pgSQL functions can capture returned values into variables using RETURNING ... INTO. Works with INSERT, UPDATE, and DELETE.
CREATE FUNCTION create_order(p_item TEXT) RETURNS INT AS $$DECLARE new_id INT;BEGIN INSERT INTO orders (item) VALUES (p_item) RETURNING id INTO new_id; RETURN new_id;END;$$ LANGUAGE plpgsql;
CREATE FUNCTION archive_user(p_id INT) RETURNS TEXT AS $$DECLARE removed_name TEXT;BEGIN DELETE FROM users WHERE id = p_id RETURNING name INTO removed_name; RETURN COALESCE(removed_name, 'not found');END;$$ LANGUAGE plpgsql;Multiple columns can be captured into separate variables:
RETURNING id, name INTO v_id, v_name;If the DML statement affects zero rows, the target variables are set to NULL. Expressions (not just column names) are supported in the RETURNING list. Following PostgreSQL semantics, RETURNING ... INTO with a statement that returns more than one row will capture the first row.
Triggers
Section titled “Triggers”Row-level triggers on INSERT, UPDATE, and DELETE. FOR EACH STATEMENT is accepted but does not
get statement-level semantics — see the caution below.
CREATE FUNCTION audit_trigger() RETURNS TRIGGER AS $$BEGIN INSERT INTO audit_log (table_name, action, changed_at) VALUES (TG_TABLE_NAME, TG_OP, NOW()); RETURN NEW;END;$$ LANGUAGE plpgsql;
CREATE TRIGGER users_audit AFTER INSERT OR UPDATE ON users FOR EACH ROW EXECUTE FUNCTION audit_trigger();Supported trigger timing: BEFORE, AFTER. Supported events: INSERT, UPDATE, DELETE. A
BEFORE trigger function runs as PL/pgSQL: it can assign to NEW (NEW.name := upper(NEW.name);)
and the modified row is what gets stored, skip the row with RETURN NULL, or reject it with
RAISE EXCEPTION — conditionally, inside IF … END IF, as well as unconditionally:
CREATE FUNCTION reject_bad() RETURNS TRIGGER AS $$BEGIN IF NEW.name = 'bad' THEN RAISE EXCEPTION 'rejected'; END IF; RETURN NEW;END;$$ LANGUAGE plpgsql;
CREATE TRIGGER guard BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION reject_bad();
INSERT INTO users VALUES (1, 'bad'); -- ERROR: rejected (P0001) — nothing is writtenDROP TRIGGER on a trigger that does not exist fails with a bare internal error (XX000), where
PostgreSQL raises 42704. Use DROP TRIGGER IF EXISTS in migration scripts.
Sequences
Section titled “Sequences”CREATE SEQUENCE order_seq START WITH 1000 INCREMENT BY 1;SELECT NEXTVAL('order_seq');SELECT CURRVAL('order_seq');SELECT LASTVAL();SELECT SETVAL('order_seq', 2000);DROP SEQUENCE order_seq;Sequence options: START, INCREMENT, MINVALUE, MAXVALUE, CACHE, CYCLE.
ALTER SEQUENCE cannot change any of them after creation — it supports only OWNER TO and
OWNED BY, so RESTART, INCREMENT BY, RENAME TO and every other clause raise a bare
ERROR: internal error carrying 0A000. Use SETVAL() to reposition a sequence. Note
that DROP SEQUENCE does not check column defaults that depend on the sequence — see
DDL — Other DDL for both caveats.
See also: DDL — Identity columns for GENERATED ALWAYS AS IDENTITY and GENERATED BY DEFAULT AS IDENTITY.
Custom Types
Section titled “Custom Types”-- Enum typeCREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral');ALTER TYPE mood ADD VALUE 'excited';
-- Composite typeCREATE TYPE address AS (street TEXT, city TEXT, zip TEXT);Collations
Section titled “Collations”CREATE COLLATION my_collation (LOCALE = 'en_US.utf8');DROP COLLATION my_collation;A collation you create is real: it persists across sessions, and declaring it on a column changes how that column sorts and compares.
CREATE COLLATION my_collation (LOCALE = 'en_US.utf8');CREATE TABLE coll_demo (t TEXT COLLATE my_collation);INSERT INTO coll_demo VALUES ('B'), ('a');
SELECT t FROM coll_demo ORDER BY t;-- a-- BA TEXT column declared without a collation sorts the same way as COLLATE "C" — by byte value —
so the same two rows come back in the opposite order (B, then a). The same applies to
comparisons: ('a' COLLATE my_collation) < 'B' is true, while an uncollated 'a' < 'B' is false.
The catalog, however, does not reflect any of this. Two lookups you might reach for are misleading:
| Lookup | What it returns | Reality |
|---|---|---|
SELECT collname FROM pg_collation | only C, POSIX, default | your collation exists |
pg_attribute.attcollation for a COLLATE column | 100, the default OID — identical to a column declared with no collation | the column does carry the declared collation |
So do not use either one to check whether a collation exists. Create it again instead: if it is
already there, that fails with ERROR: collation "my_collation" already exists (42710) — and if it
was not, remember to DROP COLLATION the one you just made.