Chapter 11 · PL/SQL Fundamentals: Blocks, Types, Procedures, Functions, and Packages

PL/SQL Block Structure, Variables, Records, Collections, Control Flow, and Scope

Use PL/SQL only where procedural orchestration adds value: build blocks, anchored variables, records, collections, control flow, and nested scope while keeping set work in SQL.

Intermediate → Advanced110–130 minutesBlock/scope + set-vs-procedural labOracle AI Database 26ai · RU 23.26.3 baselineOracle AI Database Free · SQLcl/SQL*Plus/SQL DeveloperLast reviewed: August 2026

Learning outcomes

ServiceHub needs to validate a batch request, derive several scalar values, call SQL conditionally, and emit a concise diagnostic message. One developer writes procedural logic into an oversized SQL expression; another loops through thousands of rows merely to update one column. PL/SQL is useful for orchestration, state, branching, exception handling, and reusable server-side APIs—but SQL remains the baseline for operations over sets of rows.

01

Describe the declaration, executable, and exception sections of anonymous and named PL/SQL blocks.

02

Use %TYPE and %ROWTYPE to anchor PL/SQL variables and records to database definitions.

03

Choose records and collection types deliberately, including associative arrays for in-memory keyed state.

04

Reason about IF/CASE/loops and lexical scope without hiding set operations inside unnecessary row-by-row loops.

05

Recognize SELECT INTO single-row semantics and repair TOO_MANY_ROWS by matching the procedural construct to the required result shape.

Prerequisite connection

Chapters 05–10 established Oracle SQL semantics, transactions, optimizer behavior, indexes, and cursor execution. PL/SQL sits beside SQL: procedural statements execute in the PL/SQL engine and embedded SQL is sent to the SQL engine. Good PL/SQL therefore starts by deciding which work belongs in SQL and which work truly requires procedural control.

Version, scope, privileges, tooling, and licensing baseline

Mandatory examples target a disposable ServiceHub schema in Oracle AI Database Free 26ai and were reviewed against RU 23.26.3, SQL Developer 26.2, and SQLcl 26.2.1. Free is limited to 2 foreground CPU cores, 2 GB combined SGA/PGA memory, and 12 GB user data and receives no Oracle patches or Support service requests. The chapter requires no Diagnostics Pack, Tuning Pack, RAC, Data Guard, Exadata, GoldenGate, OCI service, or COMPATIBLE change. Stored program creation in the course owner requires CREATE PROCEDURE; cross-schema execution requires explicit EXECUTE grants. Dynamic performance views used for optional session/PGA evidence should be read through a DBA observer or narrowly granted V_$ views in the disposable PDB.

1. Every PL/SQL block has an executable core

A PL/SQL block has an optional DECLARE section, a required BEGIN ... END executable section, and an optional EXCEPTION section. Anonymous blocks are submitted by a client and are not stored as named schema objects; stored procedures, functions, package bodies, and triggers contain named PL/SQL blocks compiled into the database.

sql · small anonymous block
SET SERVEROUTPUT ONDECLARE  l_open_count PLS_INTEGER;BEGIN  SELECT COUNT(*)  INTO l_open_count  FROM servicehub_plsql_case  WHERE status_code = 'OPEN';  DBMS_OUTPUT.PUT_LINE('open cases=' || l_open_count);EXCEPTION  WHEN OTHERS THEN    DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_ERROR_STACK);    RAISE;END;/

DBMS_OUTPUT is a teaching/debugging channel, not a production logging subsystem. SQLcl/SQL*Plus must enable SERVEROUTPUT to display its buffered lines.

2. Anchor types to the data model

%TYPE derives a PL/SQL variable's type from a column or another PL/SQL variable. %ROWTYPE creates a record whose fields match a table, cursor, or view row. Anchoring avoids duplicating type declarations and lets compatible schema changes flow into recompiled PL/SQL.

