Chapter 07 · Final Chapter

Locking, Concurrency & Contention

This is where production emergencies happen. A system can have perfect indexes, optimal memory, and fast storage — and still grind to a halt because 200 sessions are blocked behind one uncommitted transaction. Contention is the hardest class of performance problem to diagnose because it appears suddenly and is invisible to most monitoring tools.

MVCC model Enqueue types Blocking chains Latch contention Deadlocks FK index trap SKIP LOCKED 3AM emergency toolkit

Oracle's MVCC Locking Model

Oracle's locking philosophy is fundamentally different from SQL Server and MySQL. Understanding it prevents you from misdiagnosing half of all contention issues.

💡 The three golden rules of Oracle locking
Readers never block writers. A SELECT never prevents an UPDATE on the same row.
Writers never block readers. An uncommitted UPDATE never blocks a SELECT — readers see the pre-update version from undo.
Writers CAN block other writers on the same row — but ONLY on the same row, not the same page or the same table.

Lock information in Oracle is stored in the data block itself (the ITL — Interested Transaction List), not in a central lock table. This is why Oracle can handle thousands of concurrent DML operations with minimal overhead.

Session B wants to → A holds SELECT A holds UPDATE/DELETE A holds INSERT A runs DDL
New SELECT (any row) OK OK OK OK
UPDATE same row OK WAIT OK WAIT
UPDATE different row OK OK OK WAIT
DDL (ALTER TABLE) OK WAIT WAIT WAIT
💡 Row-level vs page-level
Oracle locks at the row level, not the page level. Session A updating row 100 does NOT block Session B updating row 200 in the same block — even in the same table. When you see mass blocking in Oracle, it is almost always: same exact row contention, table-level DDL conflict, or a sequence/index hot block.

The Enqueue System

Every named lock in Oracle is called an enqueue. The wait event name tells you exactly which enqueue is the problem: enq: XX - description where XX is the enqueue type. Click each to expand the diagnosis and fix.

SQL — All enqueue waits right now with full details
-- All enqueue waits right now — decode the lock type from P1
SELECT s.sid, s.username, s.event,
       s.seconds_in_wait,
       CHR(BITAND(s.p1,-16777216)/16777215) ||
       CHR(BITAND(s.p1, 16711680)/65535)    AS lock_type,
       s.blocking_session,
       b.username                               AS blocking_user,
       b.sql_id                                 AS blocking_sql,
       b.status                                 AS blocker_status,
       b.last_call_et                           AS blocker_idle_secs
FROM   v$session s
LEFT JOIN v$session b ON b.sid = s.blocking_session
WHERE  s.event LIKE 'enq:%'
ORDER BY s.seconds_in_wait DESC;

Blocking Chain Analysis

In production, one session often blocks many others, which in turn block more — a cascade. Standard blocking queries only show direct blockers. You need to see the whole tree.

SQL — Full blocking tree + blocker summary
-- Full blocking chain tree (A blocks B which blocks C which blocks D)
SELECT  LPAD(' ', 2*(LEVEL-1)) || s.sid  AS sid_tree,
        s.username, s.status, s.event,
        s.seconds_in_wait, s.blocking_session,
        s.sql_id, s.last_call_et AS idle_secs,
        s.machine
FROM    v$session s
START WITH s.blocking_session IS NULL
       AND s.sid IN (
           SELECT DISTINCT blocking_session
           FROM   v$session
           WHERE  blocking_session IS NOT NULL)
CONNECT BY PRIOR s.sid = s.blocking_session
ORDER SIBLINGS BY s.seconds_in_wait DESC;

-- Quick summary: how many sessions does each root blocker hold up?
SELECT b.sid, b.serial#, b.username,
       b.status, b.last_call_et     AS idle_secs,
       b.machine, b.program, b.sql_id,
       COUNT(w.sid)                 AS sessions_blocked,
       MAX(w.seconds_in_wait)       AS max_victim_wait
