PL/SQL (Oracle)
This assessment applies the coupling account — coupling as access to a shared resource, and the coupling floor [1], [2] — and the knowledge-embodiment theorem [3]; the language is assessed against its official reference [4].
1 Assessment
This page assesses PL/SQL in five parts: core coupling — what the language’s own constructs force; ecosystem coupling — what the surrounding platform, tooling, and community cost; engineering readiness — whether the artifact can be reproduced, verified, observed, worked in quickly, connected to other languages, and governed openly; the profile — the unscored facts a score cannot express; and the scoring detail — the mechanism marks and the arithmetic behind every score. A ✅ bullet marks a construct that helps. A ⚠️ or ❌ bullet marks a cost; the scoring model does not price every cost named here.
Core coupling 39/100 · Ecosystem coupling 61/100 · Engineering readiness 46/100
1.1 Core coupling
1.1.1 Type
- ✅ Static typing over SQL data types: Every variable, constant, parameter, and function return declares a type, and a mismatch is a compilation error
- ✅
%TYPEand%ROWTYPEanchor a declaration to the schema:emp%ROWTYPEfollows the table’s column list, so a column change does not require editing the declaration - ✅ Subtypes declare a constrained domain once:
SUBTYPEand a constrained type such asNUMBER(7,2)state a range or a shape and reuse it - ❌ NULL is not type-safe absence (T10): An unassigned variable and an unused column are NULL, and NULL propagates through expressions, so
IF x = 5is not true whenxis NULL - ✅ A
REF CURSORcarries a row set without fixing its row type:SYS_REFCURSORpasses a result between program units, so the producer and the consumer need not share a record type - ⚠️ Implicit conversion is allowed: Oracle converts between numeric and character types on its own, so a mismatch can pass compilation and fail at run time
1.1.2 Construction
- ✅ The package separates specification from body: The spec declares the public interface — types, variables, constants, exceptions, cursors, and subprograms — and the body holds the implementation, so a consumer binds to the spec
- ✅ Subprogram overloading is available in a package: Two subprograms with one name differ by parameter list, so a family of operations shares one name
- ✅ A cursor
FOR ... LOOPiterates the query result directly: Iteration ends at the result’s last row, so the code does not track a row count - ❌ Global mutable state by default (C1): A package variable is declared once and lives for the whole session, so every caller that touches the package sees and changes the same value
- ⚠️ Package initialisation runs on first use: An initialisation block in the body runs when the package is first touched in the session, so setup timing follows call order
- ⚠️ Triggers fire implicitly: A trigger runs in response to an insert, update, or delete, so the writer of a statement cannot see from the statement which code will run
1.1.3 Memory
- ✅ No manual memory management: The Oracle server owns SGA and PGA memory, and a block never names an address or frees a byte
- ✅ The server sizes the work areas: Automatic PGA management gives a sort or a hash operation its memory from
PGA_AGGREGATE_TARGET, so the block does not reserve it - ⚠️ A session-scoped collection holds its memory: An associative array or nested table kept in a package variable stays for the session, so retained state is a standing cost
- ⚠️ A bulk fetch needs a deliberate size limit:
BULK COLLECT ... LIMITbounds a fetch, and its absence can materialise a whole result set in PGA - ✅ A cursor is released by
CLOSE: The server reclaims the cursor’s memory, so the program does not track bytes
1.1.4 Error
- ✅ The block separates the normal path from the recovery path:
DECLARE ... BEGIN ... EXCEPTION ... END;gives a named handler section - ✅ Exceptions are matched by name: A predefined exception (
NO_DATA_FOUND,TOO_MANY_ROWS,ZERO_DIVIDE) and a user-declaredEXCEPTIONare caught by name, andWHEN OTHERScatches the remainder - ✅
RAISE_APPLICATION_ERRORsets a code and a message: A custom failure carries a number in the application range −20000 to −20999, so a caller can branch on it - ⚠️
WHEN OTHERShides the class of the failure (Er3): A handler that catches everything without loggingSQLCODEandSQLERRM, or that does not re-raise, lets the enclosing block see a normal return where the inner block failed - ⚠️ No composable result type (Er4): A failure cannot be returned as a value the caller must inspect, so error information does not compose through a call chain
1.1.5 Concurrency
- ✅ Transactions are the unit of concurrency:
COMMIT,ROLLBACK, andSAVEPOINTmake a unit of work atomic, so a block does not coordinate with other sessions statement by statement - ✅ An autonomous transaction decouples a log write:
PRAGMA AUTONOMOUS_TRANSACTIONcommits on its own, so the entry survives the caller’s rollback - ✅
SELECT ... FOR UPDATEnames the rows to lock: The lock is declared by the cursor text rather than by a separate call - ❌ Shared mutable state by default (K2): A package variable is shared with every program unit that runs in the session, and nothing in the declaration marks it private
- ⚠️ No language-level synchronisation and no race detection: PL/SQL has no mutex or atomic primitive, so serialisation rests on Oracle’s row locks and on the application’s lock ordering
- ✅
DBMS_LOCKprovides an explicit lock service: A named user lock serialises a critical section across sessions
1.1.6 Platform
- ✅ It implements the ISO SQL/PSM stored-module standard: Persistent stored modules are specified in ISO/IEC 9075 Part 4, so the package and block model is standard in concept
- ⚠️ It is implemented outside Oracle Database: TimesTen and IBM Db2 support PL/SQL, so there is more than one implementation, though the dialects are not fully interchangeable
- ❌ Cannot function outside one vendor (P1): A PL/SQL program needs an Oracle-family database, so the unit of deployment is an Oracle schema
- ❌ The useful surface is Oracle-specific:
DBMS_*server packages,ROWNUM, sequences, andSQL%ROWCOUNTare Oracle features, so the code cannot move to another engine unedited - ⚠️ A port to Transact-SQL is a rewrite, not a translation: Feature differences and different concurrency and locking models separate the two dialects
1.1.7 Trade-offs (not coupling)
- A session-scoped package variable is a deliberate cache: Keeping a value for the session trades hidden shared state for a repeated read
- Implicit conversion is convenient in short blocks: Letting numeric and character types mix keeps ad-hoc code short, at a cost that appears in code that must be reliable
- A trigger is a data-integrity mechanism: A rule kept in the database is enforced for every writer, where the same rule in the application would be visible at each call site
- Logic beside the data is the purpose of the language: A procedure that runs in the database avoids shipping rows to a client
- PL/SQL is not a general-purpose language: The assumption of an Oracle database is a scope decision
- Dynamic SQL buys flexibility:
EXECUTE IMMEDIATEruns DDL and changes object names in a statement, which static SQL cannot do
1.2 Ecosystem coupling
1.2.1 Performance
- ✅ Compilation happens at deployment: A package or procedure is compiled once and the stored form is reused, so a call does not re-parse the source
- ✅ Native compilation is selectable:
PLSQL_CODE_TYPE = NATIVEcompiles to machine code, so a hot subprogram does not pay the interpreter - ✅ Bulk processing amortises the SQL boundary:
FORALLandBULK COLLECTmove many rows per round trip, so per-row context switches do not dominate - ✅ The optimiser and the SGA serve the data path: Plan selection and buffer caching are the server’s job, not the block’s
- ⚠️ Cost follows the plan and the retained state: A poorly chosen plan, or a package variable holding a large collection, is a cost the unit cannot see
1.2.2 Ecosystem
- ❌ The ecosystem is proprietary (Ec4): The engine, the tooling, and the support come from Oracle under licence
- ✅ Tooling is mature and long-established: SQL*Plus, SQL Developer, and third-party IDEs have existed for decades
- ✅ Published practice is extensive: The PL/SQL language reference, the database manuals, and decades of books and courses document the language in detail
- ⚠️ Capability follows the licence: Tooling, support, and advanced features are licensed per seat or per processor, so a team reaches only what it has bought
- ⚠️ Enterprise code bases are long-lived and heavy: A schema commonly spans many versions, so a change has to keep the oldest callers working
1.2.3 Evolution
- ✅ Backward compatibility is strong: A program written for an early Oracle release generally still compiles and runs in the current one
- ✅ Edition-based redefinition supports an online change:
EDITIONABLEobjects let a new version of a package be deployed and switched over without downtime - ✅ Deprecation is announced before removal: Oracle documents deprecated features, so an upgrade is planned rather than discovered
- ⚠️ A language feature arrives with a database release: Adoption timing therefore follows the customer’s upgrade cycle, not a separate language release
- ⚠️ A new feature is unavailable on a pinned older database: Code that must run on an old release cannot use a recent construct
1.2.4 Domain
- ✅ Excellent fit for data-centric business logic: A procedure operates on tables with SQL directly, so a rule runs where the data is
- ✅ The language is a full procedural language inside SQL: Loops, records, collections, and subprograms live in the same unit as the queries
- ✅ Server packages reach outside the schema:
UTL_FILE,UTL_HTTP, andDBMS_SCHEDULERgive file, network, and job facilities from inside the database - ❌ Domain and vendor lock-in (D1): The language exists to run inside an Oracle database, so the Oracle data tier is the only domain
- ⚠️ A trigger can hide a domain rule: An invariant implemented in a trigger is enforced for every writer, but a reader of the application source cannot see it
- ⚠️ The language is not a general-purpose runtime: A user interface, a heavy text workload, or a high-rate service belongs in another language beside PL/SQL
1.2.5 Learning
- ✅ Moderate for a SQL-plus-Pascal background: The block structure follows Ada and Pascal, so a developer from those languages reads it quickly
- ⚠️ The SQL-to-PL/SQL boundary needs practice: A
SELECTinside a block must return exactly one row, soNO_DATA_FOUNDandTOO_MANY_ROWSare everyday cases - ⚠️ Package semantics take time: Spec versus body, per-session package state, and initialisation timing are not visible in the syntax
- ⚠️ Exception semantics reward careful reading: An exception in a nested block propagates to the enclosing handler unless it is handled there
- ✅ The reference documentation is complete: The Oracle Database PL/SQL Language Reference documents every construct with examples
1.2.6 Syntax
- ✅ Readable and keyword-led: Statements read as verbs —
INSERT INTO,FETCH,RAISE— so intent is visible - ✅ Blocks nest visibly: An inner
BEGIN ... ENDholds its own declarations and handler, so scope follows the indentation - ❌ Verbose (S1): Even a small unit needs a declaration section, a
BEGIN ... END, and an exception section, so the block structure adds lines to every subprogram - ⚠️
ENDcloses several constructs:END IF;,END LOOP;, and a packageENDmust each be matched, so a long body is read against its terminators - ⚠️ A large package body is one long file: The spec and the body are a unit boundary, not a file-per-subprogram rule
1.2.7 Testing
- ✅
utPLSQLis the established framework: An open-source test framework runs suites with assertions and reporting, so tests need not be hand-rolled - ✅ Transaction control isolates a test: A test can run and
ROLLBACK, leaving the database unchanged - ✅ The toolchain traces execution:
DBMS_PROFILER,DBMS_TRACE, and the hierarchical profiler report call counts and coverage - ⚠️ The base product ships no unit-test framework: SQL Developer offers a unit-testing feature, but the language itself has no test construct
- ⚠️ A suite depends on grants and deployment: The framework is installed in the database it tests, so permissions and schema state are preconditions
1.2.8 Security
- ✅ Bind variables remove the injection surface:
EXECUTE IMMEDIATE 'SELECT ... WHERE x = :1' USING vpasses the value out of band, so it cannot change the parse - ✅
DBMS_ASSERTvalidates an identifier before interpolation:SIMPLE_SQL_NAME,QUALIFIED_SQL_NAME, andENQUOTE_NAMEcheck the string - ✅ The rights model is explicit:
AUTHID DEFINERandAUTHID CURRENT_USERselect whether a unit runs with its owner’s privileges or the caller’s - ✅ Privileges are granted per object:
GRANT EXECUTE ON pkg TO rolescopes access, and the package specification is the grantable interface - ❌ Known vulnerability/injection (Q3):
EXECUTE IMMEDIATEandDBMS_SQLbuild a statement as text, so concatenated input can become code - ⚠️ Definer’s-rights code can escalate: A unit that runs with its definer’s rights reaches more than the caller, so it needs an audit
1.3 Engineering readiness
1.3.1 Reproducibility
- ❌ Builds are not byte-reproducible by default.
- ❌ Dependencies resolve without a lock by default.
1.3.2 Supply chain
- ⚠️ Published packages carry no signed provenance by default.
1.3.3 Observability
- ⚠️ No stable tracing and metrics standard.
- ⚠️ The standard library has no structured logging.
1.3.4 Feedback
- ✅ No charged mechanism in this dimension.
1.3.5 Interop
- ⚠️ No maintained portable target.
1.3.6 Governance
- ⚠️ One vendor decides the language’s direction.
- ⚠️ The specification is not public.
1.4 Profile
Unscored, and kept separate from every score on purpose. The profile records what a mechanism price cannot express: what a running program costs, how well a coding model and an agent work in the language, and whether a safety-critical path exists.
1.4.1 Energy and cost
What this axis asks: what does a running program cost in energy and time?
- ⚠️ Runs inside a database engine: energy and time are the engine’s, not the program’s.
1.4.2 AI and agent readiness
What this axis asks: how well does a coding model and an agent work in this language?
- ❌ Sparse training data: models write it unreliably [5].
1.4.3 Safety certification
What this axis asks: is there a certified or standards-qualified path to a safety-critical system?
- ⚠️ No certified or standards-qualified path for safety-critical systems.
1.5 Scoring detail
1.5.1 Core coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Type | T1 |
dynamic no static check | — | 0 |
T2 |
weak static | — | 0 | |
T3 |
dynamic/duck dispatch | — | 0 | |
T4 |
no sum types | — | 0 | |
T5 |
no generics | — | 0 | |
T6 |
no namespaces | — | 0 | |
T7 |
type erasure/reflection | — | 0 | |
T8 |
operator overloading | — | 0 | |
T9 |
inheritance required for polymorphism | — | 0 | |
T10 |
no null-safety | ✅ | −8 | |
T11 |
no module boundary | — | 0 | |
T12 |
no effect tracking | ✅ | −8 | |
T13 |
async/sync colouring | — | 0 | |
T14 |
unchecked foreign boundary | ✅ | −4 | |
| Type subtotal | −15 (−20) | |||
| Construction | C1 |
global mutable state by default | ✅ | −15 |
C2 |
no immutability by default | — | 0 | |
C3 |
constructors coupled to allocation | — | 0 | |
C4 |
no constructor/validation | — | 0 | |
| Construction subtotal | −15 | |||
| Memory | M1 |
manual deallocation | — | 0 |
M2 |
GC non-deterministic pauses | — | 0 | |
M3 |
no RAII | — | 0 | |
M4 |
no ownership model | — | 0 | |
| Memory subtotal | 0 | |||
| Error | Er1 |
no structured path | — | 0 |
Er2 |
unchecked exceptions only | — | 0 | |
Er3 |
errors ignored/non-exhaustive | ✅ | −4 | |
Er4 |
no composable result type | ✅ | −4 | |
| Error subtotal | −8 | |||
| Concurrency | K1 |
GIL/no parallel | — | 0 |
K2 |
shared mutable state default | ✅ | −8 | |
K3 |
manual sync no race detection | — | 0 | |
K4 |
no structured concurrency | — | 0 | |
K5 |
no primitives | — | 0 | |
| Concurrency subtotal | −8 | |||
| Platform | P1 |
cannot function outside one vendor/domain | ✅ | −15 |
P2 |
single-vendor but other uses | — | 0 | |
P3 |
legacy/discontinued platform | — | 0 | |
| Platform subtotal | −15 |
Core Dimensions score: 39/100 — total cost = −61 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.2 Ecosystem coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Performance | R1 |
interpreted no AOT/JIT | — | 0 |
R2 |
GC pauses | — | 0 | |
R3 |
JIT warmup | — | 0 | |
| Performance subtotal | 0 | |||
| Ecosystem | Ec1 |
no package manager | — | 0 |
Ec2 |
fragmented tooling | — | 0 | |
Ec3 |
small/niche/discontinued | — | 0 | |
Ec4 |
proprietary | ✅ | −8 | |
Ec5 |
platform-locked tooling | — | 0 | |
Ec6 |
monoculture | — | 0 | |
Ec7 |
legacy burden | — | 0 | |
| Ecosystem subtotal | −8 | |||
| Evolution | V1 |
breaking changes | — | 0 |
V2 |
frozen | — | 0 | |
V3 |
discontinued | — | 0 | |
V4 |
glacial standards | — | 0 | |
| Evolution subtotal | 0 | |||
| Domain | D1 |
domain/vendor lock-in | ✅ | −15 |
D2 |
poor domain fit | — | 0 | |
D3 |
framework forces architecture | — | 0 | |
| Domain subtotal | −15 | |||
| Learning | L1 |
steep | — | 0 |
L2 |
large surface/multi-paradigm | — | 0 | |
L3 |
sparse docs | — | 0 | |
| Learning subtotal | 0 | |||
| Syntax | S1 |
verbose | ✅ | −8 |
S2 |
inconsistent | — | 0 | |
S3 |
punctuation-heavy | — | 0 | |
| Syntax subtotal | −8 | |||
| Testing | G1 |
weak tooling | — | 0 |
G2 |
poor debugging | — | 0 | |
G3 |
runtime-only errors | — | 0 | |
| Testing subtotal | 0 | |||
| Security | Q1 |
memory-unsafe | — | 0 |
Q2 |
dynamic/weak typing risk | — | 0 | |
Q3 |
known vulnerability/injection | ✅ | −8 | |
| Security subtotal | −8 |
Meta Dimensions score: 61/100 — total cost = −39 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.3 Engineering readiness Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Reproducibility | Y1 |
build not reproducible | ✅ | −8 |
Y2 |
dependencies not pinned | ✅ | −8 | |
| Reproducibility subtotal | −15 (−16) | |||
| Supply chain | Y3 |
no signed provenance | ✅ | −8 |
Y4 |
registry without namespacing | — | 0 | |
| Supply chain subtotal | −8 | |||
| Observability | Y5 |
no stable tracing standard | ✅ | −8 |
Y6 |
no structured logging | ✅ | −4 | |
| Observability subtotal | −12 | |||
| Feedback | Y7 |
slow build | — | 0 |
Y8 |
no fast in-editor analysis | — | 0 | |
| Feedback subtotal | 0 | |||
| Interop | Y9 |
no foreign-function interface | — | 0 |
Y10 |
no portable target | ✅ | −4 | |
| Interop subtotal | −4 | |||
| Governance | Y11 |
single-vendor governance | ✅ | −8 |
Y12 |
closed specification | ✅ | −8 | |
| Governance subtotal | −15 (−16) |
Readiness Dimensions score: 46/100 — total cost = −54 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
Verdict: PL/SQL’s coupling is set by its host, not its syntax. The package separates a specification from its body, blocks and named exceptions give a structured failure path, and bind variables close the injection surface. Main strengths: interface and implementation are separable, error handling is structured, and server-side code runs beside its data. Main weaknesses: everything runs inside an Oracle database, a package variable is shared session state, every variable starts as NULL and NULL propagates, dynamic SQL turns concatenated text into code, and WHEN OTHERS is a standing invitation to swallow a failure.
The formal development. The definitions, theorems, and proofs behind this page are published in:
- On the Nature of Coupling: All Coupling as Shared Resource Access — Zenodo, 2026 — doi:10.5281/zenodo.21925122
- Coupling as a Consequence of Cohesion: Decomposition, the Coupling Floor, and Domain-Imposed Coupling — Zenodo, 2026 — doi:10.5281/zenodo.21925201
- On the Nature of Cohesion: Cohesion as Correctness Relative to a Partitioning Rule — Zenodo, 2026 — doi:10.5281/zenodo.21703193