PL/SQL Debugging — DBMS_OUTPUT Memory Leak Prevention
47 DBMS_OUTPUT calls accumulated 2.
- DBMS_OUTPUT is the simplest PL/SQL debugger — but only works when the client explicitly enables buffer retrieval
- SQL Developer's built-in debugger supports breakpoints, variable inspection, and step-through execution against a live database session
- Exception-driven debugging uses strategic RAISE points to narrow failure scope without any client dependency
- The debugger attaches to the database session — not the code file — so connection context determines whether breakpoints fire
- Production insight: DBMS_OUTPUT buffers silently in PGA memory if the client never calls DBMS_OUTPUT.GET_LINES — the buffer grows until the session ends or ORA-20000 fires
- Biggest mistake: leaving DBMS_OUTPUT.PUT_LINE calls in production code — they consume PGA memory on every execution and provide zero value without a connected client reading the buffer
Debugging PL/SQL is fundamentally different from debugging application code because the execution happens inside the database engine, not in your IDE's runtime. You cannot attach a debugger to a running SQL statement the way you attach to a running Java or Python process. Instead, you have three tools: DBMS_OUTPUT for print-style debugging where you scatter trace messages and read them after execution, SQL Developer's interactive debugger for breakpoint-based step-through debugging against a live session, and strategic exception raising where you embed assertions that fail loudly with diagnostic context when the execution state diverges from expectations. Each tool has a specific use case, and using the wrong one in the wrong context is the most common debugging mistake in PL/SQL.
PL/SQL debugging requires tools that work inside the database execution context. Unlike application code where you attach a debugger to a running process on your machine, PL/SQL executes on the database server and returns results to the client. Every debugging tool must bridge this server-client boundary, and understanding how each tool bridges it determines whether your debugging session produces answers or wastes hours.
DBMS_OUTPUT is the most widely used PL/SQL debugging tool and the most misunderstood. Engineers add DBMS_OUTPUT.PUT_LINE calls, execute the procedure, see no output, and conclude the code is not executing. The output is buffered silently in the session's PGA memory until the client explicitly retrieves it. Without the client-side enablement step — SET SERVEROUTPUT ON in SQL*Plus or the DBMS Output panel in SQL Developer — every DBMS_OUTPUT.PUT_LINE call is a silent no-op that allocates memory and produces nothing visible.
SQL Developer provides an interactive debugger that supports breakpoints, variable inspection, call stack navigation, watch expressions, and step-through execution. It requires a direct database connection, specific debug privileges, and a DEBUG-compiled package. The debugger attaches to the database session, not the code file — understanding this distinction is critical for debugging procedures that are called by other procedures, triggers, or scheduler jobs.
Structured logging and exception-driven debugging are the production-grade alternatives. They persist diagnostic information in database tables, require no client connection, survive transaction rollbacks via autonomous transactions, and can be enabled or disabled at runtime through configurable log levels. This guide covers all four approaches, when to use each, and the mistakes that cost the most time.
DBMS_OUTPUT: The Simplest Debugger
DBMS_OUTPUT is Oracle's built-in print debugging mechanism. PUT_LINE writes text to a session-level buffer in PGA memory. The client retrieves the buffer contents via GET_LINES after the PL/SQL block completes. The critical misunderstanding is that DBMS_OUTPUT.PUT_LINE does not write to the screen — it writes to a server-side buffer that the client must explicitly read.
The client enablement step is mandatory. In SQL*Plus, run SET SERVEROUTPUT ON SIZE UNLIMITED before executing the PL/SQL block. In SQL Developer, open the DBMS Output panel (View > DBMS Output), click the green plus icon, and select your connection. Without this step, every DBMS_OUTPUT.PUT_LINE call is a silent no-op — the code runs, PGA memory is allocated for the buffer, but nothing is displayed.
The default buffer size varies by Oracle version but is limited. Exceeding the limit raises ORA-20000: ORU-10027: buffer overflow, which terminates the PL/SQL block. In Oracle 10.2 and later, calling DBMS_OUTPUT.ENABLE(buffer_size => NULL) removes the limit, but the buffer still accumulates in PGA memory. For long-running procedures that produce extensive output, the buffer can grow to hundreds of megabytes — this is the mechanism behind the production incident described earlier.
DBMS_OUTPUT output is available only after the PL/SQL block completes. You cannot see output line-by-line during execution — the entire buffer is flushed to the client at the end. This means you cannot use DBMS_OUTPUT for real-time progress monitoring during a long-running batch. For real-time visibility, use DBMS_APPLICATION_INFO.SET_MODULE and SET_ACTION, which update V$SESSION immediately and are visible to other sessions.
The conditional debug pattern wraps all DBMS_OUTPUT calls behind a boolean flag. When the flag is FALSE (the production default), the IF check short-circuits and PUT_LINE is never called — no buffer allocation, no PGA consumption. When the flag is TRUE (during debugging), the output flows normally. This pattern is the minimum viable safety net if DBMS_OUTPUT must exist in the codebase at all.
- PUT_LINE writes to a session-level PGA buffer — it does not write to the screen or any file
- The client must call GET_LINES (or enable SERVEROUTPUT) to retrieve the buffer contents after the PL/SQL block completes
- Without client enablement, the buffer fills silently — ORA-20000: ORU-10027 when it overflows
- Output is available only after the PL/SQL block completes — no real-time line-by-line visibility during execution
- Buffer limit defaults vary by version; use DBMS_OUTPUT.ENABLE(NULL) or SET SERVEROUTPUT ON SIZE UNLIMITED for unlimited
- DBMS_OUTPUT in production consumes PGA memory per session with zero debugging value — remove before deployment
SQL Developer Interactive Debugger
SQL Developer provides a full interactive debugger for PL/SQL that supports breakpoints, variable inspection, call stack navigation, watch expressions, and step-through execution. The debugger attaches to a live database session and intercepts execution when a breakpoint is hit — similar to debugging in any IDE, but with the critical difference that execution happens on the database server, not locally.
The debugger requires two privileges granted to the debugging user: DEBUG CONNECT SESSION (to attach the debugger to the session) and DEBUG ANY PROCEDURE (to debug procedures owned by other schemas). Without these privileges, the debugger silently fails to attach and breakpoints are ignored without any error message. This is the single most common reason breakpoints do not fire.
The package or procedure must be compiled with debug symbols before breakpoints work. Run ALTER PACKAGE pkg_name COMPILE DEBUG BODY to include debug information in the compiled object. Without debug symbols, the debugger cannot map the execution point to a source line, and breakpoints are silently skipped. By default, packages are compiled without debug symbols — you must explicitly request them.
The debugger attaches to the database session, not the code file open in the editor. This means you must execute the code within the same connection that the debugger is monitoring. If you open the package in one SQL Developer worksheet and run the calling code in a different worksheet — which may be using a different database connection — the breakpoints will not fire because the execution is happening in a different session. Verify by checking the connection name in the worksheet tab.
For procedures invoked by triggers, scheduler jobs, or external applications, the SQL Developer debugger cannot attach directly because those executions happen in separate sessions that the IDE does not control. For those scenarios, use structured logging to a table or exception-driven debugging with checkpoint assertions.
After debugging, always recompile the package without debug symbols: ALTER PACKAGE pkg_name COMPILE BODY. Debug-compiled code carries additional metadata that increases the compiled object size and can marginally affect execution performance. Production code should never carry debug symbols.
- Grant DEBUG CONNECT SESSION and DEBUG ANY PROCEDURE to the debugging user — without these, breakpoints silently fail
- Compile with ALTER PACKAGE pkg COMPILE DEBUG BODY before setting breakpoints — no debug symbols means no breakpoints
- Execute the code in the SAME connection that the debugger is attached to — a different worksheet may use a different session
- After debugging, recompile without DEBUG to remove debug overhead: ALTER PACKAGE pkg COMPILE BODY
- For trigger/job/external-app debugging, use structured logging to a table — the IDE debugger cannot attach to those sessions
Structured Logging: Production-Grade Debug Output
DBMS_OUTPUT is a development tool. It has no place in production code. Production debugging requires a structured logging package that writes to a persistent table with configurable log levels, timestamps, module identifiers, and session context.
The logging table captures everything DBMS_OUTPUT cannot: log level (DEBUG, INFO, WARN, ERROR), the module and procedure name, the message, the full error stack when applicable, the database session ID, the connected user, and a precise timestamp. Engineers query the table after the fact to trace execution flow — no client enablement required, no PGA memory consumed by an ever-growing buffer, no dependency on a connected client.
Log levels enable runtime filtering without code changes. In production, only WARN and ERROR are logged by default. During incident response, a DBA or on-call engineer can temporarily lower the threshold to DEBUG for a specific session by calling the set_level procedure. This captures detailed execution flow for the duration of the investigation without affecting other sessions and without redeploying code. When the investigation is complete, the level is restored to WARN.
The logging package must use an autonomous transaction for the INSERT. This is non-negotiable. If the calling transaction rolls back — which it will during error scenarios, the exact situations where you most need the log entry — a non-autonomous INSERT rolls back with it and the log entry is lost. PRAGMA AUTONOMOUS_TRANSACTION ensures the log entry is committed independently of the calling transaction.
Equally important: the logging package must handle its own exceptions internally. A failure inside the logger — a full tablespace, a constraint violation, any unexpected error — must never propagate to the calling procedure. The caller's business logic must succeed or fail on its own merits, not because the logging infrastructure had a problem. Wrap the INSERT in a BEGIN/EXCEPTION/END block that silently swallows any logging failure.
- DBMS_OUTPUT: client-dependent, session-scoped, PGA-resident, lost on disconnect, invisible to other sessions
- Structured logging: client-independent, persistent in a table, committed via autonomous transaction, queryable by anyone with SELECT
- DBMS_OUTPUT disappears when the session ends — structured logging persists until explicitly purged
- Autonomous transactions ensure log entries survive caller rollback — critical for error logging in failed transactions
- Log levels avoid production overhead — DEBUG is off by default, enabled per-session only during incident response
Exception-Driven Debugging: Strategic RAISE Points
Exception-driven debugging uses strategic RAISE_APPLICATION_ERROR calls at key checkpoints to narrow the scope of a failure. Instead of scattering DBMS_OUTPUT calls everywhere and hoping you see the output, you embed assertions that fail loudly with diagnostic context whenever the execution state diverges from what the code expects.
The pattern is simple: at each major checkpoint in the procedure, validate the expected state. If the state is unexpected — a NULL parameter, a wrong status, a row count that should be 1 but is 0 or 5 — raise a descriptive error that includes the checkpoint identifier, the expected value, and the actual value. When the error propagates through the PL/SQL call stack, each ORA-06512 entry in the stack trace tells you exactly which procedure and line the error passed through, and the RAISE_APPLICATION_ERROR message tells you exactly what was wrong.
This approach has three advantages over DBMS_OUTPUT debugging. First, the error message is always captured — in the application's error log, in the database alert log, in the client's error handler. No client enablement is required. Second, the checkpoint errors integrate naturally with the ORA-06512 stack trace, providing a breadcrumb trail through the call chain. Third, checkpoints serve double duty: they are debugging aids during development and input validation guards in production. A well-placed checkpoint catches bad data before it corrupts downstream state.
The checkpoints should be placed at four boundaries: input validation (are the parameters non-null and within valid ranges?), pre-query state (does the prerequisite data exist in the expected state?), post-query state (did the query return the expected number and type of results?), and post-action verification (did the DML affect the expected number of rows?).
Use RAISE_APPLICATION_ERROR with the TRUE flag as the third parameter to preserve the original error stack when re-raising inside an exception handler. Without TRUE, the original stack is discarded and replaced with the location of the RAISE_APPLICATION_ERROR call, which destroys diagnostic information.
- CP1 (input): validate parameters are non-null and within valid ranges before any database operation
- CP2 (pre-query): verify prerequisite data exists and is in the expected state before acting on it
- CP3 (post-query): verify query results match expectations before using them in downstream logic
- CP4 (post-action): verify DML affected the expected number of rows — catches concurrent modifications and race conditions
- Each checkpoint error includes expected value, actual value, and full context — the ORA-06512 stack becomes a diagnostic breadcrumb trail
Tracing Execution with DBMS_TRACE and DBMS_HPROF
DBMS_TRACE is Oracle's built-in execution tracing facility for PL/SQL. It records every line executed, every exception raised, and every call/return event. The trace output is written to the PLSQL_TRACE_EVENTS table (after running the tracetab.sql setup script) or to a server-side trace file, depending on the Oracle version and configuration. No code changes are required — you enable tracing, execute the code, and analyze the results.
DBMS_TRACE is more powerful than DBMS_OUTPUT because it captures execution flow automatically. Enable tracing before executing the PL/SQL block, and every line executed is recorded. This is invaluable for understanding execution flow in complex procedures with many conditional branches where you are not sure which path the code takes.
DBMS_TRACE has two primary modes: trace_all_lines records every line executed (complete visibility but large output and significant overhead), and trace_all_exceptions records only when exceptions are raised (smaller output, lower overhead, focused on error paths). For most debugging scenarios, trace_all_exceptions is sufficient and far less expensive.
DBMS_HPROF (Hierarchical Profiler), available since Oracle 11g, provides execution time profiling. It records the time spent in each PL/SQL function and procedure, identifies hotspots, and generates a call tree. This is the right tool when the problem is performance — a procedure that runs but takes too long — rather than correctness — a procedure that raises an error.
The overhead of DBMS_TRACE with trace_all_lines is significant — typically 2x to 10x slowdown depending on the procedure's complexity. DBMS_HPROF has lower overhead because it samples at the subprogram level, not the line level. Neither should be enabled in production outside a controlled maintenance window or on a targeted individual session.
- trace_all_lines slows execution by 2x to 10x — never enable in production without a maintenance window or session isolation
- trace_all_exceptions has much lower overhead — use this mode for production-adjacent debugging when line-level detail is not needed
- Trace data is stored in SYS-owned tables — clean up old runs to avoid tablespace growth
- DBMS_HPROF writes to a server-side directory object — verify the directory exists and has write permissions before profiling
- Always recompile without DEBUG symbols after tracing — debug-compiled code carries metadata overhead in production
Debugging Workflow: Choosing the Right Tool
Each PL/SQL debugging tool has a specific use case. Using the wrong tool wastes time — either because the tool cannot surface the information you need, or because the overhead is disproportionate to the problem.
DBMS_OUTPUT is appropriate for quick, interactive debugging in a development environment where you are connected via SQL*Plus or SQL Developer and can see the output immediately after execution. It is inappropriate for production, for batch processes that run without a connected client, and for any procedure that processes more than a handful of transactions.
The SQL Developer interactive debugger is appropriate for step-through debugging of complex logic where you need to inspect variable values at specific points in execution. It requires a direct connection, debug privileges, and debug-compiled code. It is inappropriate for debugging triggers, scheduler jobs, or procedures called by external applications that use their own sessions.
Structured logging is appropriate for production debugging, batch process monitoring, and any scenario where the debugging session must survive session termination, transaction rollback, or client disconnection. It is the only tool that works for triggers, scheduler jobs, and external application calls. The overhead is minimal — a single INSERT per log entry via autonomous transaction.
Exception-driven debugging with checkpoints is appropriate for input validation, state verification, and narrowing failure scope in procedures that process data through multiple stages. Checkpoints serve double duty as debugging aids and production guards. They are always captured in the error stack and require no client enablement.
DBMS_TRACE and DBMS_HPROF are appropriate for detailed execution flow analysis and performance profiling in development or controlled investigation environments. They are inappropriate for production due to overhead.
The decision tree: Can you reproduce the issue interactively? Use the SQL Developer debugger. Cannot reproduce but can run in development? Use DBMS_TRACE. Issue is in production and you cannot reproduce it? Use structured logging. Need to verify data state at specific points? Use checkpoint assertions. Quick check during development? DBMS_OUTPUT is fine.
- DBMS_OUTPUT: quick interactive checks during development — requires connected client, not for production
- SQL Developer debugger: step-through complex logic with variable inspection — requires debug privileges and same-connection execution
- Structured logging: production debugging, batch monitoring, trigger/job debugging — works everywhere, persists across sessions
- Checkpoint assertions: input validation and state verification — always captured in error stack, no client dependency
- DBMS_TRACE: execution path analysis in development — 2-10x overhead, not for production
- DBMS_HPROF: performance profiling — identifies time hotspots at the subprogram level
DBMS_OUTPUT Debugging Left in Production Caused 40% Memory Increase on Shared Server
- DBMS_OUTPUT.PUT_LINE buffers in PGA memory — 47 calls per transaction across 10,000 transactions accumulated 2.3 GB per session
- DBMS_OUTPUT in production is a memory leak waiting to happen — the buffer grows silently because no client retrieves it during batch execution
- Use a structured logging package with configurable log levels instead of ad-hoc DBMS_OUTPUT calls — it writes to a table, consumes negligible memory, and persists across sessions
- Pre-deployment checks should grep for DBMS_OUTPUT.PUT_LINE and block deployment if found outside dedicated debug utility packages
Key takeaways
Common mistakes to avoid
6 patternsLeaving DBMS_OUTPUT.PUT_LINE calls in production code
Assuming DBMS_OUTPUT is enabled by default
Setting breakpoints in SQL Developer without compiling the package with DEBUG
Using DBMS_OUTPUT for debugging triggers and scheduler jobs
Catching exceptions in WHEN OTHERS without logging or re-raising
Forgetting to remove debug compilation from production packages
Interview Questions on This Topic
Why does DBMS_OUTPUT.PUT_LINE sometimes show no output, and how do you fix it?
Frequently Asked Questions
That's PL/SQL. Mark it forged?
10 min read · try the examples if you haven't