FROM   v$session w
JOIN   v$session b ON b.sid = w.blocking_session
WHERE  w.blocking_session IS NOT NULL
GROUP BY b.sid, b.serial#, b.username,
         b.status, b.last_call_et, b.machine, b.program, b.sql_id
ORDER BY sessions_blocked DESC;

Reading the blocker patterns

Blocker statusIdle secsWhat it meansAction
INACTIVE> 60Session did DML and stopped — app crash or user walked awayKill session after confirming with app team
ACTIVELowSession is running a long transaction — still workingLet it finish, or investigate if it's stuck
ACTIVE> 300Session appears stuck — may itself be blocked on something elseCheck blocker's own wait event — trace upstream
KILLEDAnySession was killed but PMON is still rolling back undoWait — rollback in progress. Check V$TRANSACTION for progress.
SQL — Rollback progress for a killed session
-- Check rollback progress for a killed/long-running session
SELECT t.addr, t.start_time,
       t.used_ublk                       AS undo_blocks_remaining,
       t.used_urec                       AS undo_records_remaining,
       ROUND(t.used_ublk * 8 / 1024, 1)  AS undo_mb_to_rollback,
       rs.name                           AS rollback_segment,
       s.username, s.machine
FROM   v$transaction t
JOIN   v$session     s  ON s.taddr = t.addr
JOIN   v$rollstat    r  ON r.usn   = t.xidusn
JOIN   v$rollname    rs ON rs.usn  = t.xidusn
ORDER BY t.used_ublk DESC;
-- Large undo_mb_to_rollback = long wait ahead — cannot be cancelled

Latch Contention

Latches are lightweight internal locks protecting Oracle's in-memory data structures. Unlike enqueues which queue and wait, latches use spinning — a process retries thousands of times per second. This means latch contention burns CPU even when sessions appear to be "waiting."

SQL — Find latches with high miss rates
-- System-wide latch miss rates (miss_pct > 0.5% needs attention)
SELECT name, gets, misses, sleeps,
       ROUND(misses * 100 / NULLIF(gets, 0), 3)    AS miss_pct,
       ROUND(sleeps * 100 / NULLIF(misses, 0), 1)  AS sleep_per_miss
FROM   v$latch
WHERE  misses > 0 AND gets > 1000
ORDER BY misses DESC
FETCH FIRST 15 ROWS ONLY;

-- Child latch breakdown for cache buffers chains (hot block diagnosis)
SELECT name, child#, gets, misses, sleeps,
       ROUND(misses*100/NULLIF(gets,0),3) AS miss_pct
