Transaction Control (COMMIT, ROLLBACK, SAVEPOINT)
Transaction control statements are vital for ensuring data integrity and consistency in your Oracle database applications. They allow you to group DML operations into logical units of work. If all operations within a unit succeed, you COMMIT
them, making the changes permanent. If any operation fails or you decide to revert changes, you ROLLBACK
them, undoing all operations since the last COMMIT
or ROLLBACK
. SAVEPOINT
offers finer-grained control within a transaction.
Note: Understanding transaction control is paramount for building reliable PL/SQL applications. Without proper use of COMMIT
, ROLLBACK
, and SAVEPOINT
, your data can become inconsistent, leading to errors and loss of data integrity. Always consider the transactional boundaries of your Oracle PL/SQL code.
Example 1: Basic COMMIT and ROLLBACK
DECLARE
v_employee_id NUMBER := 208;
v_first_name VARCHAR2(50) := 'Alice';
v_last_name VARCHAR2(50) := 'Smith';
v_email VARCHAR2(100) := 'ASMITH@example.com';
BEGIN
-- Start of a transaction
INSERT INTO employees (employee_id, first_name, last_name, email, hire_date, job_id, department_id, salary)
VALUES (v_employee_id, v_first_name, v_last_name, v_email, SYSDATE, 'SA_REP', 80, 5000);
DBMS_OUTPUT.PUT_LINE('Employee ' || v_first_name || ' ' || v_last_name || ' inserted.');
-- If everything is successful, commit the transaction
COMMIT;
DBMS_OUTPUT.PUT_LINE('Transaction committed.');
-- Now, attempt another operation that will be rolled back
INSERT INTO employees (employee_id, first_name, last_name, email, hire_date, job_id, department_id, salary)
VALUES (v_employee_id, 'Duplicate', 'Entry', 'DUPLICATE@example.com', SYSDATE, 'SA_REP', 80, 5000); -- This will cause DUP_VAL_ON_INDEX
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
DBMS_OUTPUT.PUT_LINE('Caught DUP_VAL_ON_INDEX. Rolling back the last INSERT.');
ROLLBACK; -- Rollback the second insert because of the error
DBMS_OUTPUT.PUT_LINE('Transaction rolled back.');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An unexpected error occurred: ' || SQLERRM);
ROLLBACK; -- Rollback all pending changes
END;
/
-- Verify if the first insert committed and the second one did not
-- SELECT employee_id, first_name, last_name FROM employees WHERE employee_id = 208;
Explanation This example illustrates the fundamental use of COMMIT
and ROLLBACK
. We first insert a new employee. If this operation is successful, we COMMIT
it, making the change permanent. Then, we attempt to insert the same employee ID again, which will raise a DUP_VAL_ON_INDEX
exception. In the exception handler, we issue a ROLLBACK
. Crucially, this ROLLBACK
only undoes the second (failed) INSERT
statement, as the first one was already committed. This demonstrates how transactions ensure data integrity in Oracle PL/SQL by allowing units of work to be either fully applied or fully undone.
Example 2: ROLLBACK to SAVEPOINT
DECLARE
v_employee_id_1 NUMBER := 209;
v_employee_id_2 NUMBER := 210;
BEGIN
-- Insert the first employee
INSERT INTO employees (employee_id, first_name, last_name, email, hire_date, job_id, department_id, salary)
VALUES (v_employee_id_1, 'Bob', 'Johnson', 'BJOHNSON@example.com', SYSDATE, 'IT_PROG', 60, 6000);
DBMS_OUTPUT.PUT_LINE('Employee ' || v_employee_id_1 || ' inserted.');
SAVEPOINT before_second_insert; -- Set a savepoint
-- Insert the second employee
INSERT INTO employees (employee_id, first_name, last_name, email, hire_date, job_id, department_id, salary)
VALUES (v_employee_id_2, 'Carol', 'Davis', 'CDAVIS@example.com', SYSDATE, 'IT_PROG', 60, 6500);
DBMS_OUTPUT.PUT_LINE('Employee ' || v_employee_id_2 || ' inserted.');
-- Simulate an error condition
IF v_employee_id_2 = 210 THEN -- Just for demonstration, normally based on some logic
RAISE_APPLICATION_ERROR(-20001, 'Simulated error before commit.');
END IF;
COMMIT; -- This line will not be reached due to the simulated error
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Caught an error: ' || SQLERRM);
ROLLBACK TO before_second_insert; -- Rollback only to the savepoint
DBMS_OUTPUT.PUT_LINE('Rolled back to savepoint "before_second_insert".');
-- Now, optionally, commit the first insert if desired, or re-raise
COMMIT; -- Committing the first insert only
END;
/
-- Verify that employee 209 is committed, but 210 is not
-- SELECT employee_id, first_name, last_name FROM employees WHERE employee_id IN (209, 210);
Explanation SAVEPOINT
allows you to set a marker within a transaction. In this example, we insert employee_id_1
, then set a SAVEPOINT
. After inserting employee_id_2
, we simulate an error. When the error occurs, ROLLBACK TO before_second_insert
is executed. This statement only undoes the operations after the before_second_insert
savepoint, meaning employee_id_1
remains inserted, while employee_id_2
is rolled back. This provides granular control over long-running transactions in Oracle PL/SQL, allowing partial rollbacks without discarding all prior successful operations.
Example 3: Explicit Transaction Control with Procedures
-- Procedure to insert an employee (without commit)
CREATE OR REPLACE PROCEDURE insert_employee_proc (
p_employee_id NUMBER,
p_first_name VARCHAR2,
p_last_name VARCHAR2
) IS
BEGIN
INSERT INTO employees (employee_id, first_name, last_name, email, hire_date, job_id, department_id, salary)
VALUES (p_employee_id, p_first_name, p_last_name, p_first_name || '.' || p_last_name || '@example.com', SYSDATE, 'AC_ACCOUNT', 90, 4000);
DBMS_OUTPUT.PUT_LINE('Employee ' || p_employee_id || ' inserted by procedure.');
END;
/
-- Main anonymous block to control the transaction
DECLARE
v_emp_id_A NUMBER := 211;
v_emp_id_B NUMBER := 212;
BEGIN
-- Call the procedure multiple times
insert_employee_proc(v_emp_id_A, 'David', 'Brown');
insert_employee_proc(v_emp_id_B, 'Eve', 'Green');
-- If all calls succeed, commit the entire transaction
COMMIT;
DBMS_OUTPUT.PUT_LINE('All employee insertions committed.');
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
ROLLBACK; -- Rollback all changes made by the procedure calls
DBMS_OUTPUT.PUT_LINE('All pending changes rolled back.');
END;
/
Explanation In this example, we define a procedure insert_employee_proc
that performs an INSERT
but does not include a COMMIT
. This is a common and recommended practice: procedures should typically not commit or rollback themselves unless they represent a complete, atomic business transaction. Instead, the calling block (the anonymous PL/SQL block here) is responsible for managing the transaction. If both insert_employee_proc
calls succeed, the main block COMMIT
s the changes. If any error occurs within the main block or any called procedure, the ROLLBACK
in the exception handler will undo all insertions, ensuring transactional integrity across multiple operations in Oracle PL/SQL.
Example 4: Implied COMMIT/ROLLBACK at Session End
-- This example demonstrates what happens implicitly
-- It's not a code block to run, but a concept explanation.
-- Scenario 1: Successful PL/SQL Block Execution
-- If a PL/SQL block completes successfully without an explicit COMMIT or ROLLBACK,
-- the pending changes will be automatically committed by Oracle.
/*
BEGIN
INSERT INTO employees (employee_id, first_name, last_name, email, hire_date, job_id, department_id, salary)
VALUES (213, 'Frank', 'White', 'FWHITE@example.com', SYSDATE, 'PU_CLERK', 50, 3000);
DBMS_OUTPUT.PUT_LINE('Employee 213 inserted.');
-- No COMMIT here
END;
/
-- After execution, if no error, Employee 213 would be committed.
*/
-- Scenario 2: Unhandled Exception or Session Termination
-- If an unhandled exception occurs in a PL/SQL block,
-- or if the database session terminates abnormally,
-- all pending changes will be automatically rolled back by Oracle.
/*
BEGIN
INSERT INTO employees (employee_id, first_name, last_name, email, hire_date, job_id, department_id, salary)
VALUES (214, 'Grace', 'Black', 'GBLACK@example.com', SYSDATE, 'PU_CLERK', 50, 3000);
DBMS_OUTPUT.PUT_LINE('Employee 214 inserted.');
-- Simulate an unhandled exception
RAISE NO_DATA_FOUND; -- This will cause an unhandled exception
-- This line will not be reached
COMMIT;
END;
/
-- After execution, Employee 214 would be rolled back due to the unhandled exception.
*/
Explanation Oracle automatically manages transactions at the session level. If a PL/SQL block or a SQL statement completes successfully without an explicit COMMIT
or ROLLBACK
, the changes are implicitly COMMIT
ted. Conversely, if an unhandled exception occurs or the database session terminates abnormally (e.g., client disconnection), all pending changes are implicitly ROLLBACK
ed. While this automatic behavior exists, it's generally best practice to explicitly manage your transactions with COMMIT
and ROLLBACK
for clarity, control, and robust error handling in Oracle PL/SQL applications.
Example 5: SET TRANSACTION READ ONLY
DECLARE
v_total_salary NUMBER;
BEGIN
-- Set the transaction to read-only mode.
-- No DML operations (INSERT, UPDATE, DELETE) are allowed in this transaction.
SET TRANSACTION READ ONLY;
SELECT SUM(salary)
INTO v_total_salary
FROM employees
WHERE department_id = 60;
DBMS_OUTPUT.PUT_LINE('Total salary for department 60: ' || v_total_salary);
-- Attempting a DML operation will result in an error: ORA-01456
-- INSERT INTO employees (employee_id, first_name, last_name, email, hire_date, job_id, department_id, salary)
-- VALUES (215, 'Ian', 'King', 'IKING@example.com', SYSDATE, 'IT_PROG', 60, 5000);
-- This COMMIT is technically not needed for READ ONLY transactions,
-- but explicitly ends the transaction and releases read locks.
COMMIT;
DBMS_OUTPUT.PUT_LINE('Read-only transaction committed (session returned to default mode).');
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
-- A ROLLBACK or COMMIT will end the read-only transaction mode.
ROLLBACK;
END;
/
Explanation The SET TRANSACTION READ ONLY
statement is used to define a transaction where no DML operations are allowed. This is particularly useful for reports or queries that need to see a consistent snapshot of the data without the risk of accidentally modifying it. Once set, any INSERT
, UPDATE
, or DELETE
attempt will raise an ORA-01456
error. A COMMIT
or ROLLBACK
statement (or session end) terminates the read-only transaction and returns the session to its default read-write mode. This ensures data integrity and consistency for read-only analytical operations in Oracle database environments.
Implicit Cursors (SQL%FOUND, SQL%NOTFOUND, SQL%ROWCOUNT, SQL%ISOPEN)
Whenever you execute a DML statement (INSERT
, UPDATE
, DELETE
) or a SELECT ... INTO
statement in PL/SQL, Oracle implicitly opens a cursor to process that SQL statement. You don't explicitly declare or open these cursors; they are managed automatically by the PL/SQL engine. However, PL/SQL provides a set of attributes, known as implicit cursor attributes, that allow you to check the outcome of these implicit SQL operations. These attributes are extremely valuable for flow control, error checking, and providing feedback to users.
Note: Implicit cursor attributes are crucial for understanding the success or failure of your DML and single-row SELECT
statements. They provide direct feedback on how many rows were affected, whether a row was found, and the status of the implicit cursor. Mastering these attributes is essential for writing robust and responsive Oracle PL/SQL applications.
SQL%FOUND
SQL%FOUND is a boolean attribute that returns TRUE
if an INSERT
, UPDATE
, or DELETE
statement affected at least one row, or if a SELECT ... INTO
statement returned one or more rows. It returns FALSE
otherwise.
Example 1: SQL%FOUND with UPDATE
DECLARE
v_employee_id NUMBER := 207; -- Assuming this employee exists from previous examples
v_new_salary NUMBER := 8500;
BEGIN
UPDATE employees
SET salary = v_new_salary
WHERE employee_id = v_employee_id;
IF SQL%FOUND THEN
DBMS_OUTPUT.PUT_LINE('Salary for employee ' || v_employee_id || ' updated successfully.');
COMMIT;
ELSE
DBMS_OUTPUT.PUT_LINE('Employee ' || v_employee_id || ' not found, no salary update performed.');
ROLLBACK;
END IF;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
ROLLBACK;
END;
/
Explanation After the UPDATE
statement executes, SQL%FOUND
is checked. If the UPDATE
statement affected one or more rows (meaning an employee with v_employee_id
was found and updated), SQL%FOUND
will be TRUE
, and a success message is displayed, followed by a COMMIT
. If no rows were updated (employee not found), SQL%FOUND
will be FALSE
, and a "not found" message is displayed, then ROLLBACK
is issued. This demonstrates how to use SQL%FOUND
to confirm the success of DML operations in Oracle PL/SQL.
Example 2: SQL%FOUND with DELETE
DECLARE
v_employee_id NUMBER := 999; -- Employee ID that likely does not exist
BEGIN
DELETE FROM employees
WHERE employee_id = v_employee_id;
IF SQL%FOUND THEN
DBMS_OUTPUT.PUT_LINE('Employee ' || v_employee_id || ' deleted successfully.');
COMMIT;
ELSE
DBMS_OUTPUT.PUT_LINE('Employee ' || v_employee_id || ' not found, no deletion performed.');
ROLLBACK;
END IF;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
ROLLBACK;
END;
/
Explanation Similar to the UPDATE
example, this block uses SQL%FOUND
after a DELETE
statement. If employee_id = 999
exists and is deleted, SQL%FOUND
will be TRUE
. If it doesn't exist, SQL%FOUND
will be FALSE
. This provides immediate feedback on whether the deletion operation was successful in terms of affecting any rows, which is crucial for controlling application flow in Oracle PL/SQL.
SQL%NOTFOUND
SQL%NOTFOUND is the opposite of SQL%FOUND
. It returns TRUE
if an INSERT
, UPDATE
, or DELETE
statement affected zero rows, or if a SELECT ... INTO
statement returned no rows. It returns FALSE
otherwise.
Example 1: SQL%NOTFOUND with SELECT INTO
DECLARE
v_employee_name VARCHAR2(100);
v_employee_id NUMBER := 1; -- Employee ID that likely does not exist in typical HR schema
BEGIN
SELECT first_name || ' ' || last_name
INTO v_employee_name
FROM employees
WHERE employee_id = v_employee_id;
DBMS_OUTPUT.PUT_LINE('Employee found: ' || v_employee_name);
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- This is where SQL%NOTFOUND for SELECT...INTO would implicitly occur
-- if we didn't handle NO_DATA_FOUND explicitly.
-- However, it's generally better to use the exception handler for NO_DATA_FOUND
-- for SELECT...INTO, as it's a specific error condition.
DBMS_OUTPUT.PUT_LINE('Employee ID ' || v_employee_id || ' not found.');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END;
/
-- Alternative usage of SQL%NOTFOUND for SELECT...INTO (less common for NO_DATA_FOUND, but valid)
DECLARE
v_employee_name VARCHAR2(100);
v_employee_id NUMBER := 1;
BEGIN
BEGIN
SELECT first_name || ' ' || last_name
INTO v_employee_name
FROM employees
WHERE employee_id = v_employee_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
v_employee_name := NULL; -- Assign NULL if not found
END;
IF SQL%NOTFOUND THEN -- This will be true if the inner SELECT...INTO resulted in NO_DATA_FOUND
DBMS_OUTPUT.PUT_LINE('Employee ID ' || v_employee_id || ' not found using SQL%NOTFOUND check.');
ELSE
DBMS_OUTPUT.PUT_LINE('Employee found: ' || v_employee_name || ' using SQL%NOTFOUND check.');
END IF;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END;
/
Explanation For SELECT ... INTO
, if no rows are returned, a NO_DATA_FOUND
exception is raised. While SQL%NOTFOUND
will technically be TRUE
in this scenario, it's generally preferred to handle NO_DATA_FOUND
with an explicit exception handler for clarity and proper error management. The second part of the example shows a less common but valid way to use SQL%NOTFOUND
after a SELECT...INTO
by suppressing NO_DATA_FOUND
with a nested block and then checking the attribute. This illustrates how SQL%NOTFOUND
provides insights into the results of single-row queries in Oracle PL/SQL.
Example 2: SQL%NOTFOUND with INSERT (after checking for existence)
DECLARE
v_new_employee_id NUMBER := 216;
v_first_name VARCHAR2(50) := 'Zoe';
v_last_name VARCHAR2(50) := 'Miller';
v_exists_check NUMBER;
BEGIN
-- Check if employee already exists before inserting
BEGIN
SELECT 1 INTO v_exists_check FROM employees WHERE employee_id = v_new_employee_id;
DBMS_OUTPUT.PUT_LINE('Employee ID ' || v_new_employee_id || ' already exists. Skipping insert.');
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- Employee does not exist, proceed with insert
INSERT INTO employees (employee_id, first_name, last_name, email, hire_date, job_id, department_id, salary)
VALUES (v_new_employee_id, v_first_name, v_last_name, v_first_name || '.' || v_last_name || '@example.com', SYSDATE, 'ST_CLERK', 50, 3000);
IF SQL%NOTFOUND THEN -- This would typically be FALSE after a successful insert
DBMS_OUTPUT.PUT_LINE('Unexpected: Insert affected no rows.');
ELSE
DBMS_OUTPUT.PUT_LINE('Employee ' || v_new_employee_id || ' inserted successfully.');
END IF;
COMMIT;
END;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
ROLLBACK;
END;
/
Explanation This code demonstrates a common pattern: check for existence before inserting to prevent DUP_VAL_ON_INDEX
. If the SELECT
statement finds no data (meaning NO_DATA_FOUND
is raised and handled), then the INSERT
proceeds. After a successful INSERT
, SQL%NOTFOUND
would logically be FALSE
because at least one row was affected. This usage of SQL%NOTFOUND
confirms whether the INSERT
operation successfully added a row, which is important for logic branches in Oracle database programming.
SQL%ROWCOUNT
SQL%ROWCOUNT is a numeric attribute that returns the number of rows affected by the most recent INSERT
, UPDATE
, or DELETE
statement, or the number of rows returned by a SELECT ... INTO
statement (which will always be 1 for a successful single-row query).
Example 1: SQL%ROWCOUNT with UPDATE
DECLARE
v_department_id NUMBER := 60;
v_increase_pct NUMBER := 0.05; -- 5% increase
BEGIN
UPDATE employees
SET salary = salary * (1 + v_increase_pct)
WHERE department_id = v_department_id;
DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT || ' employees in department ' || v_department_id || ' had their salaries updated.');
IF SQL%ROWCOUNT > 0 THEN
COMMIT;
ELSE
DBMS_OUTPUT.PUT_LINE('No employees found in department ' || v_department_id || ' to update.');
ROLLBACK;
END IF;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
ROLLBACK;
END;
/
Explanation After updating salaries for all employees in department_id = 60
, SQL%ROWCOUNT
provides the exact number of rows that were modified. This information is then used to inform the user and decide whether to COMMIT
the transaction (if any rows were updated) or ROLLBACK
(if no rows were found to update), providing precise control over bulk operations in Oracle PL/SQL.
Example 2: SQL%ROWCOUNT with DELETE
DECLARE
v_job_id VARCHAR2(10) := 'IT_PROG';
BEGIN
DELETE FROM job_history
WHERE job_id = v_job_id
AND end_date < ADD_MONTHS(SYSDATE, -12); -- Delete history older than 1 year
DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT || ' old job history records for job ' || v_job_id || ' deleted.');
IF SQL%ROWCOUNT > 0 THEN
COMMIT;
ELSE
DBMS_OUTPUT.PUT_LINE('No old job history records found for job ' || v_job_id || ' to delete.');
ROLLBACK;
END IF;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
ROLLBACK;
END;
/
Explanation This example demonstrates using SQL%ROWCOUNT
after a DELETE
operation. It precisely tells us how many rows were removed from the job_history
table based on the specified criteria. This is invaluable for reporting on the impact of DML operations and for making conditional decisions, such as whether to COMMIT
or ROLLBACK
, based on the actual number of affected rows in your Oracle database.
SQL%ISOPEN
SQL%ISOPEN is a boolean attribute that always returns FALSE
for implicit cursors. This is because Oracle automatically closes implicit cursors immediately after the SQL statement has been processed. While it exists, it's generally not used for implicit cursors in practical PL/SQL programming because their lifecycle is fully managed by the database. It is primarily relevant for explicit cursors, which you explicitly open and close.
Example 1: SQL%ISOPEN for Implicit Cursor (Always FALSE)
DECLARE
v_employee_name VARCHAR2(100);
v_salary NUMBER;
BEGIN
SELECT first_name || ' ' || last_name, salary
INTO v_employee_name, v_salary
FROM employees
WHERE employee_id = 100;
-- SQL%ISOPEN will always be FALSE immediately after an implicit SQL statement.
IF SQL%ISOPEN THEN
DBMS_OUTPUT.PUT_LINE('Implicit cursor is open.');
ELSE
DBMS_OUTPUT.PUT_LINE('Implicit cursor is closed (as expected).');
END IF;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('An error occurred: ' || SQLERRM);
END;
/
Explanation This code snippet illustrates that SQL%ISOPEN
will always evaluate to FALSE
immediately after an implicit SQL statement completes. This confirms the automatic management of implicit cursors by Oracle, where they are opened, processed, and then closed instantly. While not directly useful for controlling implicit cursor behavior, understanding this attribute helps differentiate between implicit and explicit cursor handling in Oracle PL/SQL development.