sql · %TYPE and %ROWTYPE
DECLARE  l_case_id servicehub_plsql_case.case_id%TYPE := 1;  l_row     servicehub_plsql_case%ROWTYPE;BEGIN  SELECT *  INTO l_row  FROM servicehub_plsql_case  WHERE case_id = l_case_id;  DBMS_OUTPUT.PUT_LINE(    l_row.case_id || ':' || l_row.status_code || ':' || l_row.amount  );END;/

Anchoring does not make schema changes risk-free. Dropping or renaming columns, changing semantic meaning, or modifying package specifications can still invalidate dependent code and require testing.

3. Records group fields; collections group elements

A PL/SQL record is a composite value with named fields. A collection is an ordered or keyed group of elements. PL/SQL supports associative arrays, nested tables, and varrays. Associative arrays are especially useful for session-local keyed working state.

sql · local record and associative array
DECLARE  TYPE t_summary IS RECORD (    status_code servicehub_plsql_case.status_code%TYPE,    case_count  PLS_INTEGER,    total_amt   NUMBER  );  TYPE t_count_by_status IS TABLE OF PLS_INTEGER    INDEX BY VARCHAR2(12);  l_counts t_count_by_status;BEGIN  l_counts('OPEN') := 0;  l_counts('CLOSED') := 0;  FOR r IN (    SELECT status_code, COUNT(*) AS case_count    FROM servicehub_plsql_case    GROUP BY status_code  ) LOOP    l_counts(r.status_code) := r.case_count;  END LOOP;  DBMS_OUTPUT.PUT_LINE('OPEN=' || l_counts('OPEN'));END;/

The SQL query performs the grouping as a set. PL/SQL merely consumes already-aggregated rows.

4. Control flow is for decisions and orchestration

Use IF/ELSIF/ELSE and CASE for branches, and basic/WHILE/FOR loops for genuine iteration. Prefer a cursor FOR loop over manual open/fetch/close when you simply need to process rows sequentially; Oracle manages the cursor lifecycle for the loop.

sql · conditional orchestration
DECLARE  l_total NUMBER;BEGIN  SELECT SUM(amount)  INTO l_total  FROM servicehub_plsql_case  WHERE status_code = 'OPEN';  IF NVL(l_total,0) > 10000 THEN    DBMS_OUTPUT.PUT_LINE('manual review required');  ELSIF NVL(l_total,0) > 0 THEN    DBMS_OUTPUT.PUT_LINE('normal queue');  ELSE    DBMS_OUTPUT.PUT_LINE('no open monetary exposure');  END IF;END;/

5. Lexical scope: inner declarations can hide outer names

PL/SQL uses lexical scope. An identifier declared in an inner block can hide an outer identifier with the same name. This is legal, but careless shadowing makes incident debugging difficult. Labels can qualify variables when demonstration is necessary, but production naming conventions should usually avoid the ambiguity.

sql · nested scope made explicit
<<outer_block>>DECLARE  l_status VARCHAR2(12) := 'OPEN';BEGIN  DECLARE    l_status VARCHAR2(12) := 'CLOSED';  BEGIN    DBMS_OUTPUT.PUT_LINE('inner=' || l_status);    DBMS_OUTPUT.PUT_LINE('outer=' || outer_block.l_status);  END;END outer_block;/

6. Deliberately wrong: SELECT INTO when the query can return many rows

SELECT ... INTO in PL/SQL is a single-row contract. Zero rows raises NO_DATA_FOUND; more than one row raises TOO_MANY_ROWS (ORA-01422). Adding an arbitrary row limit only hides the modeling error unless one arbitrary row is genuinely acceptable.

sql · wrong single-row assumption
DECLARE  l_case_id NUMBER;BEGIN  SELECT case_id  INTO l_case_id  FROM servicehub_plsql_case  WHERE status_code = 'OPEN';END;/-- With multiple OPEN rows: ORA-01422 / TOO_MANY_ROWS

Repair the contract: use an aggregate for one aggregate value, a cursor/collection for multiple rows, or a genuine unique predicate when the business model promises one row.

