ORA-01000: Unclosed Result Sets Took Down the Report Service
ORA-01000 means one session has more cursors open than OPEN_CURSORS allows.
20+ years shipping high-throughput database systems. Written from production experience, not tutorials.
- ✓Basic PL/SQL syntax (DECLARE/BEGIN/END blocks, explicit cursors, FOR loops)
- ✓Familiarity with Oracle session concepts (SID, SERIAL#, V$SESSION)
- ✓Ability to run queries against V$ views as a DBA or privileged user
- One session has more cursors open than OPEN_CURSORS. It's almost always a leak, not real demand.
- Find the session: V$SESSTAT 'opened cursors current'. Find the leaking SQL: GROUP BY sql_text in V$OPEN_CURSOR for that SID.
- Most common leaks: JDBC Statements or ResultSets not closed on error paths, REF CURSORs returned to the app and never closed, DBMS_SQL cursors without CLOSE_CURSOR.
- Fix the leak (try-with-resources, close REF CURSORs, cursor FOR loops). Raising OPEN_CURSORS only delays the failure.
Imagine a restaurant kitchen with a fixed number of cutting boards. Every dish you prep takes a board. If you never wash and return a board after plating, the kitchen runs out โ even though you're only cooking one dish at a time. ORA-01000 is the chef yelling 'no more boards!' because someone stacked dirty ones in the corner. The fix isn't buying more boards (raising OPEN_CURSORS); it's washing them (closing cursors).
ORA-01000 doesn't mean the database is overloaded. It means one session is holding on to cursors it never closed. Usually it's application code that opens a statement or result set and loses track of it on an error path. Because the limit is per session, and connection pools keep sessions alive for hours, the leak builds up quietly. Then one pooled connection crosses the limit and starts failing while the rest look healthy.
This guide shows what actually leaks cursors (and what doesn't), how to find the leaking session and statement in two queries, and why raising OPEN_CURSORS is a delay rather than a fix.
What Actually Counts as a Cursor (It's Not Just SELECT)
Every SQL statement a session runs needs a cursor: SELECT, INSERT, UPDATE, DELETE, and PL/SQL blocks. The OPEN_CURSORS parameter caps how many a single session can have open at once. What leaks and what doesn't matters here. Static SQL inside PL/SQL, and cursor FOR loops, are managed for you. Explicit cursors declared locally in a procedure are closed when the block exits. The things that leak are the ones whose lifetime you control: JDBC Statements and ResultSets, REF CURSORs handed to the application, DBMS_SQL cursors, and cursors declared at package level, which live as long as the session. OPEN_CURSORS defaults to 50, and most database setup templates set it to 300. The limit itself reserves no memory; only cursors that are actually open use any.
The Classic Leak: Cursors Closed Only on the Happy Path
In PL/SQL, the cursors that truly leak are the ones that outlive a single call. DBMS_SQL is the classic: DBMS_SQL.OPEN_CURSOR returns a number, and that cursor stays open until you call CLOSE_CURSOR or the session ends. If an exception skips the close, every failing call leaks one cursor, and in a pooled session those add up across requests. Package-level cursors behave the same way, because package state lives for the whole session. Note what does not leak: reopening a local explicit cursor that is still open raises ORA-06511 'cursor already open', and a local cursor is closed automatically when its block exits. The fix is to close in the exception handler too, or to avoid hand-managed cursors entirely.
REF CURSORs: Opened in the Database, Leaked in the App
A procedure that returns a SYS_REFCURSOR opens the cursor and hands responsibility to the caller. The database can't close it; only the client can, by closing the ResultSet and Statement. This is the most common source of ORA-01000 in Java and .NET applications. The code works perfectly in tests, then leaks on the one error path that skips the close. Inside PL/SQL, the same rule applies to cursor variables passed between subprograms. Whoever ends up holding a REF CURSOR must close it. Reopening the same cursor variable with OPEN ... FOR replaces its previous query, so the leak comes from cursors that are handed off and forgotten, not from reopening one variable.
Connection Pools: Why It Looks Random
ORA-01000 is per session, and a connection pool keeps each session alive for hours or days. A leak of one cursor per failed request builds up on whichever pooled connection served that request. Eventually one connection crosses the limit. From then on, every request routed to it fails, and every other connection works. That's why the error looks random and intermittent. The fix is in the code: close Statements and ResultSets in try-with-resources, so they close even when an exception is thrown. For immediate relief, evict or recycle the bad connection. Most pools support a maximum connection lifetime, and keeping it to hours rather than days limits how far a leak can grow before it's noticed.
Diagnosing the Leak: V$OPEN_CURSOR Is Your Friend
When ORA-01000 fires, you need to know what cursors are open and why. V$OPEN_CURSOR shows every open cursor per session, including the SQL text. This is the difference between guessing and knowing.
Run this query against the leaking session (you'll need the SID from V$SESSTAT). You'll see the exact SQL statements that are still open. If you see the same statement repeated 200 times, you've found your leak.
One caveat: V$OPEN_CURSOR only shows cursors that are currently open. If the session already errored out and closed everything, you're too late. Set up a monitoring job that snapshots this view every minute when opened cursors current exceeds a threshold.
The Band-Aid: Raising OPEN_CURSORS (And When It's Okay)
Raising OPEN_CURSORS is the fastest way to stop the errors, and the most overused fix. If the app leaks, a higher limit only means the leak runs longer before it fails, and it's harder to diagnose when it does. There are legitimate reasons to raise it. Apps that cache many prepared statements per connection, or ORMs with statement caching, can genuinely need several hundred cursors per session. The limit doesn't reserve memory, so a higher value costs nothing until cursors are actually opened. The real risk of a very high value is losing the early warning, not the memory. The rule: confirm there's no leak (a flat 'opened cursors current' over time), then size the limit with headroom, typically 500 to 1,000.
The Real Fix: Cursor FOR Loops and Exception-Safe Closing
The permanent fix is simple to state: close every cursor you open, on every path. In PL/SQL, prefer cursor FOR loops for static queries. They can't leak. When you need explicit control (BULK COLLECT with LIMIT, DBMS_SQL, REF CURSORs), close in the normal path and again in the exception handler, guarded by %ISOPEN or DBMS_SQL.IS_OPEN. In application code, try-with-resources (Java), using blocks (.NET) or context managers (Python) do the same job: the close happens however the block exits.
When NOT to Use Explicit Cursors at All
Explicit OPEN/FETCH/CLOSE code is often unnecessary. Since Oracle 10g, a cursor FOR loop fetches 100 rows at a time behind the scenes, so it's both shorter and fast. Keep explicit cursors for the cases that need them: BULK COLLECT with a LIMIT for large batches, REF CURSORs passed to other programs, and dynamic SQL through DBMS_SQL. In code review, when you see OPEN ... LOOP ... FETCH ... EXIT WHEN ... CLOSE, ask whether it could be a FOR loop. Usually it can.
BULK COLLECT without a LIMIT clause can consume gigabytes of PGA memory on large result sets. Always use LIMIT 100 or LIMIT 1000 โ the performance difference is negligible, but the memory difference is massive.Unclosed Result Sets Took Down the Report Service
- ORA-01000 is a leak report for one session, not a capacity problem.
- Close cursors on every path, including the error path, and alert on the trend before the limit is hit.
SELECT sid, value FROM v$sesstat st JOIN v$statname sn ON sn.statistic# = st.statistic# WHERE sn.name = 'opened cursors current' ORDER BY value DESC FETCH FIRST 5 ROWS ONLY;SELECT sql_text, COUNT(*) FROM v$open_cursor WHERE sid = <sid> GROUP BY sql_text ORDER BY 2 DESC;| File | Command / Code | Purpose |
|---|---|---|
| cursor_count_check.sql | SELECT name, value | What Actually Counts as a Cursor (It's Not Just SELECT) |
| dbms_sql_leak.sql | CREATE OR REPLACE FUNCTION count_rows(p_table VARCHAR2) RETURN NUMBER IS | The Classic Leak |
| refcursor_contract.sql | CREATE OR REPLACE PROCEDURE get_report(p_day DATE, p_rows OUT SYS_REFCURSOR) IS | REF CURSORs |
| CursorLeakExample.java | public class CursorLeakExample { | Connection Pools |
| diagnose_open_cursors.sql | SELECT sid, value | Diagnosing the Leak |
| raise_open_cursors.sql | SHOW PARAMETER open_cursors | The Band-Aid |
| safe_cursor_patterns.sql | BEGIN | The Real Fix |
| bulk_collect_example.sql | DECLARE | When NOT to Use Explicit Cursors at All |
Key takeaways
Interview Questions on This Topic
How does Oracle count cursors per session, and what happens when you hit the limit?
opened cursors current in V$SESSTAT per session. When a session tries to open a cursor and the count equals OPEN_CURSORS, it raises ORA-01000 and the statement fails. The session remains alive โ it's not killed โ but any further cursor opens will also fail until some cursors are closed.Frequently Asked Questions
20+ years shipping high-throughput database systems. Written from production experience, not tutorials.
That's PL/SQL. Mark it forged?
3 min read · try the examples if you haven't