PL/SQL Cheat Sheet
Every PL/SQL pattern you reach for daily, on one page: block structure, cursors, exception handling, procedures/functions/packages, triggers, collections, and the BULK COLLECT/FORALL performance pattern. Bookmark it, or work through the underlying concepts in the free PL/SQL tutorial.
Block structure
Every PL/SQL block has the same three parts; DECLARE and EXCEPTION are optional.
DECLARE
v_salary NUMBER;
BEGIN
SELECT salary INTO v_salary FROM employees WHERE employee_id = 100;
DBMS_OUTPUT.PUT_LINE(v_salary);
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('No such employee');
END;
/
Common data types
| Type | Stores |
|---|---|
| NUMBER(p,s) | fixed or floating-point numbers |
| VARCHAR2(n) | variable-length string, up to 32767 bytes in PL/SQL |
| DATE | date and time, second precision |
| TIMESTAMP | date and time with fractional seconds |
| BOOLEAN | TRUE / FALSE / NULL -- PL/SQL only, not a SQL column type |
| %TYPE | anchors a variable's type to a column, e.g. employees.salary%TYPE |
| %ROWTYPE | anchors a record's structure to a table or cursor's row |
Control structures
| Construct | Syntax |
|---|---|
| IF | IF cond THEN ... ELSIF cond THEN ... ELSE ... END IF; |
| Simple CASE | CASE v_grade WHEN 'A' THEN ... ELSE ... END CASE; |
| Basic LOOP | LOOP ... EXIT WHEN cond; END LOOP; |
| WHILE LOOP | WHILE cond LOOP ... END LOOP; |
| FOR LOOP | FOR i IN 1..10 LOOP ... END LOOP; |
| Cursor FOR LOOP | FOR rec IN (SELECT * FROM employees) LOOP ... END LOOP; |
Cursors
An implicit cursor (SQL%...) is created automatically for every DML statement; an explicit cursor gives you control over a multi-row query.
| Attribute / syntax | Meaning |
|---|---|
| CURSOR c IS SELECT ...; | declare an explicit cursor |
| OPEN c; FETCH c INTO ...; CLOSE c; | the open/fetch/close lifecycle |
| c%FOUND / c%NOTFOUND | did the last FETCH return a row? |
| c%ROWCOUNT | rows fetched so far |
| c%ISOPEN | is the cursor currently open? |
| SQL%ROWCOUNT | rows affected by the last implicit DML statement |
| CURSOR c(p_dept NUMBER) IS ... | a parameterized cursor |
| FOR UPDATE / WHERE CURRENT OF c | lock fetched rows, then update/delete the current one |
Exception handling
| Exception | Raised when |
|---|---|
| NO_DATA_FOUND | a SELECT INTO returns zero rows |
| TOO_MANY_ROWS | a SELECT INTO returns more than one row |
| DUP_VAL_ON_INDEX | a unique constraint/index is violated |
| VALUE_ERROR | a conversion or size/precision error |
| ZERO_DIVIDE | division by zero |
| OTHERS | catch-all -- always list it last |
| RAISE_APPLICATION_ERROR(-20001, 'msg') | raise a custom error with a user-defined number/message |
BEGIN
...
EXCEPTION
WHEN NO_DATA_FOUND THEN NULL;
WHEN OTHERS THEN
RAISE_APPLICATION_ERROR(-20001, 'Unexpected: ' || SQLERRM);
END;
Procedures & functions
A function must return exactly one value and can be called from inside a SQL expression; a procedure performs an action and is invoked with CALL/EXEC.
CREATE OR REPLACE PROCEDURE give_raise(p_emp_id IN NUMBER, p_pct IN NUMBER) IS
BEGIN
UPDATE employees SET salary = salary * (1 + p_pct/100) WHERE employee_id = p_emp_id;
END;
/
CREATE OR REPLACE FUNCTION get_annual_salary(p_emp_id NUMBER) RETURN NUMBER IS
v_salary NUMBER;
BEGIN
SELECT salary * 12 INTO v_salary FROM employees WHERE employee_id = p_emp_id;
RETURN v_salary;
END;
/
Packages
A package groups related procedures, functions, and variables behind a public spec, hiding the implementation in the body.
CREATE OR REPLACE PACKAGE emp_pkg IS
PROCEDURE give_raise(p_emp_id NUMBER, p_pct NUMBER);
FUNCTION get_annual_salary(p_emp_id NUMBER) RETURN NUMBER;
END emp_pkg;
/
CREATE OR REPLACE PACKAGE BODY emp_pkg IS
PROCEDURE give_raise(p_emp_id NUMBER, p_pct NUMBER) IS BEGIN
UPDATE employees SET salary = salary * (1 + p_pct/100) WHERE employee_id = p_emp_id;
END;
FUNCTION get_annual_salary(p_emp_id NUMBER) RETURN NUMBER IS
v_salary NUMBER;
BEGIN
SELECT salary * 12 INTO v_salary FROM employees WHERE employee_id = p_emp_id;
RETURN v_salary;
END;
END emp_pkg;
/
Triggers
| Clause | Meaning |
|---|---|
| BEFORE / AFTER | fire before or after the triggering DML statement |
| INSTEAD OF | replaces the DML entirely -- used on views |
| FOR EACH ROW | row-level trigger; fires once per affected row |
| :NEW / :OLD | the new/old value of a column in a row-level trigger |
| WHEN (condition) | restricts a row-level trigger to matching rows |
CREATE OR REPLACE TRIGGER trg_salary_audit
BEFORE UPDATE OF salary ON employees
FOR EACH ROW
BEGIN
INSERT INTO salary_audit (employee_id, old_salary, new_salary, changed_on)
VALUES (:OLD.employee_id, :OLD.salary, :NEW.salary, SYSDATE);
END;
/
Collections
| Type | Characteristics |
|---|---|
| Associative array (INDEX BY) | key-value pairs, indexed by integer or string, not persisted in the database |
| Nested table | unbounded, dense-then-sparse after deletion, can be a column type |
| VARRAY | fixed maximum size, always dense, ordering preserved |
TYPE t_salary_tab IS TABLE OF NUMBER INDEX BY PLS_INTEGER;
v_salaries t_salary_tab;
v_salaries(1) := 5000;
BULK COLLECT & FORALL
Row-by-row PL/SQL loops over SQL are slow because of the repeated context switch between the SQL and PL/SQL engines. BULK COLLECT and FORALL batch that switch into one round trip.
DECLARE
TYPE t_ids IS TABLE OF employees.employee_id%TYPE;
v_ids t_ids;
BEGIN
SELECT employee_id BULK COLLECT INTO v_ids FROM employees WHERE department_id = 60;
FORALL i IN v_ids.FIRST .. v_ids.LAST
UPDATE employees SET salary = salary * 1.1 WHERE employee_id = v_ids(i);
END;
/
Dynamic SQL
Use EXECUTE IMMEDIATE when the table/column name or statement shape isn't known until runtime. Always bind values with USING instead of concatenating them -- concatenation is the classic SQL-injection mistake.
EXECUTE IMMEDIATE 'SELECT salary FROM ' || p_table_name || ' WHERE employee_id = :1'
INTO v_salary USING p_emp_id;
Practise what's on this page
Run any query on this page live against a real schema — free, no signup, results in under a second.
Open the SQL editor →