7. Set-based SQL remains the baseline

Do not loop through rows merely to apply the same relational transformation to each row. The SQL engine can update the qualifying set directly and optimize the access path as a whole.

sql · wrong shape: procedural row-by-row DML
BEGIN  FOR r IN (    SELECT case_id    FROM servicehub_plsql_case    WHERE status_code = 'OPEN'  ) LOOP    UPDATE servicehub_plsql_case    SET amount = amount * 1.02    WHERE case_id = r.case_id;  END LOOP;END;/
sql · preferred baseline: one set-based statement
UPDATE servicehub_plsql_caseSET amount = amount * 1.02WHERE status_code = 'OPEN';ROLLBACK;

Later lessons introduce bulk PL/SQL for cases where procedural per-item logic is unavoidable. Bulk processing is an optimization over row-by-row procedural SQL—not a reason to replace one correct SQL statement.

8. Reproducible lab

sql · setup
BEGIN  EXECUTE IMMEDIATE 'DROP TABLE servicehub_plsql_case PURGE';EXCEPTION  WHEN OTHERS THEN    IF SQLCODE != -942 THEN RAISE; END IF;END;/CREATE TABLE servicehub_plsql_case (  case_id      NUMBER PRIMARY KEY,  status_code  VARCHAR2(12) NOT NULL,  amount       NUMBER(10,2) NOT NULL);INSERT INTO servicehub_plsql_case VALUES (1,'OPEN',125);INSERT INTO servicehub_plsql_case VALUES (2,'OPEN',275);INSERT INTO servicehub_plsql_case VALUES (3,'CLOSED',90);COMMIT;
sql · anchored record and collection
DECLARE  TYPE t_amounts IS TABLE OF servicehub_plsql_case.amount%TYPE    INDEX BY PLS_INTEGER;  l_row      servicehub_plsql_case%ROWTYPE;  l_amounts  t_amounts;  l_total    NUMBER := 0;BEGIN  SELECT *  INTO l_row  FROM servicehub_plsql_case  WHERE case_id = 1;  SELECT amount  BULK COLLECT INTO l_amounts  FROM servicehub_plsql_case  WHERE status_code = 'OPEN'  ORDER BY case_id;  FOR i IN 1..l_amounts.COUNT LOOP    l_total := l_total + l_amounts(i);  END LOOP;  DBMS_OUTPUT.PUT_LINE('first status=' || l_row.status_code);  DBMS_OUTPUT.PUT_LINE('open total=' || l_total);END;/
sql · cleanup
DROP TABLE servicehub_plsql_case PURGE;

9. Production judgment

Use PL/SQL where server-side procedural state, branching, exception handling, encapsulation, or multi-step orchestration is valuable. Keep large relational transformations in SQL whenever practical. Anchor types where it improves dependency alignment, keep lexical scope shallow, and make row-count contracts explicit.

No special edition, option, pack, restart, parameter, or COMPATIBLE change is required. Lesson 2 turns blocks into reusable procedures/functions and adds a critical boundary: functions called from SQL must obey SQL-callability and side-effect rules.

Check your understanding

  1. What sections can an anonymous PL/SQL block contain?
  2. What does %ROWTYPE give you?
  3. Why is SELECT INTO a single-row contract?
  4. When should a SQL UPDATE be preferred over a PL/SQL loop of UPDATE statements?
  5. What risk comes from nested variables with the same name?
Review the answers

An optional DECLARE section, required BEGIN...END executable section, and optional EXCEPTION section.

A record whose fields correspond to the referenced table, cursor, or view row.

PL/SQL expects exactly one row; zero raises NO_DATA_FOUND and more than one raises TOO_MANY_ROWS.

When the same relational transformation applies to a qualifying set; SQL can optimize it as one operation and avoids procedural row-by-row crossings.

Inner declarations can hide outer variables, making reasoning and debugging ambiguous unless scope is made explicit.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.