FROM   v$latch_children
WHERE  name = 'cache buffers chains'
AND    misses > 0
ORDER BY misses DESC
FETCH FIRST 10 ROWS ONLY;
latch: cache buffers chains
Hot block — many sessions hitting same buffer simultaneously
Find hot segment via V$SESSION P2 (block#). Fix: increase sequence CACHE size, use reverse key index for monotonic inserts, partition the hot object.
latch: shared pool
Hard parses flooding the shared pool
Enforce bind variables. Pin critical packages with DBMS_SHARED_POOL.KEEP. Increase SHARED_POOL_SIZE if fragmented.
latch: library cache
Simultaneous hard parses of the same SQL
Bind variables + increase SESSION_CACHED_CURSORS (200–500). Reduces library cache latch gets by caching parsed cursors per session.
latch: redo copy / allocation
Very high commit rate / tiny redo entries
Batch commits. Increase LOG_BUFFER. Set LOG_SIMULTANEOUS_COPIES = 2× CPU count. Increase redo log group members.
💡 SESSION_CACHED_CURSORS — the hidden latch reducer
Default = 50. Each session caches up to N parsed cursors. On re-execution, no latch needed — cursor found in session cache directly. Increasing to 200–500 dramatically reduces library cache latch gets on systems where the same SQL runs thousands of times per second. ALTER SYSTEM SET session_cached_cursors = 300 SCOPE=SPFILE;

Library Cache Lock & Library Cache Pin

These are among the most disruptive wait events because they block the entire application during a DDL operation. They cause the classic deployment outage where "recompiling a package hung the system for 60 seconds."

SQL — Find and diagnose library cache contention
-- Sessions waiting on library cache events + what object is held
SELECT s.sid, s.username, s.event,
       s.seconds_in_wait, s.sql_id,
       s.p1raw    AS handle_address,
       s.p2       AS lock_mode
FROM   v$session s
WHERE  s.event IN ('library cache lock', 'library cache pin')
ORDER BY s.seconds_in_wait DESC;

-- Identify WHAT object is being held (plug p1raw value in)
SELECT kglnaown AS owner,
       kglnaobj AS object_name,
       kglobtyp AS object_type
FROM   x$kglob
WHERE  kglhdadr = '&handle_address';

-- Who is actively using that object right now?
SELECT s.sid, s.username, s.status,
       s.sql_id, s.last_call_et, s.machine
FROM   v$session  s
JOIN   v$access   a ON a.sid = s.sid
WHERE  a.object = 'YOUR_PACKAGE_NAME'
AND    a.type   IN ('PACKAGE', 'PROCEDURE', 'FUNCTION', 'TABLE');
🔎 Real scenario — deployment causes 60-second outage
500 sessions are actively calling PRICING_ENGINE_PKG. The deployment team runs ALTER PACKAGE pricing_engine_pkg COMPILE. This tries to get an exclusive library cache lock. All 500 sessions immediately wait on library cache pin. New sessions pile up waiting on library cache lock. The system appears hung for 30–90 seconds. Fix: always check V$ACCESS before running DDL in business hours. Only compile during confirmed quiet windows, or use Edition-Based Redefinition for zero-downtime deploys.

Deadlocks

Oracle automatically detects deadlocks and resolves them by rolling back one statement (not the whole transaction) and raising ORA-00060. But deadlocks indicate an application design problem — fix the root cause, don't just catch the exception.

SQL — Deadlock detection and prevention
-- Deadlocks are recorded in the alert log AND trace files
-- Find trace file location:
SELECT value FROM v$parameter WHERE name = 'diagnostic_dest';
-- Look in: <diag_dest>/diag/rdbms/<dbname>/<instance>/trace/
-- File: <instance>_ora_<pid>.trc containing word "deadlock"

-- Deadlock frequency — spikes in user_rollbacks correlate with ORA-00060
SELECT name, value FROM v$sysstat
WHERE  name = 'user rollbacks';

-- THE FIX: enforce consistent row-access order in the application
-- Pattern that causes deadlock:
--   Session A: UPDATE orders SET status='X' WHERE id=100  (locks row 100)
--   Session B: UPDATE orders SET status='Y' WHERE id=200  (locks row 200)
--   Session A: UPDATE orders SET status='Z' WHERE id=200  (waits for B)
--   Session B: UPDATE orders SET status='W' WHERE id=100  (waits for A → DEADLOCK)

-- Fix: always lock rows in ascending primary key order
-- Both sessions update rows in order 100 then 200 → one waits, no deadlock

-- For queue processing: use SKIP LOCKED (see section 7.9)
SELECT order_id, payload FROM job_queue
WHERE  status = 'PENDING' AND ROWNUM <= 100
FOR UPDATE SKIP LOCKED;

Missing Foreign Key Index Trap

This is one of the most common and least-known causes of table-level lock contention. When you DELETE from a parent table, Oracle takes a full table lock on the child table if there is no index on the foreign key column.

🚨 How it works
When you run DELETE FROM customers WHERE customer_id = 101, Oracle must verify no child rows exist in ORDERS. Without an index on orders.customer_id, Oracle scans the entire ORDERS table and takes a TM mode-4 (SHARE) lock on it. Every other session trying to INSERT/UPDATE/DELETE in ORDERS now waits on enq: TM - contention.
SQL — Find ALL unindexed foreign keys (run on every production DB)
-- THE essential query — run this on every production database immediately
SELECT c.owner, c.table_name,
       c.constraint_name       AS fk_name,
       cc.column_name,
       p.table_name            AS parent_table,
       p.constraint_name       AS pk_name
FROM   dba_constraints   c
JOIN   dba_cons_columns  cc ON cc.constraint_name = c.constraint_name
                          AND cc.owner           = c.owner
JOIN   dba_constraints   p  ON p.constraint_name  = c.r_constraint_name
                          AND p.owner            = c.r_owner
WHERE  c.constraint_type = 'R'   -- R = foreign key
AND    c.owner NOT IN ('SYS','SYSTEM','DBSNMP','MDSYS')
-- Now exclude FKs that already have an index on the FK column:
AND NOT EXISTS (
  SELECT 1
  FROM   dba_ind_columns ic
  WHERE  ic.table_name    = c.table_name
  AND    ic.table_owner   = c.owner
  AND    ic.column_name   = cc.column_name
  AND    ic.column_position = cc.position
)
ORDER BY c.owner, c.table_name;
⚠ Before creating all missing FK indexes
Evaluate each one: (1) Is the parent table ever DELETEd from in production? If parents are only inserted, the TM lock never fires. (2) Is the child table large? Small tables may not need the index. (3) Will the index slow DML on the child table without helping queries? Index only if DELETEs on the parent are confirmed.

Sequence Contention

In a high-throughput OLTP system, sequence contention is one of the top performance killers and is almost always fixable in minutes. Every call to NEXTVAL that exhausts the cache must update the sequence header block — serialising all concurrent callers.

SQL — Diagnose and fix sequence contention
-- Step 1: Is enq: SQ - contention appearing in top waits?
SELECT event, total_waits, time_waited, average_wait
FROM   v$system_event
WHERE  event = 'enq: SQ - contention';

-- Step 2: Find sequences with dangerously small cache sizes
SELECT sequence_owner, sequence_name,
       cache_size, order_flag, cycle_flag
FROM   dba_sequences
WHERE  sequence_owner NOT IN ('SYS','SYSTEM')
AND    cache_size < 100
ORDER BY cache_size ASC;

-- Step 3: Apply fix — dramatically increase cache size
-- Rule: cache = peak_inserts_per_second × 10 (10 seconds of headroom)
-- Example: 500 inserts/sec peak → CACHE 5000
ALTER SEQUENCE apps.orders_seq     CACHE 1000;
ALTER SEQUENCE apps.customers_seq  CACHE 1000;
ALTER SEQUENCE apps.events_seq     CACHE 5000;   -- high volume
-- Takes effect immediately — no restart, no data loss

SKIP LOCKED — Eliminate Queue Contention

This pattern eliminates enq: TX - row lock contention entirely for queue-style processing tables. Multiple workers each get a unique non-overlapping set of rows — zero blocking between them.

SQL — SKIP LOCKED queue processing pattern
-- WITHOUT skip locked: all workers fight over the same rows → contention
SELECT job_id, payload FROM job_queue
WHERE  status = 'PENDING' AND ROWNUM <= 10
FOR UPDATE;   -- all 10 workers try to lock the same 10 rows

-- WITH skip locked: each worker gets unique rows → zero contention
SELECT job_id, payload FROM job_queue
WHERE  status = 'PENDING' AND ROWNUM <= 10
FOR UPDATE SKIP LOCKED;   -- each worker skips rows locked by others

-- Full PL/SQL worker pattern
DECLARE
  CURSOR c_jobs IS
    SELECT job_id, payload
    FROM   job_queue
    WHERE  status = 'PENDING'
    AND    ROWNUM <= 50
    FOR UPDATE SKIP LOCKED;
BEGIN
  FOR r IN c_jobs LOOP
    process_job(r.job_id, r.payload);
    UPDATE job_queue
    SET    status = 'DONE',
           processed_at = SYSTIMESTAMP
    WHERE  job_id = r.job_id;
  END LOOP;
  COMMIT;
END;
/

-- NOWAIT and WAIT variants for explicit lock control
SELECT order_id FROM orders WHERE order_id = 101
FOR UPDATE NOWAIT;    -- fail immediately if locked (raises ORA-00054)

SELECT order_id FROM orders WHERE order_id = 101
FOR UPDATE WAIT 5;    -- wait up to 5 seconds then fail

The Complete Contention Emergency Toolkit

Run these queries in exact order when your phone rings saying "everything is blocked." Each query answers the next diagnostic question.

SQL — Complete 5-query emergency sequence
-- ============================================================
-- QUERY 1: How many sessions are blocked and what event?
-- ============================================================
SELECT wait_class, event,
       COUNT(*)                   AS blocked_sessions,
       MAX(seconds_in_wait)        AS max_wait_secs,
       ROUND(AVG(seconds_in_wait), 1) AS avg_wait_secs
FROM   v$session
WHERE  blocking_session IS NOT NULL
GROUP BY wait_class, event
ORDER BY blocked_sessions DESC;

-- ============================================================
-- QUERY 2: Who is the root blocker? (most victims first)
-- ============================================================
SELECT b.sid, b.serial#, b.username,
       b.status, b.last_call_et    AS idle_secs,
       b.machine, b.program, b.sql_id,
       COUNT(w.sid)                AS sessions_blocked,
       MAX(w.seconds_in_wait)      AS max_victim_wait
FROM   v$session w
JOIN   v$session b ON b.sid = w.blocking_session
WHERE  w.blocking_session IS NOT NULL
GROUP BY b.sid, b.serial#, b.username,
         b.status, b.last_call_et, b.machine, b.program, b.sql_id
ORDER BY sessions_blocked DESC;

-- ============================================================
-- QUERY 3: What was the blocker's last SQL?
-- ============================================================
SELECT s.sid, s.username, s.status, s.sql_id,
       sq.sql_text,
       s.prev_sql_id,
       psq.sql_text AS prev_sql_text
FROM   v$session  s
LEFT JOIN v$sqlarea sq  ON sq.sql_id  = s.sql_id
LEFT JOIN v$sqlarea psq ON psq.sql_id = s.prev_sql_id
WHERE  s.sid = &blocker_sid;

-- ============================================================
-- QUERY 4: What transaction does the blocker have open?
-- ============================================================
SELECT t.start_time, t.status,
       t.used_ublk                    AS undo_blocks,
       ROUND(t.used_ublk*8/1024,1)   AS undo_mb,
       t.log_io, t.phy_io,
       s.username, s.machine
FROM   v$transaction t
JOIN   v$session     s ON s.taddr = t.addr
WHERE  s.sid = &blocker_sid;

-- ============================================================
-- QUERY 5 (if needed): Kill the blocker
-- Use IMMEDIATE to signal the session to die NOW
-- ============================================================
ALTER SYSTEM KILL SESSION '&sid,&serial#' IMMEDIATE;

-- Verify all blocking cleared
SELECT COUNT(*) AS still_blocked
FROM   v$session
WHERE  blocking_session IS NOT NULL;

-- Post-incident: capture what happened from ASH history
SELECT TO_CHAR(sample_time,'HH24:MI:SS') AS time,
       session_id, event, blocking_session, sql_id
FROM   v$active_session_history
WHERE  sample_time BETWEEN :incident_start AND :incident_end
AND    blocking_session IS NOT NULL
ORDER BY sample_time, session_id;

Prevention Strategies

Diagnosing and killing blockers is tactical. Preventing them is strategic.

Set IDLE_TIME profile
Auto-kill sessions idle more than N minutes. Prevents abandoned transactions from holding locks indefinitely. CREATE PROFILE app_profile LIMIT IDLE_TIME 30;
Use DDL lock timeout
Prevents DDL from waiting forever on a locked object. ALTER SESSION SET ddl_lock_timeout = 30; DDL waits 30s then raises ORA-00054 instead of hanging.
Use FOR UPDATE NOWAIT / WAIT N
Application-level lock timeout. FOR UPDATE NOWAIT fails immediately if locked. FOR UPDATE WAIT 5 waits 5 seconds then raises ORA-00054. Implement retry logic in the application.
Monitor long open transactions
Any transaction open > 10 minutes in OLTP is suspicious. Alert on it before it becomes a blocking incident. Query V$TRANSACTION for transactions older than your threshold.
SQL — Proactive contention prevention queries
-- Monitor long-running uncommitted transactions (run as an alert job)
SELECT s.sid, s.username, s.status,
       t.start_time,
       ROUND((SYSDATE -
         TO_DATE(t.start_time,'MM/DD/YY HH24:MI:SS'))*24*60,1)
         AS txn_minutes,
       t.used_ublk, t.used_urec,
       s.machine, s.program
FROM   v$transaction t
JOIN   v$session     s ON s.taddr = t.addr
WHERE (SYSDATE -
       TO_DATE(t.start_time,'MM/DD/YY HH24:MI:SS'))*24*60 > 10
ORDER BY txn_minutes DESC;

-- Create session idle timeout profile
CREATE PROFILE app_user_profile LIMIT
  IDLE_TIME          30    -- kill sessions idle > 30 minutes
  CONNECT_TIME       480   -- max 8 hours connected
  SESSIONS_PER_USER  50;   -- max concurrent sessions per user

ALTER USER app_user PROFILE app_user_profile;

Self-Check Questions

  • Q1 Session A has been INACTIVE for 25 minutes holding a row lock. Forty sessions are blocked behind it. Before killing it, what three things should you verify? Why does ALTER SYSTEM KILL SESSION sometimes NOT immediately release the locks — what is Oracle doing internally?
  • Q2 Your DBA colleague says "we get a TM lock storm every night when the purge job runs DELETE FROM customers WHERE status = 'INACTIVE'." The ORDERS table has a foreign key to CUSTOMERS. What is the root cause and what is the single DDL statement that fixes it?
  • Q3 You add a sequence with CACHE 20 for a table receiving 2,000 inserts per second. After deployment, enq: SQ - contention appears in the top-5 wait events. Explain precisely why, calculate the correct cache size, and write the fix DDL.
  • Q4 A developer calls — a library cache pin wait is blocking 300 sessions for 90 seconds. The deployment team just ran ALTER PACKAGE pricing_engine_pkg COMPILE. What is happening internally in the shared pool, and what are your immediate options to resolve it?
  • Q5 You run the unindexed FK query on your production database and find 12 foreign keys without indexes. Before creating all 12 indexes, what three factors do you evaluate for each one to decide whether the index is actually needed?

Course Complete

What you now know — the full Oracle tuning stack
ChapterTopicCore skill gained
Ch 1ArchitectureInstance vs database, SGA/PGA layout, background processes, query lifecycle
Ch 2Wait eventsTime-based tuning model, wait classes, ASH analysis, 3AM diagnostic toolkit
Ch 3SQL OptimizerParse/bind/execute/fetch, CBO statistics, execution plans, bind peeking, SPM
Ch 4IndexingB-tree internals, clustering factor, composite ordering, covering indexes, 8 killer conditions
Ch 5MemoryBuffer cache advisor, shared pool, PGA work areas, ASMM vs AMM, HugePages
Ch 6I/O & StorageI/O taxonomy, redo log tuning, temp, checkpoints, partitioning, storage layout
Ch 7ContentionMVCC model, enqueues, latch contention, deadlocks, FK indexes, SKIP LOCKED

What to do next

Practice every diagnostic query on a real database — even a dev instance with DBMS_WORKLOAD_REPOSITORY and DBMS_SQLTUNE loaded.
Build your own runbook — a personal copy of the 3AM toolkits from each chapter tuned to your specific environment and schema names.
The next level: Oracle RAC adds the gc wait class (global cache) and Cache Fusion layer — everything from these 7 chapters applies, with RAC-specific contention on top.
Certification to target: Oracle Database 19c Performance Management and Tuning (1Z0-084).
Deep read: Cary Millsap's "Optimizing Oracle Performance" — the book that introduced time-based tuning. Still the best reference available.