Symptoms
- Users or app threads hang on simple updates.
- AWR/ASH or monitoring shows the wait event
enq: TX - row lock contentionat the top. - Connection pools fill up because sessions don't return.
Oracle locks rows, not pages or tables. A session waits on enq: TX - row lock contention when it wants to change a row that another session has changed but not yet committed. The cause is almost always an uncommitted transaction, not a database fault.
1. Who is blocked and who is blocking?
SELECT s.inst_id, s.sid, s.serial#, s.username, s.program, s.event,
s.seconds_in_wait, s.blocking_instance, s.blocking_session,
s.final_blocking_instance, s.final_blocking_session, s.sql_id
FROM gv$session s
WHERE s.blocking_session IS NOT NULL
ORDER BY s.seconds_in_wait DESC;
final_blocking_session points straight to the root blocker in a chain, even across RAC instances.
Look at the root blocker:
SELECT inst_id, sid, serial#, username, osuser, machine, program, module, action,
status, event, last_call_et, sql_id, prev_sql_id, logon_time
FROM gv$session
WHERE (inst_id, sid) IN ((&blocker_inst, &blocker_sid));
| Blocker state | Meaning |
|---|---|
status = INACTIVE, event SQL*Net message from client, large last_call_et | Changed rows and is waiting for the app; never committed (the most common case) |
ACTIVE running a long DML | Large update or delete still running |
ACTIVE, itself waiting on something else | Chain; the real problem is further along |
2. What SQL and which object?
The blocker's current SQL is often not the statement that took the lock. Check prev_sql_id too:
SELECT sql_id, sql_text FROM gv$sql
WHERE sql_id IN ('&sql_id', '&prev_sql_id') AND inst_id = &blocker_inst;
The object and row the waiter wants:
SELECT s.sid, o.owner, o.object_name, o.object_type,
s.row_wait_obj#, s.row_wait_file#, s.row_wait_block#, s.row_wait_row#
FROM gv$session s
JOIN dba_objects o ON o.object_id = s.row_wait_obj#
WHERE s.event = 'enq: TX - row lock contention';
Build the ROWID and fetch the row:
SELECT * FROM app.orders
WHERE rowid = DBMS_ROWID.ROWID_CREATE(1, &row_wait_obj#, &row_wait_file#, &row_wait_block#, &row_wait_row#);
For a data object ID different from the object ID (partitions, after moves), use dba_objects.data_object_id in ROWID_CREATE.
3. How long has the transaction been open?
SELECT s.inst_id, s.sid, s.username, t.start_time, t.used_urec AS undo_records, t.used_ublk AS undo_blocks
FROM gv$transaction t
JOIN gv$session s ON s.saddr = t.ses_addr AND s.inst_id = t.inst_id
ORDER BY t.start_time;
A transaction open for hours with few undo records is a forgotten session, often an ad-hoc tool (SQL Developer, Toad) with autocommit off.
4. Resolve it
Prefer this order:
- Ask the owner to commit or roll back. The session's
osuser,machineandprogramtell you who it is. - Kill the blocking session if the owner can't be reached and the business impact justifies it:
ALTER SYSTEM KILL SESSION '&sid,&serial#,@&inst_id' IMMEDIATE;
Warning
Killing the blocker rolls back its transaction. A large transaction can take a long time to roll back and keeps holding its locks until rollback finishes. Check
used_urecfirst, and tell the owner that their changes will be lost.
On Amazon RDS for Oracle, use EXEC rdsadmin.rdsadmin_util.kill(sid => &sid, serial => &serial);.
5. Other TX enqueue modes
The event name tells you the kind of contention:
| Event | Usual cause |
|---|---|
enq: TX - row lock contention | Same row changed by an uncommitted transaction; also unique key conflicts on insert, and unindexed foreign keys |
enq: TX - index contention | Index block splits on a hot right-hand index (sequence keys) |
enq: TX - allocate ITL entry | Too many concurrent transactions in one block; raise INITRANS |
enq: TM - contention | Unindexed foreign key columns when the parent table is updated or deleted |
Find unindexed foreign keys, which is a classic cause of TM and TX waits:
SELECT c.owner, c.table_name, cc.column_name, c.constraint_name
FROM dba_constraints c
JOIN dba_cons_columns cc ON cc.owner = c.owner AND cc.constraint_name = c.constraint_name
WHERE c.constraint_type = 'R' AND c.owner = 'APP'
AND NOT EXISTS (
SELECT 1 FROM dba_ind_columns ic
WHERE ic.table_owner = c.owner AND ic.table_name = c.table_name
AND ic.column_name = cc.column_name AND ic.column_position = cc.position);
6. History (after the event)
SELECT sample_time, session_id, blocking_session, sql_id, event, current_obj#
FROM dba_hist_active_sess_history
WHERE event = 'enq: TX - row lock contention'
AND sample_time > SYSTIMESTAMP - INTERVAL '1' DAY
ORDER BY sample_time;
ASH and AWR require the Diagnostics Pack licence. Without it, use Statspack or your own sampling of v$session.
Prevent it next time
- Applications commit promptly. No user think time inside a transaction.
- Idle-in-transaction limits: Oracle profiles (
IDLE_TIME) or app-side timeouts. Note that IDLE_TIME doesn't distinguish open transactions from idle sessions. - Index foreign key columns.
- Alert on sessions blocking others for longer than N minutes.
Checklist
- Root blocker found with final_blocking_session
- Blocker's SQL (current and previous), user and program identified
- Locked object and row known
- Owner contacted before killing
- Rollback size checked before kill
- Root cause fixed in the application or schema