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
  • ✅ %TYPE and %ROWTYPE anchor a declaration to the schema: emp%ROWTYPE follows the table’s column list, so a column change does not require editing the declaration
  • ✅ Subtypes declare a constrained domain once: SUBTYPE and a constrained type such as NUMBER(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 = 5 is not true when x is NULL
  • ✅ A REF CURSOR carries a row set without fixing its row type: SYS_REFCURSOR passes 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 ... LOOP iterates 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 ... LIMIT bounds 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-declared EXCEPTION are caught by name, and WHEN OTHERS catches the remainder
  • ✅ RAISE_APPLICATION_ERROR sets 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 OTHERS hides the class of the failure (Er3): A handler that catches everything without logging SQLCODE and SQLERRM, 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, and SAVEPOINT make 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_TRANSACTION commits on its own, so the entry survives the caller’s rollback
  • ✅ SELECT ... FOR UPDATE names 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_LOCK provides 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, and SQL%ROWCOUNT are 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 IMMEDIATE runs 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 = NATIVE compiles to machine code, so a hot subprogram does not pay the interpreter
  • ✅ Bulk processing amortises the SQL boundary: FORALL and BULK COLLECT move 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: EDITIONABLE objects 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, and DBMS_SCHEDULER give 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 SELECT inside a block must return exactly one row, so NO_DATA_FOUND and TOO_MANY_ROWS are 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 ... END holds 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
  • ⚠️ END closes several constructs: END IF;, END LOOP;, and a package END must 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

  • ✅ utPLSQL is 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 v passes the value out of band, so it cannot change the parse
  • ✅ DBMS_ASSERT validates an identifier before interpolation: SIMPLE_SQL_NAME, QUALIFIED_SQL_NAME, and ENQUOTE_NAME check the string
  • ✅ The rights model is explicit: AUTHID DEFINER and AUTHID CURRENT_USER select whether a unit runs with its owner’s privileges or the caller’s
  • ✅ Privileges are granted per object: GRANT EXECUTE ON pkg TO role scopes access, and the package specification is the grantable interface
  • ❌ Known vulnerability/injection (Q3): EXECUTE IMMEDIATE and DBMS_SQL build 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:

All published papers

References

[1]
Y. Loth, “On the nature of coupling: All coupling as shared resource access.” Zenodo, Aug. 2026. doi: 10.5281/zenodo.21925122.
[2]
Y. Loth, “Coupling as a consequence of cohesion: Decomposition, the coupling floor, and domain-imposed coupling.” Zenodo, Aug. 2026. doi: 10.5281/zenodo.21925201.
[3]
Y. Loth, “On the nature of cohesion: Cohesion as correctness relative to a partitioning rule.” Zenodo, Jul. 2026. doi: 10.5281/zenodo.21703193.
[4]
Oracle Corporation, “Database PL/SQL language reference.” Oracle, 2024. Available: https://docs.oracle.com/en/database/oracle/oracle-database/26/lnpls/
[5]
S. Joel, J. J. Wu, and F. H. Fard, “A survey on LLM-based code generation for low-resource and domain-specific programming languages,” ACM Transactions on Software Engineering and Methodology, 2025, doi: 10.1145/3770084.