PL/SQL — Avoiding TOO_MANY_ROWS Errors in Batch Jobs
ORA-01422 from duplicate employee_id kills nightly batch job.
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- PL/SQL adds procedural logic (if, loop, error handling) on top of SQL
- Every program is a block: DECLARE (optional), BEGIN (required), EXCEPTION (optional), END
- Variables use %TYPE to automatically match column definitions — avoids hardcoded types
- Blocks run inside the database engine, cutting network round trips by up to 100x
- Biggest mistake: forgetting SET SERVEROUTPUT ON — DBMS_OUTPUT.PUT_LINE silently discards output
SQL is like giving orders at a counter: 'Give me all customers from London.' One instruction, one response. PL/SQL is like handing the counter a recipe card: 'Look up customers from London, if there are more than 100 send a report, otherwise send a reminder — and repeat this every Monday.' PL/SQL gives SQL the ability to make decisions, loop, and remember state.
SQL is declarative — you describe what data you want and the database figures out how to get it. But real-world database logic often requires conditional branching, loops, error handling, and reusable procedures. That's what PL/SQL adds.
PL/SQL (Procedural Language extension to SQL) is Oracle's procedural extension to SQL. It runs inside the Oracle database engine itself, which means it avoids the round-trip overhead of sending individual SQL statements from an application. A loop that issues 1000 SQL statements from Java sends 1000 network requests. The same loop in PL/SQL sends one.
By the end of this article you'll understand the PL/SQL block structure, how to use variables, write conditionals and loops, and handle exceptions — the four pillars of every PL/SQL program.
What PL/SQL's TOO_MANY_ROWS Error Actually Means
PL/SQL is Oracle's procedural extension to SQL, embedding imperative logic directly in the database. The TOO_MANY_ROWS exception fires when a SELECT INTO statement returns more than one row — a hard runtime error, not a warning. This is a fundamental contract: SELECT INTO expects exactly one row; anything else is an exception.
In batch jobs, this bites teams because a single unexpected duplicate or data drift can kill an entire multi-hour run. The error surfaces at the exact row where the violation occurs, leaving no partial results and no easy rollback. Unlike a bulk COLLECT which gracefully handles zero or many rows, SELECT INTO is brittle by design — it enforces a one-row guarantee at the cost of zero tolerance for variance.
Use SELECT INTO only when the query's cardinality is guaranteed by a unique constraint or primary key. For batch processing where data quality may degrade over time, prefer explicit cursor loops with a counter check or BULK COLLECT with LIMIT. The real cost isn't the error itself — it's the lost batch window and the manual recovery effort.
The PL/SQL Block Structure
Every PL/SQL program is a block. Blocks have four sections: DECLARE (optional, for variables), BEGIN (required, the logic), EXCEPTION (optional, error handling), and END. Blocks can be anonymous (run once, not stored) or named (procedures and functions stored in the database). Understanding this structure is the foundation of everything else in PL/SQL.
You'll see the slash (/) at the end — that's what tells SQL*Plus, SQL Developer, and most tools to actually execute the block. Without it, nothing happens.
Variables, Conditions, and Loops
PL/SQL variables are strongly typed. The %TYPE attribute lets you declare a variable that automatically matches a table column's data type — if the column type changes, your variable adapts automatically. %ROWTYPE does the same for an entire row structure. Conditionals use IF/ELSIF/ELSE. Loops come in three flavours: basic LOOP, WHILE, and the most common FOR loop. The FOR loop is preferred for simple iterations because it manages the loop counter implicitly and you don't need a separate variable declaration.
Exception Handling in PL/SQL
PL/SQL provides an EXCEPTION block where you can catch and handle errors gracefully. Without it, any runtime error terminates the block and rolls back uncommitted changes. Common built-in exceptions include NO_DATA_FOUND, TOO_MANY_ROWS, DUP_VAL_ON_INDEX, and ZERO_DIVIDE. You can also define your own exceptions using RAISE_APPLICATION_ERROR.
The WHEN OTHERS clause catches every unhandled exception. In production code, every block should include at least WHEN OTHERS to log the error and decide whether to re-raise or continue.
Working with Cursors
When you need to process multiple rows from a query, use a cursor. An explicit cursor is declared, opened, fetched, and closed manually. PL/SQL also offers implicit cursors via the FOR loop — the simplest and most common approach for single-table queries. For large row sets, BULK COLLECT speeds processing by fetching many rows at once into a collection, drastically reducing context switches between SQL and PL/SQL.
Stored Procedures and Functions
While anonymous blocks run once, stored procedures and functions live in the database and can be called repeatedly — by other PL/SQL, from application code, or even directly from SQL. Procedures perform actions (INSERT, UPDATE, etc.) and can have OUT parameters to return values. Functions return a single value and can be used in SQL statements if they are free of side effects (i.e., they don't modify database state). Packages group related procedures, functions, types, and variables together, providing encapsulation and namespace management.
Why PL/SQL Exists (And Why You Should Care)
SQL is declarative. You tell the database what you want, not how to get it. That's fine until you need to validate data across three tables, roll back a transaction when a constraint fails, or loop through result sets and fire off audit records. That's where SQL breaks.
PL/SQL is Oracle's procedural extension. It lets you wrap SQL in logic: if-then-else, loops, exception handling, and stateful variables. Same database, same data, but now you control the sequence of operations. It's not a separate language — it's SQL with a brain.
Every time you write a stored procedure, trigger, or anonymous block, you're telling the database engine: "Execute these steps in order, and if something breaks, do this instead." That's the entire point. No ORM, no middleware, no network round-trips for simple business rules. The database becomes the application server.
What PL/SQL Won't Tell You (The Hidden Features)
Every tutorial lists the same features: tight SQL integration, error checking, loops, conditionals. Fine. Here's what they bury: PL/SQL gives you bulk operations that make row-by-row processing look like a relic. The FORALL statement and BULK COLLECT are not optional — they're the difference between a query that finishes in three seconds and one that still runs during lunch.
Another hidden gem: associative arrays (index-by tables). They work like hash maps in the database. Need to cache a lookup table for a single session? That's a PL/SQL associative array, not a temp table. No I/O, no parsing, instant access.
And the autoreconnect trick: PL/SQL won't save you from a network blip. But if your transaction is wrapped in a loop with SAVEPOINT and EXCEPTION handling, you can retry the failed step without blowing away the entire transaction. That's not in the brochure.
Finally: autonomous transactions. PRAGMA AUTONOMOUS_TRANSACTION lets you commit logging or auditing independently of your main transaction. Audit trail survives even if the main rollback happens. Use it sparingly — it breaks atomicity — but when you need it, nothing else works.
BULK COLLECT when you're fetching more than 100 rows. For updates, pair it with FORALL. Your DBA will thank you, and your users won't timeout.Who Actually Needs PL/SQL in 2024?
If you're building a CRUD app with Rails, Django, or Node and your database is just a dumb store, you probably don't need PL/SQL. But the moment you need data integrity that survives application bugs, or you're processing millions of rows nightly, or your compliance team demands auditable transaction logs, you do.
PL/SQL is for: backend engineers who maintain legacy Oracle systems (they're still everywhere in finance, healthcare, and logistics). Data engineers running ETL pipelines that transform data inside the database. DevOps folks who need to write database migrations that actually roll back cleanly. And architects who understand that pushing business logic into the database isn't 'old school' — it's the difference between eventual consistency and actual consistency.
The job market backs this up. Oracle PL/SQL developers consistently rank in the top 10 highest-paid database roles. Why? Because companies running Oracle at scale can't find people who understand both SQL and procedural logic. They're desperate for engineers who can open a procedure and debug it instead of rewriting everything in Java.
If you're a junior who thinks PL/SQL is dead, you're wrong. It's just not trendy. But trendiness doesn't keep bank transactions atomic or hospital records accurate. PL/SQL does.
Unhandled TOO_MANY_ROWS Takes Down Nightly Batch
MAX() to guarantee a single row.- Always use exception handlers for NO_DATA_FOUND and TOO_MANY_ROWS when using SELECT INTO.
- For production code, prefer explicit cursors with FOR loops — they handle zero, one, or many rows gracefully.
- Test with edge-case data after any migration that affects lookup tables.
SELECT COUNT(*) FROM employees WHERE department_id = 10;ADD EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE('No rows');| File | Command / Code | Purpose |
|---|---|---|
| first_block.sql | DECLARE | The PL/SQL Block Structure |
| loops_and_conditions.sql | DECLARE | Variables, Conditions, and Loops |
| exception_handling.sql | DECLARE | Exception Handling in PL/SQL |
| cursors.sql | DECLARE | Working with Cursors |
| procedures_functions.sql | CREATE OR REPLACE PROCEDURE raise_salary( | Stored Procedures and Functions |
| WhyPLSQLMatters.sql | SELECT status FROM orders WHERE order_id = 1001; | Why PL/SQL Exists (And Why You Should Care) |
| BulkCollectDemo.sql | DECLARE | What PL/SQL Won't Tell You (The Hidden Features) |
| WhoNeedsIt.sql | DECLARE | Who Actually Needs PL/SQL in 2024? |
Key takeaways
Interview Questions on This Topic
What are the four sections of a PL/SQL block, and which are optional?
Frequently Asked Questions
20+ years shipping high-throughput database systems. Notes here come from systems that actually shipped.
That's PL/SQL. Mark it forged?
4 min read · try the examples if you haven't